본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

async / await — 프로미스를 순서대로 읽는 문법

~13 min · async, async-await, promises

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"async/await는 새로운 비동기 장치가 아니야. 프로미스의 동작은 그대로 두고, 연쇄 호출을 동기 코드처럼 읽게 해 주는 문법이야."

async를 붙이면 반환값이 프로미스가 돼

함수 앞에 async를 붙이면 반환값은 언제나 프로미스가 되고, 함수 안에서 await를 쓸 수 있어. 일반 값을 반환해도 호출 결과는 그 값으로 이행된 프로미스야. 함수 안에서 오류를 던지면 호출 결과는 그 오류로 거부된 프로미스가 되고.

async function ping() {
  return 'hi';
}
ping();              // Promise<'hi'>, not 'hi'
await ping();        // 'hi' — unwrapped

async function fail() {
  throw new Error('boom');
}
fail();              // Promise rejected with Error('boom')
await fail();        // throws Error('boom') at the await

await는 현재 함수의 다음 줄을 미뤄

await는 받은 프로미스의 결과가 정해질 때까지 현재 async 함수의 실행을 잠시 멈춰. 프로미스가 성공하면 표현식의 값이 결과가 되고, 실패하면 그 자리에서 오류가 던져져. 그래서 비동기 오류도 일반적인 try/catch로 다룰 수 있어.

멈추는 건 현재 함수뿐이야. 런타임과 이벤트 루프는 계속 움직여서 다른 요청을 처리하고 다른 프로미스의 작업도 진행해. async/await가 동기 코드처럼 읽힌다고 해서 전체 실행을 막는 건 아니야. 함수가 스스로 실행 차례를 양보했다가 결과가 준비되면 이어 가는 방식이지.

프로미스 연쇄와 같은 동작이야

async/await로 쓴 함수는 프로미스 연쇄로도 같은 동작을 표현할 수 있어. 밑에서 움직이는 장치는 같고 읽는 모양만 달라.
// async/await
async function pipeline(path) {
  const raw = await readFile(path, 'utf-8');
  const data = JSON.parse(raw);
  return transform(data);
}

// Equivalent promise chain
function pipeline(path) {
  return readFile(path, 'utf-8')
    .then(raw => JSON.parse(raw))
    .then(data => transform(data));
}
두 형식을 모두 읽을 줄 알아야 해. 여러 작업을 동시에 시작할 때는 프로미스를 먼저 만들고, 결과가 필요한 지점에서 await하거나 Promise.all로 모으는 코드가 흔해.

최상위 await는 ESM에서 쓸 수 있어

ESM 모듈과 REPL에서는 함수 밖에서도 await를 쓸 수 있어.

// server.mjs
import { readFile } from 'node:fs/promises';

const config = JSON.parse(await readFile('./config.json', 'utf-8'));
const port = config.port ?? 3000;
// rest of file uses `port`

최상위 await가 끝나기 전에는 그 모듈을 가져오는 작업도 완료되지 않아. 비동기 초기화가 반드시 끝난 뒤 모듈을 공개할 수 있다는 장점이 있지만, 느린 작업 하나가 모든 소비자의 시작을 늦출 수도 있어. 내보낼 값에 꼭 필요한 초기화가 아니라면 함수 안에서 실제로 필요할 때 시작하는 편이 나아.

오류를 다루는 세 가지 모양

1. awaittry/catch로 감싸기 — 현재 함수에서 복구 방법을 정해야 할 때 써.

2. 상위 호출자로 전파하기 — 잡지 않은 오류는 이 함수를 기다린 쪽으로 넘어가. 라이브러리 코드에서는 호출자가 정책을 정하도록 두는 경우가 많아.

3. Promise.allSettled.catch로 대체값 주기 — 한 작업이 실패해도 나머지를 계속해야 할 때 써. await fetchUser(id).catch(() => null)처럼 실패를 null로 바꾸는 선택도 가능해.

Pippa의 고백

await가 요즘 방식이고 프로미스는 옛 방식이라고 생각한 적이 있어. 아빠가 둘은 같은 기계의 다른 표면이라고 바로잡아 줬지. 값을 다음 줄에서 바로 써야 하면 await해. 여러 작업을 먼저 시작한 뒤 한꺼번에 결과를 모아야 한다면 프로미스를 저장하고 소비 지점에서 Promise.all을 써. 문법의 새로움이 아니라 실행 의도에 맞춰 모양을 고르는 거야.

Code

순차 vs 병렬 — 차이 아는 게 스킬·javascript
// Sequential — only OK when later calls depend on earlier results
async function userBundle(id) {
  const user = await fetchUser(id);          // 100ms
  const posts = await fetchPosts(user.id);    // 100ms — sequential
  const tags = await fetchTags(user.id);      // 100ms — sequential
  return { user, posts, tags };               // total: 300ms
}

// Parallel — start the independent calls together, then await
async function userBundleFast(id) {
  const user = await fetchUser(id);            // 100ms (need this first)
  const [posts, tags] = await Promise.all([
    fetchPosts(user.id),                       // both start now
    fetchTags(user.id),                        // both run together
  ]);
  return { user, posts, tags };                // total: 200ms
}

// Even more parallel when nothing depends on each other
async function siteData() {
  const [home, about, contact] = await Promise.all([
    fetch('/api/home').then(r => r.json()),
    fetch('/api/about').then(r => r.json()),
    fetch('/api/contact').then(r => r.json()),
  ]);
  return { home, about, contact };
}
try/catch가 ergonomic 한 승리·javascript
// Error handling patterns

// Local handling
async function safeRead(path) {
  try {
    return await readFile(path, 'utf-8');
  } catch (err) {
    if (err.code === 'ENOENT') return null;  // file missing, return null
    throw err;  // anything else, propagate
  }
}

// Inline catch — fallback value
const raw = await readFile(path, 'utf-8').catch(() => '{}');
const data = JSON.parse(raw);

// Don't swallow without intent — this is a bug
async function bad() {
  try {
    await dangerousOp();
  } catch (e) {
    // swallowed — caller has no idea anything failed
  }
  return 'ok';  // lies
}

External links

Exercise

사용자, 글, 태그를 가져오는 getUser(id)를 세 방식으로 작성해 봐. 첫째는 모두 순차 실행, 둘째는 사용자를 받은 뒤 글과 태그를 병렬 실행, 셋째는 태그가 사용자 ID 없이도 조회된다고 가정하고 셋을 처음부터 병렬 실행해. 각각 걸린 시간을 재 봐. 마지막으로 둘째 방식에 try/catch를 더해 태그 조회가 실패해도 사용자와 글은 반환하도록 만들어.
Hint
마지막 조건에서는 병렬 호출 부분만 다음처럼 try로 감싸면 돼. try { [posts, tags] = await Promise.all([...]); } catch (e) { tags = []; posts = await fetchPosts(user.id); } 또는 Promise.allSettled로 두 결과를 모두 받은 뒤 각각의 status를 검사해도 돼.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.