Asynchronous JavaScript: call stack, macro/microtask queue

dalbodre·2022년 2월 24일
0

study

목록 보기
3/6
post-thumbnail

자바스크립트는 싱글 스레드 언어로 잘 알려져 있다. 자바스크립트 엔진은 콜스택에 들어온 함수를 바로 실행한다. 오랜 시간이 걸리는 동기함수가 콜스택에 들어오면 그동안 다른 함수는 전혀 실행될 수 없는데, 결국 '응답 없는 페이지'라는 경고가 왕왕 발생하기도 한다.

외부 호스팅 환경을 통한 JavaScript의 비동기 처리

그렇다면 어떻게 자바스크립트에서는 비동기 처리를 하고 있는 것일까. 다들 한번쯤은 setTimeout과 같은 기능을 써보았을텐데, 싱글 스레드만으로 비동기처리를 한다는 것이 도저히 상상이 가지 않는다. 내 머리가 잘못 된건가 아니면 자바스크립트가 사실은 싱글스레드가 아닌 것인가..?!

다행히 둘다 아니다. 자바스크립트의 비동기처리는 브라우저와 같은 외부 호스팅 환경에 의존한다. 외부 호스팅 환경의 Web API, Callback Queue, Event Loop가 비동기 처리를 위해 이용된다.

처음 자바스크립트 콜스택에 setTimeout 함수가 들어오면 자바스크립트 엔진은 이를 수행한다. 자바스크립트 엔진은 Web API를 통해 호스팅환경에게 타이머를 실행하도록 넘기는 것으로 함수 실행을 마치고, 완료된 setTimeout 함수는 콜스택에서 지워진다. 브라우저는 이후 타이머가 완료되면 (setTimeout과 함께 전달되었던) 콜백을 콜백큐에 넘긴다. 이벤트루프는 반복해서 콜스택이 비어있는지를 확인하는데, 비어있는 경우에 콜백큐의 첫번째 이벤트를 콜스택에 밀어넣는다.

setTimeout 코드 예시

console.log('Hi')
setTimeout(function A() {
  console.log('Hello Again')
},1000)
console.log('Bye')

setTimeout이 포함된 예시코드가 어떻게 동작하는지 단계별로 알아보자.

  1. 코드가 실행되면, 가장 첫번째 줄인 console.log(‘Hi’)가 콜스택에 추가된다. 이를 JS엔진이 실행하여 콘솔에 hi가 출력되면 콜스택에서 console.log가 삭제된다.
  2. 다음줄인 setTimeout(~)가 콜스택에 추가되고 실행된다. 자바스크립트 엔진은 WebAPI를 통해 브라우저에 타이머를 생성한다. 브라우저에서 카운트다운이 이어서 처리된다. 자바스크립트 엔진은 WebAPI 호출을 완료하였기 때문에 카운트다운이 되고있는지와는 상관없이 콜스택에서 setTimeout을 제거한다.
  3. Bye를 출력하는 함수가 콜스택에 추가되고, Hi와 동일하게 콘솔에 출력된 후 콜스택에서 제거된다.
  4. 시간이 지나 타이머가 완료되면 브라우저는 setTimeout의 콜백함수였던 A를 콜백큐에 밀어넣는다. 이벤트루프는 현재 Bye까지 출력되어 콜스택이 비어있기 때문에 A함수를 콜백큐에서 콜스택으로 밀어넣는다.
  5. A함수가 실행되면 콜스택에 console.log("Hello Again") 함수 블록이 추가된다. 3번이 끝나면서 비어있던 콜스택에 A 함수 블럭이 쌓이고 그 위에 Hello Again을 출력하는 함수블럭이 또 있는 상태가 된다. 자바스크립트 엔진은 콘솔창에 Hello Again을 출력하는 것으로 console.log 함수를 완료하고, 이에 따라 console.log 함수 블럭과 하단의 A 함수 블럭이 차례대로 콜스택에서 제거된다.

WebAPI 타이머 사용 시 주의점

주의해야할 점은 실제 콜백함수가 언제 실행될지 아무도 장담할 수 없다는 것이다. 위에서는 1000ms 이후에 콜백을 실행해달라고 코드를 작성했지만, 브라우저는 단순히 1000ms 타이머가 완료되면 콜백큐에 해당 콜백을 넣어준다. 콜백 큐에는 이미 추가된 이벤트들이 존재할 수도 있고, 언제 콜스택이 완전히 비어 이벤트루프가 콜백큐의 함수를 실행할 수 있을지는 아무도 모른다.
setTimeout에 대한 MDN 글에서 콜백 딜레이, 비활성 탭의 타임아웃, 추적 스크립트 스로틀링, 페이지 로드 등 타이머 실행 지연에 대한 자세한 설명을 확인할 수 있다.

마이크로 태스크 큐 : 모든 콜백이 시간 순으로 실행되는 것은 아니다

Promise는 자바스크립트에서 비동기 처리를 쉽게 처리하기위한 객체이다. MDN에서는 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)이라고 간단하게 설명하고 있다. Promise는 resolve되면 다음 then의 함수를 이행한다.

그렇다면, 아래 코드를 실행하면 어떻게 될까? setTimeout과 Promise가 하나의 코드에 존재한다. setTimeout은 0초의 시간간격을 두고 있고 Promise또한 바로 resolve했기 때문에, setTimeout과 Promise 모두 즉시 콜백 함수를 콜백 큐에 넣게 될것이다.

setTimeout(()=>{console.log("A")},0)
Promise.resolve()
  .then(()=>{console.log('B')})
  .then(()=>{console.log('C')})

정답은 B-C-A 순이다. Promise는 마이크로 태스크로 취급되기 때문이다.

콜백 큐에 들어가는 task들에는 우선순위가 존재한다. 비동기처리를 하는 데에 모든 task들이 FIFO 수준으로 처리되기에는 부족했을 것이다. 따라서 일반 (macro) task보다 먼저 실행되어야 하는 microTask를 구분하기 시작했다. 위에서 설명했던 콜백 큐가 macroTask들만 가진 큐와 microTask만으로 구성된 큐로 분리된 것이다. 이벤트 루프는 자바스크립트의 콜스택이 비었는지 확인하고, 만약 비었다면 (일반)태스크 큐 대신 마이크로 태스크 큐의 함수들을 먼저 실행한다.

Promise는 일반적으로 마이크로 태스크로 취급된다. 물론 특정 브라우저에서는 Promise가 일반 태스크로 취급되는 경우도 있다고 한다. 우선 크롬과 사파리가 아니면 상관 없는게 아닐까 ㅎㅎ.. 각 태스크큐의 종류와 해당하는 태스크들은 다음과 같다.

  • microtask queue : Promise, queueMicrotask, MutationObserver
  • (macro)task queue : setTimeout, setInterval
  • RAF queue : requestAnimationFrame

과거 ThreeJS를 통해 3D 애니메이션을 만들 때에 setInterval 대신 requestAnimationFrame을 사용했었는데, 오랜만에 보니까 반갑기도 하고,,, 해당 내용은 ThreeJS 포스팅을 하면서 내용을 추가하거나 함께 정리해보아야겠다!

profile
휘뚜루마뚜루

0개의 댓글