자바스크립트의 이벤트 루프

김용현·2023년 5월 9일
0
post-thumbnail

자바스크립트는 싱글 스레드 기반에 언어입니다. 이는 한번에 하나의 작업만을 처리할 수 있다는 것을 의미합니다. 하지만 우리가 자바스크립트를 사용해보면 동시에 여러 작업을 처리하는 것으로 보입니다. 자바스크립트는 동시성을 지원하기 위해서 이벤트 루프를 사용합니다. 이벤트 루프는 자바스크립트를 실행시켜주는 브라우저에서 지원합니다.

먼저 브라우저에서 실행되는 자바스크립트의 구조는 아래 이미지처럼 구성되어있습니다.

  • 콜 스택(call stack): 자바스크립트에서 실행된 함수들을 처리하는 스택입니다. 실행 컨텍스트 스택이라고도 합니다.
  • 힙(heep) : 자바스크립트의 객제가 저장되는 메모리 공간입니다. 객체는 원시 값과 달리 크기가 정해져 있지 않고 런타임에 동적으로 할당됩니다. 따라서 객체가 저장되는 힙은 구조화 되어있지 않다는 특징이 있습니다.
  • 이벤트 루프(event loop): 이벤트 루프는 콜 스택에 현재 실행 중인 작업이 있는지, 콜백 큐에 대기 중인 작업이 있는지 반복적으로 확인합니다. 만약 콜 스택이 비어있고 콜백 큐에 대기 중인 작업이 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 작업을 콜 스택으로 이동시킵니다. 그리고 콜 스택으로 이동된 작업은 실행되어서 처리됩니다.
  • 콜백 큐(callback queue): 비동기 함수의 콜백 함수나 이벤트 핸들러가 대기하는 장소입니다. 콜백 큐를 부르는 다양한 이름이 있지만 이 글에서는 태스크 큐로 부르겠습니다.
  • Web API: ECMAScript 사양에 정의된 함수가 아니라 브라우저에서 제공하는 API이며, DOM을 조작하는 API와 타이머 함수, HTTP 요청과 같은 비동기 처리를 포함한 기능을 제공합니다.
function foo() {
	console.log('foo');
}

function bar() {
	console.log('bar');
}

setTimeout(foo, 0);
bar();
  1. 전역 코드가 평가되고 전역 실행 컨텍스트가 생성되고 콜 스택에 푸시됩니다.
  2. 전역 코드가 실행되고 setTimeout 함수가 실행된다. 이때 setTimeout 함수가 콜 스택에 푸시되고 실행됩니다. 이때 Web API인 타이머 함수도 함수이므로 콜 스택에 푸시되어 실행됩니다. 그리고 콜백 함수 foo를 예약하고 종료되어 콜 스택에서 팝합니다. 브라우저는 타이머가 종료되면 예약된 콜백 함수를 태스크 큐에 푸시합니다.
  3. bar 함수가 실행되어 콜 스택에 푸시되고 종료되면 콜 스택에서 팝됩니다. 여기까지 실행되면 콜 스택은 비어있게 됩니다.
  4. 이벤트 루프에 의해서 콜 스택이 비어있는 것이 확인됩니다. 그리고 브라우저에서 타이머가 종료되어 예약된 foo 함수를 태스트 큐에 푸시했다면, 이벤트 루프는 foo 함수를 콜 스택으로 이동시킵니다. 콜 스택에 푸시된 foo 함수는 실행되고 종료되어 콜 스택에서 팝됩니다.

자바스크립에서 비동기 함수가 실행되면 태스크 큐에서 대기하다가 이벤트 루프에 의해서 콜 스택에 푸시되어 실행됩니다. 여기서 자바스크립트는 싱글 스레드 기반으로 동작하고 브라우저는 멀티 스레드로 동작하다는 것에 주의해야합니다. 만약 이러한 이벤트 루프가 없다면 자바스크립트가 실행 중인 코드가 완료되기 전까지는 사용자는 코드의 실행을 그대로 기다리고 있어야합니다.

마이크로 태스트 큐(Micro Task Queue)

자바스크립트에서는 비동기 함수의 실행 순서에 따라서 동기적으로 코드를 실행하기 위한 패턴으로 콜백 함수를 사용했습니다. 하지만 콜백 함수를 사용하는 패턴은 콜백 헬로 인해서 가독성과 에러 처리를 어렵게 만들었습니다.

ES6에서는 비동기 처리의 다른 방안으로 Promise가 도입되었습니다. Promise는 전통적인 콜백 헬을 해결하고 실행 순서를 명확하게 파악할 수 있는 장점이 있습니다. 또한 ES8에서는 async/await도 추가되었습니다.

setTimeout(() => console.log(1), 0);

Promise.resolve()
	.then(() => console.log(2))
	.then(() => console.log(3));

Promise의 후속 처리 메소드인 .then,.catch,.finally 또한 비동기로 작동합니다. 따라서 콘솔에 출력되는 순서는 1 → 2 → 3 으로 생각할 수 있지만 실제 출력은 2 → 3 → 1 순서로 출력됩니다.

그 이유는 Promise 의 후속처리 메소드는 태스크 큐에서 처리되지 않고 마이크로 태스크 큐에서 처리됩니다. 마이크로 태스트 큐는 태스크 큐보다 우선 순위가 높습니다. 그래서 태스트 큐에서 처리되는 setTimeout 보다 먼저 실행됩니다. 위 코드에서 이벤트 루프는 다음과 같이 동작합니다.

  1. 이벤트 루프는 콜 스택이 비어있는 것을 확인하고, 마이크로 태스크 큐에 대기 중인 첫 번째 .then의 콜백 함수를 콜 스택에 푸시합니다. 그리고 첫 번째 .then의 콜백 함수가 실행이 완료되면 프로미스가 반환 두 번째 .then이 마이크로 태스크 큐에 푸시됩니다.
  2. 다시 콜 스택이 비어있는 것을 확인하고, 마이크로 태스크 큐에 대기 중인 두번째 .then의 콜백 함수를 콜 스택에 푸시합니다. 두번 째 .then까지 실행되면 마이크로 태스크 큐는 비어있게 됩니다.
  3. 이벤트 루프는 마이크로 태스크 큐에 대기 중인 작업이 없는 것을 확인하고 태스크 큐에 있는 setTimeout의 콜백 함수를 콜 스택으로 푸시합니다.

참고 링크

profile
프론트개발자 김용현

0개의 댓글