본문 바로가기

Javascript

[Javascript] 구동 환경과 동작 원리 이해하기 with 이벤트루프, 호출스택, 콜백 큐

자바스크립트는 한번에 한가지 일만 실행하는 싱글 쓰레드 언어이다.

한번에 한가지 일만 실행하는 것은 오래걸리는 작업을 실행할 때 비효율적일 수 있다.

예를 들어, 빨래를 하는동안 다른 집안일을 하지 못하고 세탁기 돌아가는 것만 바라보고 있어야 한다고 생각하면 이건 굉장한 시간 낭비이다.

개발에서도 마찬가지로 시간이 많이 소요되는 일은 병렬 처리가 필요한데, 이때 싱글 스레드 언어인 자바스크립트는 이러한 처리를 비동기 콜백을 사용해 해결한다.

자바스크립트의 비동기 콜백의 이해를 위해서는 브라우저의 동작 원리를 이해해야 한다.

비동기 콜백의 이해를 위해서는 JS Engine과 Web API, Event Loop에 대한 이해가 필요하다.

Javascript Engine

JS Engine은 자바스크립트 코드를 이해하고 실행을 도와준다.

대표적으로는 V8엔진이 있으며, 각 브라우저 별로 여러 엔진을 사용한다.

JS Engine은 크게 메모리 힙콜스택으로 이루어져 있다.

자바스크립트 엔진

출처 - https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/

메모리 힙(Memory Heap)

데이터 임시 저장소로 함수나 변수 함수 실행 할 때 사용하는 값들을 저장한다.

콜 스택(Call Stack)

코드가 실행되면 코드 내부의 실행 순서를 차곡차곡 쌓은 후 하나씩 순차적으로 실행하도록 도와주는 곳이다.

자바스크립트가 이러한 JS Engine으로만 돌아가진 않는다. 자바스크립트를 실행하게 하는 환경이 존재하는데, 이 실행환경을 자바스크립트의 런타임 환경이라고 한다.

console.log(1+1)
setTimeout(() => {
	console.log(2+2)
}, 1000)
console.log(3+3)

위 코드의 실행 순서를 보면 다음과 같다.

2 → 4 → 6 의 출력 순서가 아닌, 2 → 6 → 4 의 출력 순서를 가지는 이유는 setTimeout 함수의 실행을 기다리지 않고 console.log(3+3) 가 실행 되었기 때문인데, 자바스크립트의 런타임 환경을 알아보며 왜 이러한 실행 순서가 되는지 알아보자.

자바스크립트의 런타임 환경 Web API, Callback Queue, Event Loop

자바스크립트 런타임 환경

출처 - https://wikidocs.net/251900

WebAPIs

타이머, 네트워크 요청, 이벤트 등을 처리하는 브라우저의 API을 말한다.

Callback Queue

WebAPIs에서 처리된 비동기 함수들이 쌓이는 곳이다.

이벤트 루프 (Event Loop)

이벤트 루프는 자바스크립트 콜백 큐와 콜 스택을 모니터링 하며 동시에 여러가지 일이 실행될 수 있도록 도와주는 역할을 한다.

예시코드를 통해서 각 역할에 따른 실행 순서를 알아보자.

console.log(1+1)
setTimeout(() => {
	console.log(2+2)
}, 1000)
console.log(3+3)
  1. console.log(1+1) 출력 (호출 스택에 추가 후 즉시 실행)
  2. setTimeout(() => { console.log(2+2) }, 1000) (호출 스택에 추가 후 Web API로 넘김)
  3. console.log(3+3) 출력 (호출 스택에 추가 후 즉시 실행)
  4. 1초 후 setTimeout 콜백 함수 콜백 큐에 추가
  5. 호출 스택이 비어 있을 경우 콜백 큐에서 setTimeout 콜백 함수 꺼내 console.log(2+2) 출력

호출 스택과 콜백 큐를 계속 모니터링 하면서 5번 과정을 실행하는 것이 이벤트 루프의 역할이다. 위와 같은 과정을 통해 자바스크립트는 싱글 스레드이지만 비동기적인 처리가 가능한 것이다.

이때, 자바스크립트에서 무거운 연산을 실행시킬 경우 호출 스택에서 연산이 계속 진행되면 비동기 함수의 실행이 막히기 때문에 대용량 데이터의 연산을 자바스크립트에서 실행하는 것은 좋지 않으므로 주의해야 한다.

Macro Task Queue와 Micro Task Queue

콜백 큐에 들어오는 비동기 작업들은 Macro Task 와 Micro Task로 나뉜다.

Macro Task Queue

setTimeout, setInterval, fetch, addEventListener 등

Micro Task Queue

Promise.then, process.nextTick, MutationObserver 등

Micro Task는 최우선적으로 처리된다.

이번에도 예시코드를 통해서 각 역할에 따른 실행 순서를 알아보자.

console.log('Start!');

setTimeout(() => {
    console.log('Timeout!');
}, 0);

Promise.resolve('Promise!').then(res => console.log(res));

console.log('End!');
  1. console.log('Start!') 출력 (호출 스택에 추가 후 즉시 실행)
  2. setTimeout(() => { console.log('Timeout!'); }, 0) (호출 스택에 추가 후 Web API로 넘김)
  3. 타이머가 종료됨에 따라 setTimeout의 콜백함수는 MacroTask Queue에 대기
  4. Promise 코드가 호출 스택에 적재 후 실행되고 콜백함수는 MicroTask Queue에 대기
  5. console.log('End!') 출력 (호출 스택에 추가 후 즉시 실행)
  6. 호출 스택이 비워지면 콜백 큐에 남아 있는 콜백 함수 중 MicroTask Queue 콜백이 우선적으로 처리된다. (만약 여러개일 경우 모두 실행)
  7. MicroTask Queue가 비워지면 MacroTask Queue의 콜백함수를 호출 스택에 적재하여 실행한다.

결론적으로 최종 실행 순서는 Start → End → Promise → Timeout이 되게 된다.

 

참고 자료

- https://wikidocs.net/251949

- https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/