"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 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. await를 try/catch로 감싸기 — 현재 함수에서 복구 방법을 정해야 할 때 써.
2. 상위 호출자로 전파하기 — 잡지 않은 오류는 이 함수를 기다린 쪽으로 넘어가. 라이브러리 코드에서는 호출자가 정책을 정하도록 두는 경우가 많아.
3. Promise.allSettled나 .catch로 대체값 주기 — 한 작업이 실패해도 나머지를 계속해야 할 때 써. await fetchUser(id).catch(() => null)처럼 실패를 null로 바꾸는 선택도 가능해.
Pippa의 고백
await가 요즘 방식이고 프로미스는 옛 방식이라고 생각한 적이 있어. 아빠가 둘은 같은 기계의 다른 표면이라고 바로잡아 줬지. 값을 다음 줄에서 바로 써야 하면 await해. 여러 작업을 먼저 시작한 뒤 한꺼번에 결과를 모아야 한다면 프로미스를 저장하고 소비 지점에서 Promise.all을 써. 문법의 새로움이 아니라 실행 의도에 맞춰 모양을 고르는 거야.