02. ⏳ 자바스크립트 엔진과 비동기 이벤트 루프

2026년 3월 5일 수정됨

📋 개요

실행 컨텍스트, 클로저, 그리고 이벤트 루프의 미세한 동작 원리를 통해 자바스크립트의 엔진 내부를 정복합니다.

📌 이 면접 항목의 목표

⏱️ 예상 읽기 시간: 25분 (핵심 요약: 12분)

🗺️ 이 챕터의 흐름
[개념 사전] → [질문 1: 이벤트 루프 & 큐] → [질문 2: 클로저 & 메모리] → [질문 3: 비동기 에러 핸들링]

🎯 이 챕터를 다 읽으면 할 수 있는 것

  • 마이크로태스크와 매크로태스크의 우선순위를 완벽히 설명할 수 있습니다.
  • 클로저로 인한 메모리 누수 패턴을 진단하고 해결할 수 있습니다.
  • async/await 내부의 상태 관리 메커니즘을 논리적으로 설명합니다.

📚 핵심 개념 사전 (Concept Glossary)

1. 실행 컨텍스트 (Execution Context)

자바스크립트 코드가 실행되는 환경(Scope, Hoisting, this 등)을 추상화한 개념입니다. 콜 스택(Call Stack)은 이 컨텍스트들이 쌓이고 빠지는 물리적인 통로입니다.

2. 클로저 (Closure)

함수가 선언될 때의 렉시컬 환경(Lexical Environment)을 기억하여, 함수가 외부 스코프 밖에서 실행될 때도 그 환경에 접근할 수 있는 기술입니다. 캡슐화와 상태 유지의 핵심입니다.

3. 태스크 큐 (Task Queue) vs 마이크로태스크 큐 (Microtask Queue)

  • 마이크로태스크: Promise.then, MutationObserver, queueMicrotask. 현재 실행 중인 자바스크립트가 끝나면 가장 먼저 전부 비워집니다.
  • 태스크(매크로태스크): setTimeout, setInterval, setImmediate. 브라우저가 렌더링을 마친 후, 루프의 한 사이클에서 하나씩 꺼내어 실행합니다.

🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'

  • 🐣 영철 ( 신입 ): "영호 님! setTimeout(..., 0)을 썼는데 왜 바로 실행이 안 되고 한참 뒤에 돌까요? 자바스크립트는 1초에 수억 번 연산한다면서요! 🤯"
  • 🦁 영호 ( 리드 ): "영철 님, 자바스크립트는 싱글 스레드입니다. 연산 속도가 아무리 빨라도 메인 스레드라는 1차선 도로가 막히면 아무것도 못 하죠. 특히 이벤트 루프의 신호 체계를 모르면 '유령 버그'에 평생 시달릴 겁니다. 자, 이 코드부터 분석해 보죠."

Q1. 이벤트 루프 동작 방식과 마이크로/매크로태스크 큐의 차이를 설명해 보세요.

🎯 출제 의도

비동기 자바스크립트의 실행 순서를 정확히 예측할 수 있는지, 그리고 메인 스레드 차단(Blocking) 문제를 어떻게 진단하는지 확인합니다.

🐣 영철이의 Naive 구현 (Bad Case)

영철이는 게시판 검색 기능을 구현하면서, 무거운 필터링 작업을 아무런 비동기 처리 없이 메인 스레드에서 돌려버렸습니다.

// 🐣 영철: "데이터가 10만 개뿐인데 금방 돌겠죠?"
searchButton.addEventListener('click', () => {
    showLoadingSpinner(); // ⚠️ 스피너가 안 돔! (메인 스레드 점유 때문)
    
    // ⚠️ 10만 개 데이터 필터링 (메인 스레드 차단)
    const results = expensiveFilter(allPosts, query); 
    
    renderResults(results);
    hideLoadingSpinner();
});

🦁 영호의 팩폭 조언
"영철 님, showLoadingSpinner를 호출해도 브라우저가 화면을 그릴 '틈'(Render step)을 주지 않고 곧바로 무거운 연산을 시작하면, 사용자는 멈춘 화면만 보게 됩니다. 이벤트 루프는 메인 스레드가 완전히 비워질 때까지 렌더링을 시도조차 못 하거든요."

🦁 영호의 아키텍처 가이드 (Good Case)

영호 리드는 비동기 큐를 활용하여 브라우저에게 숨 쉴 틈을 주는 방식을 제안합니다.

// 🦁 영호: "메인 스레드를 잘게 쪼개어 브라우저에게 렌더링 기회를 주세요."
searchButton.addEventListener('click', async () => {
    showLoadingSpinner();
    
    // 브라우저가 스피너를 그릴 수 있도록 마이크로태스크 큐로 한 템포 넘김
    await new Promise(resolve => setTimeout(resolve, 0)); 
 
    // 무거운 작업을 Web Worker로 넘기거나, Task를 분할함
    const results = await runInWorker(expensiveFilter, allPosts, query);
    
    renderResults(results);
    hideLoadingSpinner();
});

📊 레벨별 답변 가이드 (Self-Check)

  • Level 1 (Junior): "자바스크립트는 싱글 스레드라 한 번에 하나의 일만 합니다. 비동기 작업은 Web API로 넘겼다가 콜백 큐에 쌓이고, 콜 스택이 비면 이벤트 루프가 이를 하나씩 꺼내 실행합니다."
  • Level 2 (Senior): "마이크로태스크 큐가 매크로태스크 큐보다 우선순위가 높음을 설명합니다. 하나의 매크로태스크가 실행된 후 브라우저는 렌더링 기회를 가지며, 그 직전에 마이크로태스크 큐가 완전히 비워져야 한다는 '렌더링 사이클'과의 관계를 설명합니다."
  • Level 3 (Specialist): "이벤트 루프 기반 아키텍처에서 'Starvation(기아 상태)' 문제를 설명할 수 있습니다. 마이크로태스크가 계속해서 마이크로태스크를 생성할 경우 매크로태스크나 렌더링이 영원히 실행되지 않을 수 있음을 지적하고, 이를 해결하기 위한 리액트의 SchedulerMessageChannel 활용 사례를 예로 듭니다."

Q2. 실행 컨텍스트(Execution Context)와 클로저(Closure)의 개념을 설명하고, 이것이 상태 관리 라이브러리 내부에서 어떻게 활용되는지 유추해 보세요.

🎯 출제 의도

자바스크립트의 가장 강력하면서도 위험한 기능인 '클로저'의 동작 원리를 이해하고, 이를 이용해 어떻게 외부에서 접근할 수 없는 독립된 상태를 만드는지(캡슐화) 설계 능력을 확인합니다.

🐣 영철이의 Naive 구현 (Bad Case)

영철이는 게시판 필터링 상태를 전역 변수로 관리하다가, 여러 컴포넌트에서 값을 덮어쓰는 바람에 버그가 발생했습니다.

// 🐣 영철: "전역 변수에 담아두면 어디서든 쓰기 편하니까요!"
let globalFilter = { category: 'all' };
 
function updateFilter(newCategory) {
    globalFilter.category = newCategory; // ⚠️ 외부에서 누구나 수정 가능 (예측 불가능)
}

🦁 영호의 팩폭 조언
"영철 님, 전역 변수는 공유의 축복이 아니라 오염의 저주예요. 클로저를 쓰면 특정 함수만 접근할 수 있는 '닫힌 방(Scope)'을 만들 수 있습니다. 리액트의 useState가 왜 리렌더링 사이에서도 값을 기억하는지, 그 뿌리가 바로 여기에 있죠."

🦁 영호의 아키텍처 가이드 (Good Case)

영호 리드는 클로저를 활용해 상태를 캡슐화하는 '간이 Store' 구조를 보여줍니다.

// 🦁 영호: "접근 권한을 통제하세요. 이것이 상태 관리의 기본입니다."
function createStore(initialState) {
    let state = initialState; // 🔒 외부에서 직접 접근 불가 (클로저 변수)
 
    return {
        getState: () => state,
        setState: (newState) => {
            state = { ...state, ...newState }; // 불변성 유지
            console.log('상태 업데이트 완료!');
        }
    };
}
 
const filterStore = createStore({ category: 'all' });
filterStore.setState({ category: 'notice' });
console.log(filterStore.getState()); // { category: 'notice' }
// state 변수 자체에는 절대 직접 접근할 수 없음!

📊 레벨별 답변 가이드 (Self-Check)

  • Level 1 (Junior): "클로저는 함수가 선언된 환경을 기억하는 것입니다. useState 같은 훅에서 이전 값을 기억할 때 사용됩니다."
  • Level 2 (Senior): "실행 컨텍스트의 Lexical EnvironmentOuter Reference의 관계를 설명합니다. 상위 함수가 종료되어 호출 스택에서 사라져도, 하위 함수가 참조하고 있다면 메모리에서 유지되는 메커니즘을 설명합니다."
  • Level 3 (Specialist): "클로저로 인한 메모리 누수 사례(캡처된 변수가 의도치 않게 큰 객체를 참조할 때)를 사례로 들고, useState 내부 구현에서 클로저를 통해 어떻게 독립적인 상태 인덱스를 관리하는지(Fiber 노드 연동) 아키텍처 관점에서 설명합니다."

Q4. 가비지 컬렉션(GC)의 동작 원리와 메모리 누수 사례를 설명해 보세요.

  • 🎯 출제 의도: 자바스크립트 엔진의 자동 메모리 관리 메커니즘을 이해하고, 개발자의 부주의로 인해 메모리가 해제되지 않는 상황(누수)을 방어할 수 있는지 확인합니다.
  • 💡 핵심 원리 & 답변: 자바스크립트는 'Reachability(도달 가능성)'라는 개념을 바탕으로 레퍼런스 카운팅이나 마크 앤 스윕(Mark-and-Sweep) 알고리즘을 사용해 더 이상 참조되지 않는 객체를 메모리에서 해제합니다. 하지만 전역 변수에 큰 객체를 할당해 두거나, 해제되지 않은 이벤트 리스너, 혹은 클로저 내부에서 의도치 않게 거대한 외부 변수를 계속 참조하고 있을 경우 GC가 이를 회수하지 못해 메모리 누수가 발생합니다. 특히 SPA 환경에서는 페이지 이동 시에도 메모리가 유지되므로, 컴포넌트 언마운트 시 클린업 로직을 철저히 관리하는 것이 시니어의 역량입니다.

Q6. 자바스크립트의 비동기 처리 방식(Promise, async/await)이 내부적으로 어떻게 상태를 관리하나요?

  • 🎯 출제 의도: 비동기 문법의 설탕(Sugar) 너머에 있는 마이크로태스크 큐와 상태 머신(State Machine)의 동작 원리를 이해하고 있는지 확인합니다.
  • 💡 핵심 원리 & 답변: Promisepending, fulfilled, rejected 세 가지 상태를 가지는 객체이며, .then() 콜백은 마이크로태스크 큐에 쌓여 현재 콜 스택이 비는 즉시 실행됩니다. async/await은 제너레이터(Generator)와 프로미스를 결합한 형태로, await 키워드를 만나면 실행 컨텍스트를 일시 중단하고 비동기 작업이 완료될 때까지 제어권을 이벤트 루프에 넘깁니다. 이는 동기적인 코드 흐름을 유지하면서도 메인 스레드를 차단하지 않는 우아한 비동기 프로그래밍을 가능하게 합니다.

Q13. 이벤트 루프 관점에서 Promise.resolve().then()과 setTimeout(..., 0) 중 어떤 콜백이 먼저 실행되나요?

  • 🎯 출제 의도: 태스크 큐 간의 명확한 우선순위 차이를 실무 수준에서 구분할 수 있는지 확인합니다.
  • 💡 핵심 원리 & 답변: 마이크로태스크 큐(Promise.then)가 매크로태스크 큐(setTimeout)보다 명백히 높은 우선순위를 가집니다. 이벤트 루프는 콜 스택이 비는 즉시 마이크로태스크 큐에 쌓인 모든 작업을 소진할 때까지 다음 단계(렌더링이나 매크로태스크 실행)로 넘어가지 않습니다. 따라서 Promise.resolve().then()이 항상 setTimeout(0)보다 먼저 실행됩니다. 만약 마이크로태스크 안에서 계속해서 새로운 마이크로태스크를 생성하면, setTimeout이나 화면 UI 업데이트가 아예 멈버버리는 '기아 상태(Starvation)'에 빠질 수 있으므로 주의해야 합니다.

🐣 영철이의 복기 일기

오늘 setTimeout(0)이 단순한 0초 대기가 아니라, '현재 하던 일을 다 끝내고 브라우저에게 다음 차례를 양보하겠다'는 정중한 제스처라는 걸 배웠다. 그동안 유령처럼 나를 괴롭히던 "화면 멈춤" 현상이 내가 브라우저의 마이크로태스크 고속도로를 꽉 막고 있어서 그랬다니... 미안해 브라우저야! 😅

💡 "자바스크립트 싱글 스레드는 외로운 길이다. 혼자 다 하려 하지 말고, 적절히 큐에 일을 맡겨 브라우저와 협업하라."

내일은 영호 님이 가장 싫어하시는 '클로저 메모리 누수'에 대해 처절하게 깨져볼 예정이다. 메모리 프로파일러 켜고 대기해야지! 💻🔥