자바스크립트 동시성 모델

GJ·2025년 6월 13일
0

프론트엔드지식

목록 보기
17/17

자바스크립트는 기본적으로 싱글 스레드(Single-threaded) 언어이다. 이는 한 번에 하나의 작업만 처리할 수 있음을 의미한다. 그러나 자바스크립트는 네트워크 요청(예: 데이터 가져오기), 타이머(예: setTimeout), 사용자 이벤트(예: 클릭)와 같이 시간이 오래 걸리는 비동기 작업들을 메인 스레드를 멈추지 않고 처리한다. 이 모든 것은 비동기 실행 환경(Asynchronous Execution Environment) 또는 동시성 모델(Concurrency Model) 덕분이다.

이 모델은 몇 가지 핵심 구성 요소들로 이루어져 있다.


자바스크립트 비동기 실행 환경의 핵심 구성 요소

  1. 자바스크립트 엔진 (JavaScript Engine)
    자바스크립트 코드를 파싱하고, 컴파일하고, 실행하는 역할을 담당한다. V8 엔진(Chrome, Node.js), SpiderMonkey(Firefox), JavaScriptCore(Safari) 등이 있다.

    • 호출 스택 (Call Stack): 자바스크립트 코드가 실행될 때 함수 호출들을 추적하는 스택(Stack, 후입선출 LIFO) 구조다. 함수가 호출되면 스택에 쌓이고, 함수 실행이 완료되면 스택에서 제거된다. 모든 자바스크립트 코드는 이 호출 스택 위에서 실행된다. 단일 스레드이기 때문에, 스택에 함수가 쌓여 있는 동안에는 다른 어떤 작업도 실행될 수 없다. 스택이 가득 차거나 너무 오래 걸리는 동기 함수가 실행되면 "블로킹(Blocking)"이 발생한다.
    • 힙 (Heap): 객체나 변수 같은 데이터가 저장되는 메모리 공간이다. 엔진이 직접 메모리를 할당하고 관리한다.
  2. 런타임 환경 (Runtime Environment)
    자바스크립트 엔진만으로는 부족한, 자바스크립트 코드가 외부 세상과 상호작용할 수 있도록 해주는 추가적인 구성 요소들을 포함한다. 주요 런타임 환경은 브라우저Node.js다.

    • 웹 API (Web APIs) / Node.js API (Node.js APIs):

      • 웹 API (브라우저): setTimeout(), fetch(), XMLHttpRequest(), DOM 조작 (document.getElementById(), addEventListener()), localStorage 등 브라우저가 자바스크립트에게 제공하는 기능들이다. 이들은 브라우저에 내장되어 있으며, 자바스크립트 엔진과는 별개로 존재한다.
      • Node.js API (Node.js): fs 모듈(파일 시스템 접근), http 모듈(네트워크 통신), setTimeout() 등 Node.js 런타임이 제공하는 기능들이다. 이들 역시 V8 엔진 외부에 존재한다.
      • 역할: 자바스크립트 엔진의 메인 스레드를 블로킹하지 않고 비동기 작업을 수행할 수 있도록 해주는 외부 환경의 기능들이다.
    • 태스크 큐 (Task Queues):

      • 매크로태스크 큐 (Macrotask Queue / Callback Queue): setTimeout, setInterval, DOM 이벤트 핸들러, fetch 콜백 등이 비동기 작업 완료 후 대기하는 곳이다. 이 큐는 런타임 환경에서 관리된다.
      • 마이크로태스크 큐 (Microtask Queue): Promise의 .then(), .catch(), .finally() 콜백, queueMicrotask() 콜백 등이 대기하는 곳으로, 매크로태스크 큐보다 높은 우선순위를 가진다. 이 큐 역시 런타임 환경에서 관리된다.
    • 이벤트 루프 (Event Loop):

      • 자바스크립트 비동기 실행 환경의 핵심적인 "조정자"다. 호출 스택이 비어 있는지 지속적으로 확인하고, 호출 스택이 비어 있다면 큐에 대기 중인 콜백 함수들을 호출 스택으로 옮겨 실행시키는 역할을 하는 무한 루프다.
      • 작동 원리:
        1. 이벤트 루프는 호출 스택이 완전히 비어 있는지 지속적으로 모니터링한다.
        2. 호출 스택이 비어 있다면, 가장 먼저 마이크로태스크 큐를 확인한다. 큐에 있는 모든 마이크로태스크를 호출 스택으로 차례대로 이동시켜 실행시킨다. 마이크로태스크 큐가 완전히 비워질 때까지 이 과정을 반복한다.
        3. 마이크로태스크 큐가 완전히 비워지면, 매크로태스크 큐(콜백 큐)를 확인한다. 큐에 있는 첫 번째 매크로태스크를 호출 스택으로 이동시켜 실행시킨다.
        4. 이 매크로태스크의 실행이 완료되어 호출 스택이 다시 비워지면, 이벤트 루프는 다시 2단계(마이크로태스크 큐 확인)부터 이 과정을 반복한다.
      • 특징: 이벤트 루프는 런타임 환경에 포함되어 엔진과 큐, 웹/Node.js API들을 연결하고 조율하는 시스템이다. 이는 단일 스레드인 자바스크립트가 논블로킹 비동기 작업을 처리하는 핵심 메커니즘을 제공한다.

비동기 작업 처리 흐름 (예시: setTimeout과 Promise)

  1. 초기 코드 실행: 모든 동기 코드는 호출 스택에 쌓여 순서대로 실행된다.
  2. 비동기 함수 호출:
    • setTimeout(callback, delay): setTimeout 함수는 호출 스택에 쌓인 후 웹 API로 전달된다. 웹 API는 타이머를 시작하고, 메인 스레드는 블로킹되지 않고 다음 코드를 실행한다. delay 시간이 지나면 callback 함수는 매크로태스크 큐로 이동하여 대기한다.
    • new Promise((resolve, reject) => { /* ... */ }).then(callback): Promise 생성자 내의 동기 코드는 즉시 실행된다. resolvereject가 호출되면, .then()에 등록된 callback 함수는 마이크로태스크 큐로 이동하여 대기한다.
  3. 호출 스택 비워짐: 모든 동기 코드의 실행이 끝나고 호출 스택이 비워진다.
  4. 이벤트 루프 동작:
    • 이벤트 루프는 호출 스택이 비어 있음을 감지한다.
    • 먼저 마이크로태스크 큐를 확인한다. 큐에 있는 모든 콜백 함수(Promise 콜백 등)를 호출 스택으로 한꺼번에 옮겨 실행시킨다.
    • 마이크로태스크 큐가 완전히 비워질 때까지 이 과정을 반복한다.
    • 마이크로태스크 큐가 비워지면, 매크로태스크 큐(콜백 큐)를 확인한다. 큐에 있는 첫 번째 콜백 함수(setTimeout 콜백, DOM 이벤트 콜백 등)를 호출 스택으로 옮겨 실행시킨다.
    • 이 콜백 함수가 실행되는 동안 또 다른 동기 코드가 실행될 수 있으며, 새로운 비동기 작업이 시작될 수도 있다.
  5. 반복: 호출 스택이 다시 비워지면 이벤트 루프는 위 과정을 반복하여 큐에 있는 다음 콜백을 처리한다.

주요 개념 재확인

  • 메시지 큐에 콜백을 실행 중이고 아직 많이 남아있을 때 동기 코드가 생기면 어떻게 되는가?
    매크로태스크 큐에서 하나의 콜백 함수가 호출 스택으로 옮겨져 실행되는 동안, 그 콜백 함수 안에서 새로운 동기 코드가 생성되거나 실행된다면, 그 동기 코드는 해당 콜백의 실행 흐름 내에서 먼저 완료된다. 콜백 함수가 완전히 종료되고 호출 스택이 다시 비워져야 이벤트 루프가 큐에 있는 다음 콜백을 가져올 수 있다. 즉, 하나의 매크로태스크가 실행되는 동안은 동기적으로 동작한다.

  • 호출 스택은 메시지 큐에서 하나씩 빼서 넣는가?
    매크로태스크 큐(Callback Queue)에서는 이벤트 루프가 한 번에 하나의 콜백만 호출 스택으로 옮긴다. 하지만 마이크로태스크 큐에서는 호출 스택이 비워진 직후 큐에 있는 모든 마이크로태스크를 한 번에 호출 스택으로 옮겨 실행시킨다.

  • 큐가 왜 두 개 있는가? (매크로태스크 큐 vs. 마이크로태스크 큐)
    우선순위가 다른 두 가지 주요 큐가 존재한다.

    • 매크로태스크 큐 (Macrotask Queue) / 콜백 큐: setTimeout, setInterval, I/O (Node.js), UI 렌더링, requestAnimationFrame, MessageChannel 콜백, DOM 이벤트 등. 비교적 큰 작업 단위를 가진다.
    • 마이크로태스크 큐 (Microtask Queue): Promise .then(), .catch(), .finally() 콜백, queueMicrotask(), MutationObserver 콜백 등. 매크로태스크보다 높은 우선순위를 가지며, 현재 실행 중인 매크로태스크가 완료된 후 다음 매크로태스크로 넘어가기 전에 모든 마이크로태스크가 실행된다.
  • queueMicrotask()는 주로 언제 사용하는가?
    queueMicrotask()는 주어진 콜백 함수를 마이크로태스크 큐에 추가한다. 다음과 같은 경우에 사용한다:

    • 동기 코드 실행 직후, UI 업데이트 직전에 로직 실행: Promise와 유사하게 현재 동기 코드 블록의 실행이 끝난 직후, 브라우저가 화면을 렌더링하기 전에 특정 로직을 실행하고 싶을 때 유용하다.
    • Promise 체인과 유사한 즉각적인 비동기 동작 보장: Promise를 사용하지 않으면서도 Promise .then()과 동일한 시점에 비동기적으로 코드를 실행해야 할 때 사용한다.
    • 잠재적인 경쟁 조건(Race Condition) 방지: 어떤 작업을 즉시 실행하되, 현재 실행 중인 모든 동기 작업이 완료된 후에 실행되도록 하여 예측 불가능한 상태 변화를 방지할 때 사용한다.

이러한 구성 요소들이 긴밀하게 상호작용하면서, 자바스크립트는 단일 스레드 언어임에도 불구하고 복잡하고 동적인 웹 애플리케이션을 효율적으로 구축한다.

profile
Frontend Developer

0개의 댓글