자바스크립트 동작 원리

싱글 스레드 (Single Thread)

자바스크립트는 싱글 스레드[1] 언어이다. 그러나, 자바스크립트의 특징들 중, 이와 상반되는 개념들이 여러 존재한다. 예를 들면, 비동기, 동시성, 논블로킹 입출력 등이 있다.

싱글 스레드인데 어떻게 동시성을 가질 수 있을까?

js-loop1
Fig 1. 자바스크립트의 구성 요소

자바스크립트 런타임은 메모리 힙(Memory Heap)콜 스택(Call Stack)으로 구성되어 있다. 메모리 힙은 메모리 할당을 담당하며, 콜 스택은 코드가 호출되면서 스택으로 쌓이는 곳이다.

자바스크립트는 메인 스레드 하나로 이뤄진 싱글 스레드 언어이다. 하나의 메인 스레드에서 호출되는 함수들이 콜 스택에 쌓인다. 이 함수들은 LIFO(Last In First Out) 방식으로 실행된다. 자바스크립트가 싱글 스레드 기반 언어라는 것은 자바스크립트가 하나의 메인 스레드와 하나의 콜 스택을 갖고있다는 의미이다.

const foo = () => {
  bar();
  console.log('foo');
};
const bar = () => {
  console.log('bar');
};
foo();
console.log('foo and bar');

위 함수들의 실행에 따른 출력 순서는 다음과 같다.

  1. foo 실행
  2. foo 내부의 bar 실행
  3. console.log('bar') 실행 후 콜 스택에서 제거
  4. bar 함수는 실행을 마쳤으므로 콜 스택에서 제거
  5. foo 함수로 돌아와서 console.log('foo') 실행 후 콜 스택에서 제거
  6. foo 함수는 실행을 마쳤으므로 콜 스택에서 제거
  7. console.log('foo and bar') 콜 스택에 추가, 실행 후 제거

자바스크립트의 비동기 지원 여부

자바스크립트 런타임 자체에서 비동기 API를 지원하지 않는다. 동시성을 보장하는 비동기, 논블로킹 작업들은 자바스크립트 엔진을 구동하는 런타임 환경에서 담당한다. 이 런타임 환경은 브라우저가 될 수 있고, Node.js가 될 수 있다.

js-loop2
Fig 2. 자바스크립트 엔진과 런타임 환경의 조합

자바스크립트 런타임 환경이 브라우저인 경우, 자바스크립트 구성 요소와 함께 이벤트 루프(Event Loop), 콜백 큐(Callback Queue) 등이 조합되어 있다. Web API도 확인할 수 있는데, 만약 런타임 환경이 Node.js라면 Web API 대신 Node.js가 지원하는 라이브러리 및 API가 될 수 있다.

브라우저 런타임의 구성 요소들의 수행 역할은 다음과 같다.

이벤트 루프

이벤트 발생 시 호출되는 콜백 함수들을 관리하여 콜백 큐에 전달한다. 콜백 큐에 담겨있는 콜백 함수들을 콜 스택에 넘겨준다.

이벤트 루프가 콜백 큐에서 콜 스택으로 콜백 함수를 넘겨주는 작업은 콜 스택에 쌓여있는 함수가 없을 때만 수행한다.

콜백 큐

Web API 에서 비동기 작업들이 실행된 후 호출되는 콜백 함수들이 기다리는 공간이다. 이벤트 루프가 정해준 순서대로 줄을 서있으며, FIFO(First In First Out) 방식을 따른다.

Web API

브라우저에서 자체 지원하는 API이다. 이는 DOM 이벤트, AJAX(XmlHttpRequest), setTimeout 등 비동기 작업을 수행할 수 있도록 API를 지원한다.

브라우저 런타임에서 비동기 코드가 실행되는 방식

Web API를 사용한 비동기 작업을 수행하는 코드는 아래 순서로 실행된다.

  1. 코드가 콜 스택에 쌓인 후 실행되면 자바스크립트 엔진은 비동기 작업을 Web API에게 위임한다.
  2. Web API는 해당 비동기 작업을 수행하고, 콜백 함수를 이벤트 루프를 통해 콜백 큐에 넘겨준다.
  3. 이벤트 루프는 콜 스택에 쌓여있는 함수가 없을 때, 콜백 큐에서 대기하는 콜백 함수를 콜 스택으로 넘겨준다.
  4. 콜 스택에 쌓인 콜백 함수가 실행된 후, 콜 스택에서 제거된다.

위와 같이 비동기 논블로킹 입출력 작업이 수행된다.

만약, 서버로부터 대용량의 리소스를 응답받아야 하는 HTTP 요청을 동기로 수행한다면 이 요청을 보내는 함수는 콜 스택에 쌓인 채로 머물게 된다. 즉, 자바스크립트 엔진은 이 함수의 실행이 끝날 때 까지 어떠한 작업도 수행할 수 없다. HTTP 요청으로 인해 다른 코드들을 블로킹하고 있는 것이다. 그러나 브라우저 런타임의 자바스크립트 엔진이 이 비동기 작업들을 Web API로 넘겨줌으로써, 해당 작업이 완료될 때 까지 다른 코드들을 실행할 수 있다. 이것이 논블로킹 입출력이다.

console.log('첫번째로 실행');
setTimeout(() => console.log('최소 1초 후에 실행됩니다.'), 1000);
console.log('나는 언제 실행될까요?');

위 코드의 출력 순서는 다음과 같다.

  1. console.log('첫번째로 실행') 이 콜 스택에 쌓인다. 바로 실행되며, 실행 후 콜 스택에서 제거된다.
  2. setTimeout이 콜 스택에 쌓인다. 이는 Web API로, 이 비동기 작업을 Web API로 넘긴다. Web API에서 timer가 생성된다.
  3. console.log('나는 언제 실행될까요?') 가 콜 스택에 쌓이고, 바로 실행된 후 콜 스택에서 제거된다.
  4. timer는 생성된 시점을 기준으로 하여 1초 후에 콜백 큐로 콜백 함수를 전달한다.
  5. 이벤트 루프가 콜 스택이 비어있는 것을 확인했고, 콜백 큐에 쌓인 setTimeout의 콜백 함수를 콜 스택으로 넘기고 실행한다.
  6. console.log('최소 1초 후에 실행됩니다.') 가 실행된 후 콜 스택에서 제거된다.

만약, setTimeouttimer를 0초로 설정하더라도 결과는 동일하다. 중요한 것은 시간이 아니라, setTimeout이 Web API가 지원하는 비동기 함수라는 것이다.

setTimeout의 콜백 함수는 바로 콜 스택에 쌓이지 않고 Web API에서 비동기 처리를 거친 후 콜백 함수가 콜백 큐로 전달된다. 시간이 0초여도 콜백 큐에 담긴 후 이벤트 루프가 콜 스택이 비어있는 것을 확인한 다음에야 콜백 함수가 콜 스택으로 넘겨지기 때문에 같은 결과가 나오는 것이다.

Notes

[1]싱글 스레드란 말 그대로 한 번에 하나의 작업만 수행할 수 있음을 의미한다. 다른 작업이 중간에 끼어들 수 없고, 기존에 수행하던 작업이 끝나야만 그 다음 작업을 수행할 수 있다.

Reference

YUNSU BAE

YUNSU BAE

주니어 웹 개발자 배윤수 입니다!

예술의 영역을 동경하고 있어요. 🧑‍🎨