JS의 비동기 딥다이브하기! (callback → Promise → async/await(+ generator))
스스로하는 Q&A
Q1. JS에서 Promise가 필요한 이유가 무엇인가요?
JS는 싱글 스레드로 한번에 한가지 일만 할 수 있습니다. 하지만 API요청 등 비동기 처리를 하는 동안 프로그램이 멈추게 된다면 UX적 측면으로 매우 안좋게 됩니다. 이러한 경우를 방지하기 위해 기존의 JS는 callback 패턴을 이용해서 비동기 처리를 JS엔진 외부인 브라우저나 Node.js같은 멀티 스레드 환경으로 위임했습니다. 덕분에 JS는 비동기 처리를 끝날때까지 기다리는 게 아닌 콜백함수를 멀티 스레드 환경으로 넘겨주고 다음 코드를 실행합니다.
이러한 콜백 패턴에서는 치명적인 2가지 문제점이 있었는데
첫번째 문제점은 비동기로 처리한 값을 콜백 함수 내에서만 사용할 수 있다는 것입니다. 그렇기 때문에 비동기 처리의 결과값을 사용하여 어떠한 로직을 작성 시 콜백 함수의 내부에서만 사용을 할 수 있어 depth가 깊어지고 가독성이 않좋아지는 콜백 헬이 발생합니다.
이는 가독성이 떨어지고 유지보수에 어려움을 겪게 합니다.
두번째 문제점은 에러를 핸들링하기 어렵다는 것입니다. JS에서 에러를 처리하는 방식은 대표적으로 ES3에 나온 try…catch문을 사용합니다.
try…catch는 에러가 발생한 곳부터 콜 스택의 아래방향으로 전파가 됩니다. 따라서 자신을 호출 한 곳에서 catch문으로 에러 핸들링을 할 수 있게 해줍니다.
하지만 비동기 함수에 대한 콜백 함수의 내부 동작은 JS 외부로 위임된 후 테스크 큐에 담겨있다가 콜 스택이 비어있을 때 추가되기 때문에 catch문으로 에러 핸들링을 할 수가 없습니다.
이러한 문제를 해결하기 위해서 Promise가 도입되었는데요
Promise는 비동기 로직에 성공했을 시와 실패했을 시 실행할 두개의 콜백함수 resolve와 reject를 인수로 받습니다. 그리고 성공시 resolve를, 실패시 reject를 실행하도록 구현합니다.
먼저 첫번째 문제는 Promise의 후속 메서드를 통해 해결하였습니다.비동기 함수가 성공하여 resolve가 호출 되었을 경우 처리결과를 then 메서드의 인수로 전달하고 실패했을 경우 catch 메서드의 인수로 전달합니다.
이를 통해 비동기처리를 콜백함수 내부에서만 핸들링할 수 있던 어려움을 외부에서 후속 메서드를 통해 핸들링할 수 있습니다. 하지만 메서드 체이닝 내부에서만 핸들링해야 하기때문에 완벽한 해결은 아니었고 이를 위해 제너레이터를 이용한 async/await문법이 나왔습니다.
두번째 문제도 후속 메서드인 catch를 통해 해결할 수 있습니다. 비동기 처리가 실패하거나, 후속 메서드 then에서 에러가 날경우 에러객체를 catch 메서드로 전달하여 처리 할 수 있습니다. 즉, 제어권이 코드 외부가 아닌 내부로 돌아오는 것입니다. 이렇게 후속메서드 catch문을 통해 try … catch문과 동일한 효과를 낼 수 있습니다.
Q2. async/await의 동작 방식은 어떻게 되나요?
async/await은 제너레이터와 프로미스를 결합해서 만든 문법적 설탕이라고도 할 수 있습니다.
제너레이터 함수의 가장 큰 특징은 함수 호출자가 함수 내부의 실행을 제어하고 데이터를 주고받을 수 있다는 것입니다.
(함수호출자.next() ⇒ 함수 시작 ⇒ yield value ⇒ 함수호출자.next().value에 value 바인딩 ⇒ 함수 호출자가 포함된 소스코드 실행 ⇒ … ⇒ 함수호출자.next(value2) ⇒ yield에 value2 바인딩)
이러한 특성을 이용해서 Promise에서 후속 메서드 안에서만 처리해야했던 문제를 제너레이터를 이용해서 비동기 처리에 대한 결과를 Promise 바깥의 변수에 담아 사용할 수 있게 되었습니다.
다만 이렇게 제너레이터를 사용하게 되면 가독성이 안좋아지는 문제가 생겼는데 이를 해결하기 위해서 Promise객체를 반환하는 제너레이터를 추상화하여 async/await 문법을 만들게 되었습니다.
이때 생기는 궁금증은 Promise의 비동기처리 완료 시점은 밖에서 모른다는 것입니다.
JS의 no-blocking 언어로 멈출 수 없는데 어떻게 Promise의 비동기 처리가 완료될때까지 기다리는 것일까요?
barbel을 통해 트렌스 파일러된 코드를 보면 yield에 담긴 Promise가 완료될 때 까지 재귀적으로 루프를 돌며 머물러 있는 것을 확인 할 수 있었습니다. 따라서 JS의 no-blocking 특성이 유지됩니다.
댓글남기기