노트

Promise continuation

Promise Continuation

프론트엔드#js · 연결된 개념 4개

쉽게 말하면

Promise continuation은 진동벨을 받을 때 '울리면 이어서 할 일'을 미리 적어 두는 메모예요. 함수는 그 자리에서 잠깐 빠지고, 벨이 울리면 메모에 적힌 곳부터 다시 이어서 해요.

비유의 한계 메모는 벨이 울린 뒤가 아니라 await를 만난 순간 이미 걸려요. 결과가 나오면 바로 끼어들지 않고 마이크로태스크 차례를 기다리고, 실패하면 그 await 자리에서 예외가 난 것처럼 이어져요.

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 연산은 대략 이렇게 동작한다.

  1. 지금 실행 중인 async 함수의 실행 맥락(execution context)을 붙잡아 둔다
  2. 기다릴 값을 PromiseResolve로 Promise로 바꾼다. Promise가 아닌 값이나 이미 끝난 Promise도 마찬가지다
  3. "이 맥락을 값과 함께 재개하라"는 성공 함수와 "예외로 재개하라"는 실패 함수를 만들어 그 Promise에 then처럼 등록한다
  4. 호출한 쪽으로 제어를 돌려준다. 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

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • use()로 Promise 읽기

    use()는 렌더 중에 Promise나 Context 같은 리소스의 값을 읽는 React 19 API(Application Programming Interface)다. Promise가 아직 pending이면 컴포넌트가 suspend되어 가장 가까운 suspense의 fallback이 보이고, 끝나면 그 값으로 다시 렌더된다. reject되면 가장 가까운 에러 경계(Error Boundary)로 던져진다.

  • 동기·비동기와 블로킹·논블로킹

    동기·비동기는 결과를 언제 어떻게 받느냐, 블로킹·논블로킹은 기다리는 동안 호출한 스레드가 멈추느냐의 문제다.

  • 커넥션 드레이닝과 무중단 재시작

    배포 중 서버를 재시작하는 몇 초 동안 로드밸런서가 그 서버로 요청을 보내면 502가 난다. 먼저 로드밸런서에서 빼고(드레이닝), 진행 중인 요청을 마친 뒤 재시작하고, 준비되면 다시 넣는다.

  • 커링

    커링(Currying)은 인수 여러 개를 받는 함수 f(a, b, c)를 인수 하나씩 받는 함수의 사슬 f(a)(b)(c)로 바꾸는 기법이다. 바꾸기만 하고 원래 함수를 호출하지는 않는다. 이름은 해스컬 커리(Haskell Curry)에서 왔다.

  • 함수 합성

    함수 합성(Function Composition)은 한 함수의 출력을 다음 함수의 입력으로 넘기는 식으로 여러 함수를 하나로 묶는 방법이다. 마지막 함수의 출력이 합성된 함수의 결과가 된다.

보기 옵션