node.js는 모든 것을 논블로킹 I/O로 처리함으로써 고도로 동시적인 서버를 쉽게 만들었지만, 그 대가는 구조로 나타났다. 데이터베이스 조회나 API 호출이 하나 추가될 때마다 콜백 단계가 하나씩 늘었고, 수만 개의 장기 연결을 유지하도록 만들어진 koush 자신의 프로젝트들은 흔히 일곱, 여덟 단계의 들여쓰기까지 깊어졌다. 단순한 요청과 쿼리만으로도 이미 두 개의 중첩 콜백이 필요했고, 오류 처리는 각 단계를 자체 try/catch로 감쌌으며, 페이지 단위 API 호출처럼 순차적으로 처리해야 하는 작업은 재귀적 연속 함수로 변했다.

2011년에 이용 가능했던 해결책들은 문제를 없애기보다 옮기는 쪽이었다. yield 연산자는 이터레이터를 일시 중단하고 재개했지만 여전히 투박했고, TameJS 같은 소스 대 소스 변환기는 깔끔해 보이는 코드를 어디서나 실행 가능한 평범한 JavaScript로 컴파일했지만, 그 대가로 개발자가 작성한 코드가 아니라 생성된 코드를 디버깅해야 했다. Koush의 기준점은 C# 5의 async/await였는데, 이는 비동기 코드를 동기적으로 읽을 수 있게 해주는 명시적인 언어 기능이었다.

그의 선택은 그 기능을 런타임에 직접 구축하는 것이었다. V8에서 구현된 코루틴, async 함수 표시자, 그리고 콜백이 완료되면 함수를 종료하고 재개하는 await를 갖춘 node.js 포크였다. 파서는 여전히 콜백 버전과 거의 같은 AST를 생성했기 때문에 하나의 try/catch가 모든 await를 포괄했고, while 루프가 페이지 단위 API 요청을 순차 처리할 수 있었으며, ndb와 node-inspector는 여전히 원본 코드를 디버깅할 수 있었다. 그 대가는 커스텀 node 빌드였는데, 자체 서버를 통제하는 팀이라면 현실적으로 치를 수 있는 비용이었다.

V8 코루틴을 통해 await에서 함수를 일시 중단하여 비동기 코드를 동기 코드처럼 읽을 수 있게 함

중첩 콜백은 I/O 단계마다 배로 늘어나고 오류 처리를 별도 스코프로 밀어낸다. await는 전체 흐름을 하나의 try/catch로 감싸므로 예외가 동기 코드처럼 콜백 경계를 넘나든다.

소스 대 소스 변환이 아니라 런타임에 실제 코루틴이 있으므로 개발자가 작성한 코드가 곧 실행되는 코드이며, ndb와 node-inspector는 생성된 출력이 아니라 원본을 디버깅한다.

순차적으로 보이는 제어 흐름 덕분에 평범한 루프로 순차 비동기 작업을 표현할 수 있다. 페이지 단위 GDATA 예제는 재귀적 청크 함수를 while 루프로 대체한다.

서버 팀이 배포를 통제하기 때문에 node.js와 V8을 포크하는 것은 현실적인 비용이었고, 이 설계는 제안서가 아니라 테스트를 갖춘 동작하는 브랜치로 배포되었다.

우회책이 복잡성을 생성된 코드나 더 깊은 중첩으로 밀어낼 때는 언어 수준에서 고쳐라. I/O에서 함수 전체를 일시 중단시켜 읽기 쉬운 코드가 곧 실행되고 디버깅되는 코드가 되게 하라

이 프로젝트는 병합된 기능이 아니라 포크로 남았다. 25번의 수정 끝에 2013년 4월에 마지막으로 편집된 위키는 커스텀 node 빌드를 배포 옵션으로 규정했고, TODO에는 await를 포함한 브랜치 최적화가 나열되어 있었다. 2011년 9월 29일 Hacker News에 게시되어 222점과 101개의 댓글을 받았으며, 독자들은 이 접근 방식을 매우 우아하다고 평했고 올바른 해결책은 코루틴을 도입하는 것이라는 데 동의했다. 기록 어디에도 이 패치가 node.js에 들어갔다는 내용은 없다. 그 가시적인 효과는 JavaScript가 언어 수준에서 async/await를 얻을 수 있는 방법을 동작하는 코드로 보여준 것이었다.

근거 확인하기

출처

  1. "async" support in node.js (koush/node wiki) GitHub — koush/node wiki · github.com
  2. Fixing the callback spaghetti in node.js | Hacker News Hacker News · news.ycombinator.com ·