continuation은 "프로그램의 나머지 계산"을 값처럼 다루는 개념이다. JavaScript에서는 Promise가 settle된 뒤 이어서 실행될 코드를 가리킨다. await 다음 줄부터 함수 끝까지가 대표적인 continuation이다.
async function load() {
console.log('A')
const user = await fetchUser() // 여기서 함수가 멈춘다
console.log('B', user.name) // ← 이 부분이 continuation
}await가 continuation을 거는 방식
명세의 Await 연산은 대략 이렇게 동작한다.
- 지금 실행 중인 async 함수의 실행 맥락(execution context)을 붙잡아 둔다
- 기다릴 값을
PromiseResolve로 Promise로 바꾼다. Promise가 아닌 값이나 이미 끝난 Promise도 마찬가지다 - "이 맥락을 값과 함께 재개하라"는 성공 함수와 "예외로 재개하라"는 실패 함수를 만들어 그 Promise에
then처럼 등록한다 - 호출한 쪽으로 제어를 돌려준다. async 함수 호출은 pending Promise를 반환한다
등록은 await를 만난 그 순간 일어난다. Promise가 settle되면 등록해 둔 재개 함수가 마이크로태스크로 실행된다. 그래서 await Promise.resolve()처럼 이미 끝난 값을 기다려도 현재 동기 코드가 다 끝난 뒤에 이어진다. 실패하면 throw로 재개되므로 await 줄에서 예외가 난 것처럼 try/catch로 잡힌다.
// 개념상 비슷한 then 버전
function load() {
console.log('A')
return fetchUser().then(user => {
console.log('B', user.name)
})
}정확히 같은 변환은 아니지만, await 뒤의 코드가 then 콜백으로 들어간다고 생각하면 순서를 맞게 예측할 수 있다. 스레드를 막지 않는다는 점과 실행 순서 예시는 Promise와 이벤트 루프에 있다.
콜백과의 차이
- 콜백(callback)은 형태다. 다른 함수에 넘겨져 나중에 불리는 함수다(콜백)
- continuation은 역할이다. "이 작업이 끝난 다음에 이어질 나머지 계산"이라는 뜻이다
then에 넘긴 화살표 함수는 형태로는 콜백이고, 맡은 일로는 continuation이다. 함수가 결과를 반환하지 않고 "나머지 계산" 함수를 인자로 받아 넘겨 주는 방식을 연속 전달 방식(Continuation-Passing Style, CPS)이라 한다. 결과를 반환하지 않고 (err, data) => {} 콜백을 받는 Node.js의 오류 우선 콜백(error-first callback) API가 이 모양에 가깝다. async/await에서는 앞의 Await 단계처럼 엔진이 재개 함수를 만들어 등록하므로, continuation을 직접 넘기지 않아도 된다.
실무에서 갈리는 지점
- 서로 의존하지 않는 요청을
await두 번으로 이어 쓰면, 두 번째 요청이 첫 번째의 continuation 안에서 시작되어 순차 실행된다. 먼저 둘 다 시작하고Promise.all로 기다린다 - continuation이 계속 새 continuation을 만들면 마이크로태스크 큐가 비지 않아 렌더링이 끼어들지 못한다(마이크로태스크)
명세: ECMAScript — Await ( arg )
출처: MDN — await: Control flow effects of await · Node.js — JavaScript Asynchronous Programming and Callbacks · Wikipedia — Continuation