tech.junha.kr

브라우저에서 자바스크립트가 동작하는 원리

1. JavaScript Runtime 구성 요소

브라우저 내의 JavaScript *런타임은 크게 JavaScript 엔진과 웹 API로 구성되어있다. 그리고 여기에 테스크 큐와 이벤트 루프라는 개념 또한 존재한다. 이를 통해 싱글 쓰레드 기반의 프로그래밍 언어라 불리는 JavaScript도 비동기적으로 코드를 실행시킬 수 있게 된다.

*런타임: 프로그래밍 언어로 작성된 코드가 구동되는 환경

2. JavaScript 엔진

JavaScript 코드를 읽고, 해석, 실행을 담당하는 인터프리터이다. 쓰레드 하나로만 동작하기 때문에 스택 영역도 단 하나만 사용하게 된다. 이로 인해 하나의 함수가 실행이 끝나기 전에는 다른 함수가 실행될 수 없는 "Run to Completion" 특성을 가지게 된다.

JavaScript 엔진의 메모리 모델의 두 가지 영역

1) 힙 영역

  • 메모리를 관리하는 영역
  • 동적으로 생성되는 객체들을 할당하는 곳
  • 구조화되지 않은 넓은 메모리 영역을 지칭한다.
  • ex) 변수 할당시 관련 정보 저장

2) 스택 영역

  • 실행할 테스크를 관리하는 영역
  • 함수가 실행될 때마다 해당 스택 프레임이 푸시되고,
  • 함수가 종료될 때마다 해당 스택 프레임이 팝 되는 영역을 지칭한다.
  • ex) 함수 호출 시 스택에 쌓임

스택 영역에 푸시되는 함수의 스택 프레임은 그 함수의 **실행 맥락(Execution Context)**을 의미한다. 실행 맥락이란, 해당 함수를 실행하기 위해 필요한 각종 정보들(ex. 지역 변수)의 집합이다. JavaScript 엔진은 JavaScript 코드의 실행을 시작함과 동시에 전역 실행 맥락(Global Execution Context)을 스택 영역에 푸시하고, 이후 특정 함수의 호출 문을 만나는 순간 그 함수의 실행 맥락을 스택 영역에 푸시하고 그 함수가 종료될 때 그 실행 맥락을 팝 한다.

JavaScript 코드의 실행이 전부 다 끝나기 전까지는 전역 실행 맥락이 스택 영역에서 팝 되지 않고 남아 있다. 이를 기억하고 있어야 뒤에서 설명할 "스택이 비는 순간"이라는 말의 의미를 조금 더 명확히 이해할 수 있다. 즉, 스택이 비는 순간이라는 것은 결국 더 이상 실행할 JavaScript 코드가 없어서 전역 실행 맥락까지 스택 영역에서 팝이 된 경우를 말한다.

3. 비동기 방식 Event loop로 Javascript의 동시성을 지원한다.

**이벤트 루프(Event Loop)**를 이용해서 callback queue(브라우저에서 발생한 이벤트) 를 call stack에 보낸다. call stack을 지켜보고있다가, 스택이 비는 순간 callback queue에서 callback function을 꺼내서 실행시켜주는 일만 한다. 이벤트 루프는 계속 stack의 empty 여부만을 신경쓰기 때문에, 비동기 방식으로 실행중인 태스크가 call stack에 있는지, 없는지를 확인한다. 브라우저 입장에서는 single thread 로 처리중인 javascript가 마치 동시에 처리되고 있는 것 처럼 보인다.

구동환경 애니메이션 보기: https://www.jsv9000.app/

4. Web APIs

Web api란?

웹 API는 JavaScript 엔진의 바깥에 위치한다. Ajax 요청, setTimeout(), 이벤트 핸들러의 등록과 같이 웹 브라우저에서 제공하는 기능들을 말한다. 그런데 중요한 것은 이러한 요청들의 처리가 JavaScript 엔진의 쓰레드와는 다른 쓰레드들에서 이뤄진다는 점이다. JavaScript 엔진의 스택에서 실행된 비동기 함수가 요청하는 비동기 작업에 대한 정보와 콜백 함수를 웹 API를 통해 브라우저에게 넘기면, 브라우저는 이러한 요청들을 별도의 쓰레드에 위임하게 되는 것이다. 그러면 그 쓰레드는 해당 요청이 완료되는 순간 전달받았던 콜백 함수를 JavaScript 엔진의 태스크 큐라는 곳에 집어넣는다.

예를 들어, setTimeout() 함수가 실행되면 JavaScript 엔진은 웹 API를 통해 브라우저에게 setTimeout() 작업을 요청하면서 콜백 함수를 전달하고, 브라우저는 이러한 타이머 작업을 별도의 쓰레드에게 위임한다. 그러고 나면 JavaScript 엔진의 스택에서는 setTimeout() 함수의 스택 프레임이 즉시 팝 된다. 그리고 인자로 명시한 시간이 흐르고 나면 해당 타이머 작업을 처리하고 있던 쓰레드는 전달받았던 콜백 함수를 JavaScript 엔진의 태스크 큐에 집어넣게 된다.

5. 이벤트 루프 (Event Loop)

한편, 위와 같은 동작이 가능하려면 매 순간 스택이 비어있는지 여부와 태스크 큐에 콜백 함수가 기다리고 있는지 여부를 확인해야만 한다. 이러한 역할을 수행하는 것이 바로 이벤트 루프이다. 즉, 이벤트 루프는 매 순간 스택이 비어있는지 확인을 해서 스택이 비어있다면 태스크 큐에 콜백 함수가 들어올 때까지 기다렸다가 첫 번째로 들어오는 콜백 함수를 스택에 쌓는 역할을 한다. 만약 스택이 비어있지 않다면 일반적인 방식으로 스택에서 함수의 호출을 처리하게 된다. 결국 이벤트 루프는 이름 그대로 같은 작업을 무한히 반복하는 무한 루프라고 생각하면 이해하기 쉬울 것이다. 참고로 이렇게 매 순간 태스크 큐와 스택을 확인하는 작업을 **틱(Tick)**이라고 부른다. 이벤트 루프의 동작 방식을 가상의 코드로 설명한다면 다음과 같다고 한다(출처 : MDN).

while (queue.waitForMessage()) {
    queue.processNextMessage();
}

The event loop facilitates this process; it constantly checks whether or not the call stack is empty. If it is empty, new functions are added from the event queue. If it is not, then the current function call is processed. (발췌)

6. 이벤트 루프 기반 비동기 처리 모델

  • 여러 팀에게 작업을 지시한 후, 그 팀들이 작업을 완료하면 결과를 받아보고 후속 조치를 하는 것과 비슷합니다. 메인 스레드(매니저)는 실제 작업을 하지 않고, 작업을 분배하고 결과를 받아 처리하는 역할을 합니다.
  • 메인 스레드는 작업을 백그라운드로 던져줌.
  • 각 작업이 완료되면 콜백을 통해 다시 메인 스레드에서 처리.

주요 장점

  • 병렬 처리: 메인 스레드가 실제로 무거운 작업을 직접 하지 않기 때문에, 여러 비동기 작업들이 백그라운드에서 병렬로 처리될 수 있습니다.
  • 비동기 작업의 효율적 관리: 메인 스레드는 이벤트 루프를 통해 여러 비동기 작업이 완료될 때마다 콜백을 순서대로 처리하여 효율적인 실행 흐름을 유지합니다.

결론

  • 메인 스레드의 핵심 역할은 작업을 외부 쓰레드에 위임하고 그 작업이 완료되면 콜백으로 처리하는 것입니다. 이를 통해 자바스크립트는 단일 스레드 모델을 유지하면서도, 비동기적인 비효율을 최소화하고, 동시성을 활용하여 복잡한 작업을 효율적으로 처리할 수 있습니다.
  • 웹 브라우저는 다양한 작업을 최대한 역할별로 여러 쓰레드에 분산시켜 병렬 처리함으로써 성능을 최적화합니다.최종적으로 자바스크립트의 단일 스레드는 특정한 핵심 역할을 맡고 있습니다.

7. 브라우저의 여러 쓰레드

브라우저는 성능과 효율성을 위해 여러 쓰레드를 사용하여 특정 작업을 비동기적으로 처리합니다. 예를 들면:

  1. 렌더링 엔진: 레이아웃 계산, 페인팅, 컴포지팅 작업 등을 처리하는 렌더링 관련 쓰레드.
  2. 네트워크 쓰레드: 네트워크 요청과 응답 처리 (XHR, Fetch API 등).
  3. 입출력(I/O) 쓰레드: 파일 읽기/쓰기, 데이터베이스 접근 등과 같은 I/O 작업.
  4. 타이머 쓰레드: setTimeout, setInterval 같은 타이머 작업을 비동기적으로 관리.
  5. GPU 쓰레드: 그래픽 렌더링, 하드웨어 가속 작업 등을 담당하는 GPU 관련 쓰레드.
  6. Compositor 쓰레드: 화면에 여러 레이어를 조합해 최종적인 결과물을 렌더링하는 쓰레드.

이 외에도, 브라우저는 비동기 작업을 효율적으로 처리하기 위해 여러 백그라운드 쓰레드를 활용합니다.

브라우저의 메인 스레드

  1. 작업 위임
    • 메인 스레드는 시간이 오래 걸리는 작업(예: 네트워크 요청, 파일 입출력, 렌더링 등)을 해당하는 쓰레드에 위임합니다. 예를 들어, XHR이나 fetch 요청을 보내면, 메인 스레드는 해당 요청을 네트워크 쓰레드로 넘기고 즉시 다음 작업을 수행할 수 있습니다.
  2. 비동기 작업 관리
    • 작업을 다른 쓰레드로 넘긴 후, 메인 스레드는 즉시 다른 코드를 계속해서 실행합니다. 이 덕분에 자바스크립트는 동시적으로 여러 비동기 작업을 처리할 수 있습니다.
  3. 콜백 처리
    • 다른 쓰레드에서 비동기 작업이 완료되면, 해당 작업의 결과는 콜백 형태로 메인 스레드에 전달됩니다. 이 때 메인 스레드는 Task Queue나 Microtask Queue에 있는 콜백 함수를 차례대로 실행합니다.
    • 예를 들어, setTimeout이나 Promise가 완료되면 그 콜백이 메인 스레드에서 실행됩니다.
  4. DOM 조작 및 UI 업데이트
    • 메인 스레드는 DOM 조작과 UI 업데이트도 담당합니다. 사용자가 버튼을 클릭하거나 폼을 제출하는 등의 UI 이벤트를 처리하고, 그에 따른 DOM 변경 작업도 메인 스레드에서 이루어집니다.
  • 브라우저는 다양한 작업을 병렬로 처리하지만, 자바스크립트 단일 스레드는 비동기 작업이 완료된 후 큐에 대기 중인 작업들을 순차적으로 실행합니다.
  • 병목 현상 방지: 브라우저의 여러 쓰레드는 네트워크 요청, 렌더링, I/O 작업 등 시간이 오래 걸리는 작업을 병렬로 처리하고, 자바스크립트 엔진의 단일 스레드는 완료된 비동기 작업의 콜백만 처리하므로 병목 현상을 줄입니다.

8. 태스크 큐 (Task Queue)

**태스크 큐는 웹 API를 처리하고 있던 쓰레드로부터 전달받은 콜백 함수들을 FIFO(First-In First-Out) 구조로 저장하고 있는 일종의 큐(Queue)**로, JavaScript 엔진 자체에 포함되어 있는 부분이다. 여기에 저장된 콜백 함수들은 스택이 비는 순간 스택에 순서대로 푸시된다. 이러한 원리로 비동기 작업이 완료된 이후 콜백 함수가 실행되는 것이다. 그런데 비동기 작업이 완료되어 태스크 큐에 콜백 함수가 들어가 있더라도 스택이 비어있지 않다면 해당 콜백 함수가 바로 실행되지 못한다는 특징이 있다. 그래서 setTimeout() 함수도 인자로 명시한 시간은 '잠드는 최소 시간'일 뿐, 그것보다 더 오래 잠들 수 있다.

*여기서 말하는 태스크란 곧 콜백 함수를 의미하기 때문에, 태스크 큐는 콜백 큐(Callback Queue)라고 부르고, 이벤트 큐, 메세지 큐라고도 부른다. (콜스택이 비어있을 경우에 등록된 함수를 콜스택에 추가)

9. 잡 큐 (Job Queue)

ECMA 2015 에서 나온 개념으로 Promises에 의해 사용된다. promises는 async 함수를 콜스택에 넣는 대신에 가능한 빨리 실행하도록 하는 방법이다. Promises 는 현재 함수가 끝나기 전에 resolve 된후 현재 함수가 끝나자마자 바로 실행된다. job queue는 event queue 보다 우선 권을 가지고 있어 event queue에 쌓인 함수보다 먼저 실행된다. (상위함수가 종료되기 전에 콜스택이 비어있지 않더라도 잡큐에 등록된 콜백을 콜스택에 추가)

const task = () => console.log('task');
const job = () => console.log('job');

const run = () => {
  console.log('callStack');
  setTimeout(task, 0);
  new Promise((resolve, reject) => resolve('job'))
  	.then((resolve) => job());
};

run();

// 'callStak'
// 'job'
// 'task'

요약

브라우저는 대부분의 무거운 작업(렌더링, 네트워크, I/O 등)을 별도의 쓰레드로 분리하여 비동기적으로 처리하고, 그 결과를 단일 자바스크립트 스레드에서 콜백 함수로 처리합니다. 단일 스레드는 자바스크립트 코드 실행, 이벤트 루프 관리, DOM 조작 등을 담당하며, 비동기 작업의 콜백을 순차적으로 실행하여 브라우저가 매끄럽게 동작할 수 있도록 합니다.

메인 스레드(자바스크립트의 단일 스레드)**의 주된 역할은 여러 작업을 적절한 백그라운드 쓰레드나 시스템 작업자에게 넘겨주고, 그 작업이 완료되었을 때 콜백을 실행하는 것입니다. 이 과정에서 자바스크립트는 비동기적으로 여러 작업을 처리할 수 있지만, 실제로 작업을 실행하는 것은 메인 스레드가 아닌 다른 쓰레드들입니다.