FEInterview Prep

javascript · high priority

JavaScript 비동기의 진화사

Callback → Promise → async/await → AsyncContext — 어떤 문제를 어떻게 풀어왔는가

intermediate 난이도3시간토스카카오네이버당근배민
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

해결하려 했던 문제

초창기 브라우저 환경에서 비동기는 `onload`, `setTimeout`, `XHR onreadystatechange` 같은 개별 콜백 API 뿐이었다. 통일된 추상화가 없어 라이브러리마다 비동기 규약이 달랐고, 에러 처리와 합성(compose)이 지옥이었다.

역사적 맥락

2009 년 Node.js 가 error-first 콜백 컨벤션 `(err, result)` 로 서버 비동기를 정리했고, 2011 년 jQuery 1.5 가 `$.Deferred` 로 promise-like 객체를 대중화했다. 2012 년 Promises/A+ 커뮤니티 스펙이 라이브러리 간 thenable 상호 운용을 표준화했고, ES6(2015) 가 `Promise` 를 언어에 편입했다. ES2017 의 `async/await` 는 Promise 체인을 동기식으로 읽히게 했고, 이후 `allSettled`(2020) · `any`(2021) · top-level await(2022) 가 차례로 추가됐다. 최신 흐름은 TC39 의 **AsyncContext** 제안으로, await 를 넘나드는 컨텍스트(trace ID, transaction, i18n locale) 추적 문제를 언어 차원에서 푼다.

이전에는 어떻게 했나

jQuery Deferred 는 Promises/A+ 비호환이었고(에러 forwarding 부족, 다중 resolve 허용), Q/Bluebird/When.js 는 표준 이전 시절 대안이었으나 ES6 Promise 도입 이후 점점 밀려났다. RxJS 는 "이벤트 스트림" 추상화로 별도 생태계를 유지한다 — Promise 가 "단발 값" 이라면 Observable 은 "다발 값".

쉬운 설명

복잡한 개념을 실생활 비유로 설명합니다.

우편 제도의 진화

초창기 콜백은 "심부름꾼에게 "다녀와서 나한테 다시 전해줘" 라고 부탁" 하는 것 — 심부름꾼이 안 올 수도, 두 번 올 수도, 엉뚱한 물건을 가져올 수도 있다. Promise 는 "받을 물건의 영수증" 을 먼저 주는 택배 시스템 — 한 번만 발행되고, 발행 후 취소는 없다. async/await 는 "택배가 올 때까지 잠깐 낮잠 자는 대신 눈을 감았다 뜨면 이미 도착해 있는" 마법이고, AsyncContext 는 "택배 상자마다 누가 보낸 건지 적힌 송장" 을 끝까지 유지하는 개선판이다.

핵심 개념

비동기 API 연대표
연도표준/런타임추가된 것푼 문제
1995NetscapesetTimeout · 이벤트 핸들러브라우저가 "지연 실행" 을 표현할 방법이 생김
2009Node.jserror-first callback (err, res)서버 I/O 에러 전파의 일관된 컨벤션
2011jQuery 1.5$.Deferred / $.Promisecallback hell → 체이닝 가능한 객체
2012Promises/A+커뮤니티 표준라이브러리 간 thenable 상호운용
2015ES6Promise 언어 내장표준 Promise, .then/.catch
2017ES2017async / awaitthen 체인 verbosity · try/catch 복구
2018ES2018async iterator · for await...of비동기 스트림 순회
2019ES2019Promise.prototype.finally체인 끝 cleanup 표준
2020ES2020Promise.allSettled · ?? · ?.부분 실패 허용 집약
2021ES2021Promise.any · AggregateError다중 소스 fallback
2022ES2022top-level await (ESM)모듈 초기화에서 비동기 표현
2024+TC39 Stage 3AsyncContext (제안)await 경계를 넘는 컨텍스트 보존

"진화" 의 방향성

비동기 API 의 진화는 언제나 **"코드가 동기 코드처럼 읽혀야 한다"** 는 방향으로 움직여 왔다. 콜백 → Promise(체이닝) → async/await(선형) → AsyncContext(암묵적 컨텍스트) 모두 "사람이 비동기임을 덜 의식하게" 만드는 시도다.

실무 적용

어떤 상황에서 사용하는가

팀이 레거시 콜백 기반 코드를 Promise 로, 다시 async/await 로 점진 마이그레이션하는 중. Node 서버에서 transaction ID 를 로그에 심고 싶은데 await 를 넘으면서 ID 가 섞이는 문제를 발견했다.

어떻게 적용하는가

첫째, 콜백 API 는 `util.promisify` 로 일괄 Promise 화 — 수동 래핑은 에러 원본을 잃기 쉬우니 지양. 둘째, Promise 는 가능한 경우 async/await 로 이전하되, fan-out 병렬은 `Promise.all` 을 유지. 셋째, transaction 컨텍스트는 Node `AsyncLocalStorage` 로 열고 모든 로거가 `als.getStore()` 를 조회하도록 통일. 브라우저에서도 같은 니즈가 생기면 TC39 AsyncContext 폴리필로 임시 대응하다가 표준화되면 정식 API 로 전환.

흔한 실수와 안티패턴

  • `util.promisify` 쓰면서 `this` 바인딩을 잊음 — 메서드는 `.bind(obj)` 필요
  • async 함수를 `setTimeout` 같은 콜백 API 에 직접 넘기고 에러를 잡지 못함
  • 전역 변수로 request context 를 구현 → async 동시성 환경에서 교차 오염
  • `Promise.resolve().then(fn)` 으로 "다음 틱" 을 구현했는데 microtask 라 실제로는 같은 턴에 실행됨
  • AsyncLocalStorage 를 worker_threads 에 넘길 수 있을 거라 착각 — 스레드 경계에서는 전파 안 됨

흔한 오해

오해

"async/await 은 Promise 의 완전한 대체재다."

교정

병렬 실행 · 분기 합류 · 스트림 합성에서는 여전히 Promise combinator 가 표현력이 높다. async/await 는 **순차 흐름에 강한** 표현.

왜 중요

async 함수 본문이 기본적으로 순차 실행이라 `await` 를 루프에 넣으면 직렬화된다. 의도적 병렬은 `Promise.all(map(...))` 이 올바른 방식.

오해

"top-level await 는 모든 모듈에서 쓸 수 있다."

교정

ECMAScript 모듈(ESM) 에서만 동작한다. CommonJS 는 동기 `require` 세만틱상 지원 불가.

왜 중요

CJS 의 `require` 는 모듈을 동기로 반환해야 하는데 top-level await 는 모듈 평가를 중단한다 — 근본적으로 호환되지 않는다.

오해

"AsyncLocalStorage 는 성능 페널티가 없다."

교정

`async_hooks` 는 모든 비동기 자원 생성에 훅을 끼워 측정 가능한 오버헤드가 있다. V8 팀이 지속 최적화 중이지만, hot path 에서는 프로파일링해서 사용.

왜 중요

Node 공식 문서도 "가장 자주 사용되는 건 성능 비용이 있다" 고 명시. 일부 고성능 프로덕션은 context 전파를 경량 수동 구현으로 대체하기도 한다.

면접 질문

중급토스카카오네이버

답변 방향 힌트

각 단계에서 "무엇이 불가능/어려웠는지" 와 "어떻게 해결되는지" 를 매칭하세요.

반드시 언급할 키워드

  • Callback 문제: 피라미드 · 제어권 반전 · 호출 횟수 불보장
  • Promise 해법: 체이닝 · 1회 settle · thenable 상호운용
  • 하지만 Promise 도 분기/루프에서 verbose — async/await 가 제어 흐름 복원
  • async/await 의 한계: await 경계에서 컨텍스트 손실 → AsyncContext
  • 각 단계는 이전 단계를 완전히 대체가 아니라 "선형 코드 표현력" 을 확장

예상 꼬리 질문

  • 왜 callback 에서 Promise 로 넘어갈 때 `then` 메서드 이름이 선택됐을까요?
  • "Promise is eager, Observable is lazy" 라는 표현의 의미는?

자기 점검

콜백 방식의 3가지 근본 문제를 Promise 가 각각 어떻게 해결했는지 답해보세요.

기대 키워드

피라미드제어권 반전호출 횟수체이닝settle 1회error 전파

자주 하는 오해

Promise 가 "읽기 쉬워졌다" 만 기억하는 경우가 많습니다. 본질은 "호출 횟수 보장" 과 "에러 자동 전파" 입니다.

async/await 이 Promise 와 구별되는 엔진 구현 차이는?

기대 키워드

state machine재개 지점로컬 변수 보존스택 트레이스try/catch

자주 하는 오해

"단순 문법 설탕" 이라는 오해 — 실제로는 컴파일 타임에 state machine 으로 변환돼 디버거와 스택 트레이스 기능이 개선됩니다.

AsyncLocalStorage / AsyncContext 가 필요한 구체적 시나리오를 하나 설명해보세요.

기대 키워드

request IDawait 경계trace contextloggertransaction

자주 하는 오해

"그냥 전역 변수로 되지 않나" 라고 생각하지만, 이벤트 루프가 여러 요청을 병행 처리하는 동안 전역 변수는 시점마다 다른 요청의 값을 가리킵니다.

학습 자료