이메일 보기
Javascript

이벤트루프 알아보기

최종 수정일2024년 9월 16일

자바스크립트는 한 번에 하나의 일만 처리할 수 있는 싱글 스레드 언어입니다. 하지만 실제로 자바스크립트를 사용해보면 어떤가요? 데이터를 가져오는 동안 다른 함수를 실행할 수 있고 setTimeOut으로 지정한 시간 뒤에 특정 함수를 실행시키려 할 때 다른 함수가 바로 실행되는 등 동시에 여러 일을 처리하는 것처럼 보입니다. 이는 자바스크립트가 실행되는 런타임 환경인 브라우저가 비동기 작업을 지원하기 때문입니다. 오늘은 브라우저에서 비동기 작업을 처리하는 이벤트 루프(Event Loop)에 대해서 알아보도록 하겠습니다.

자바스크립트 엔진과 런타임 환경


자바스크립트 런타임 환경

자바스크립트 엔진은 자바스크립트 코드를 해석하고 실행하는 프로그램입니다. 자바스크립트가 싱글 스레드 언어이기 때문에 자바스크립트 엔진은 한 번에 하나의 일만 수행 가능하며 이는 함수의 실행을 관리하는 콜 스택이 하나라는 의미이기도 합니다. 대표적으로 크롬과 Node.js에서 사용하는 V8엔진이 있습니다.


자바스크립트 런타임 환경은 자바스크립트가 실행되는 환경, 즉 크롬과 같은 브라우저(혹은 Node.js) 입니다. 브라우저에서 제공하는 Web API들(fetch, setTimeOut, eventListner...)을 통해 비동기 작업을 지원하며 이벤트 루프를 통해 비동기 작업의 우선 순위를 정하고 처리합니다.


콜 스택 (Call Stack)


콜 스택은 자바스크립트 엔진이 함수의 실행 순서를 관리하기 위해 사용하는 스택(Stack) 자료구조입니다. 함수가 호출되며 생성된 실행 컨텍스트가 콜 스택에 차곡차곡 쌓이게 되며 가장 나중에 호출된 함수가 가장 먼저 실행되는LIFO(Last In First Out) 구조입니다.


태스크 큐 (Task Queue)


태스크 큐브라우저에서 태스크(작업)를 관리하는 큐(Queue) 자료구조입니다. 비동기적으로 처리되는 작업들(event listner의 콜백함수, setTimeOut 함수의 콜백함수 등)을 관리하는 일종의 대기소 역할을 합니다. setTimeOut에서 지정한 시간이 됐거나 click 이벤트가 발생한 시점에 작업(콜백함수)을 태스크 큐에 넣어두어 대기시킨 후 콜 스택이 비워졌을 때 가장 먼저 들어온 작업부터 하나씩 콜 스택으로 이동하는 FIFO(First In First Out) 구조입니다.


마이크로태스크 큐 (Microtask Queue)


마이크로태스크 큐는 브라우저에서 promise의 콜백함수 즉, then, catch, finally와 mutation observer의 콜백함수 등을 관리하는 큐(Queue) 자료구조입니다. 태스크 큐 보다 높은 우선 순위를 가지며 콜 스택이 비워졌을 때 먼저 들어온 작업부터 하나씩 콜 스택으로 이동시켜 실행시킵니다. 태스크 큐와 마이크로태스크 큐가 나누어진 이유는 작업 처리에 우선 순위를 두어 태스크 큐에 있는 작업에 영향을 받지 않고 마이크로태스크를 동일한 환경에서 처리할 수 있기 때문이라고 합니다.


이벤트 루프 (Event Loop)란?


이벤트 루프는 브라우저에 내장된 비동기 작업을 처리하는 매커니즘입니다. 말 그대로 계속 루프를 돌며 콜 스택과 태스크 큐, 마이크로태스크 큐를 관찰하며 처리할 작업(콜백함수)이 있다면 순서대로 처리합니다.


이벤트루프를 통한 비동기 작업 순서


앞서 본 이미지를 보며 비동기 작업이 어떤 순서로 진행되는지 알아보겠습니다.


자바스크립트 런타임 환경

  1. 이벤트 루프는 콜 스택이 비워져 있는지 확인합니다.
  2. 콜 스택이 비워져 있지 않다면 작업을 콜 스택으로 이동시키지 않습니다.
  3. 콜 스택이 비워져 있다면 마이크로태스크 큐에 있는 작업들을 들어온 순서대로 하나씩 콜 스택으로 이동시킵니다. 앞서 언급했듯 마이크로태스크 큐는 태스크 큐보다 높은 우선 순위를 갖습니다.
  • 콜 스택으로 이동한 콜백함수가 실행, 종료 된 후 콜 스택이 비워지게 되면 다시 마이크로태스크 큐에 있는 작업을 콜 스택으로 이동시킵니다. 이 과정은 마이크로태스크 큐에 있는 모든 작업이 비워질 때까지 반복됩니다. 만일 콜 스택에서 함수가 실행되고 있는 중간에 마이크로태스크 큐에 작업이 추가된다면 해당 작업마저 모두 실행된 후 마이크로태스크 큐가 비워져야 합니다.
  1. 콜 스택이 비워져 있고 마이크로태스크 큐에 들어가 있는 작업이 없다면 태스트 큐에 들어있는 작업을 하나씩 들어온 순서대로 처리합니다. 태스크 큐에 있는 모든 작업이 비워질 때 까지 1~4를 반복합니다.

이벤트 루프는 위와 같은 과정을 통해 비동기 작업을 처리합니다.


실제로 코드를 통해 이벤트 루프 동작을 살펴볼 겸 한가지 재밌는 실험을 해볼까요? setTimeOut(()=>, 0)이 정말 0ms 후에 실행되는지 알아보겠습니다.


javascript
const runAfterZero = () => {
    const runTime = performance.now();
    console.log("시작!"); // 1번 
    setTimeout(() => {
        const afterTime = performance.now();
        console.log(`실행하는데 걸린 시간 : ${afterTime - runTime}ms`); //2번
    }, 0);
    console.log("끝!"); //3번
    };
runAfterZero();

이벤트 루트 동작 확인

setTimeOut 0는 0ms 뒤에 실행 되지...않았습니다...!


  1. 전역 컨텍스트가 생성되어 콜 스택에 쌓입니다.
  2. runAfterZero 함수가 호출되며 콜 스택에 runAfterZero 함수의 실행 컨텍스트가 쌓입니다.
  3. 1번 로그가 찍힙니다.
  4. setTimeOut이 호출되며 0ms 후에 태스크 큐에 콜백함수가 들어가게 됩니다.
  5. 2번 로그가 찍힙니다.
  6. 함수의 실행 모두 종료된 후 함수 컨텍스트가 콜 스택에서 제거됩니다.
  7. 전역의 코드가 모두 종료된 후 전역 컨텍스트가 콜 스택에서 제거됩니다.
  8. 콜 스택이 모두 비워진 후 태스크 큐에 있는 setTimeOut의 콜백함수가 실행되며 3번 로그가 찍힙니다. 실행되는데 걸린 시간은 0.5ms이네요.🤓

여기서 주목해야 할 점은 콜 스택이 비워져 있어야 남아있는 작업을 처리할 수 있다는 것입니다. 만약 실행되는데 아주 오래 걸리는 코드가 있다면 어떨까요? 그 시간동안 마이크로태스크 큐, 태스크 큐에 등록된 작업들은 그저 대기하고 있을 수 밖에 없습니다.


javascript
const runAfterZero = () => {
    const runTime = performance.now();
    console.log("시작!"); // 1번 코드
    setTimeout(() => {
        const afterTime = performance.now();
        console.log(`실행하는데 걸린 시간 : ${afterTime - runTime}ms`); //2번 코드
    }, 0);
    console.log("끝!"); //3번 코드
    };
runAfterZero();

const start = Date.now();
const duration = 3000;
while (Date.now() - start < duration) {
    const elapsed = Date.now() - start;
}

아까와 같은 코드이지만 전역에 3초 동안 실행되는 반복문을 추가해보았습니다. 어떨게 될까요? 두근두근


3초 후 실행

3초 동안 반복문이 실행되고 전역 코드가 모두 종료되어 전역 컨텍스트가 제거된 후 setTimeOut의 콜백함수가 실행됩니다.😱 이렇게 되면 성능에도 좋지 않고 사용자가 화면을 클릭 했을 때 등록된 이벤트 리스너 콜백이 바로 실행되지 않는 등 사용성이 굉장히 떨어질 수 밖에 없습니다. 따라서 setTimeOut(()=>,0)을 사용하여 복잡한 코드를 나누어 태스크 큐에 넣어두는 방식으로 작업을 관리할 수 있습니다. (저는...그렇게까지 코드 관리해 본 적은 없지만요...)


그리고 브라우저는 성능 최적화와 안정적인 사용자 환경을 위해 콜 스택에 있는 함수가 모두 실행된 후 화면의 변경을 업데이트합니다. 보통의 경우에는 60fps (약 16ms) 주기로 업데이트 되지만 콜 스택에서 굉장히 오랫동안 실행되는 코드가 있다면 정상젹으로 UI가 업데이트 되지 않는 이슈가 발생할 수 있기 때문에 역시 너무 크고 복잡한 작업을 나누어 관리하는 것이 중요합니다.


UI 업데이트 테스트

콜 스택에서 함수가 실행될 동안 mouse hover로 인한 UI 업데이트가 되지 않음


포스팅을 마치며


오늘은 자바스크립트 런타임 환경에서의 이벤트 루프에 대해 알아보았습니다. 코드를 한줄 한줄 읽어서 실행하는 인터프리어 언어..(하지만 호이스팅!), 싱글 스레드를 가진 동기식 언어..(자, setTimeOut에 코드를 비동기로 싸서 드셔보세요😋) 자바스크립트 당신은 도대체...? 종잡을 수 없지만 알고 보면 다 나름의 이유가 있기 때문에 자바스크립트를 열심히 공부하는 것이 중요하다고 생각됩니다.


특히 이번에 이벤트 루프를 정리하며 제가 전역 컨텍스트에 대해서 오해하고 있다는 걸 알게 되었습니다. 저는 전역 컨텍스트가 앱이 실행되는 동안에 계속 콜 스택에 남아 있다고 생각했습니다. 그래서 전역 컨텍스트가 있는데 어떻게 콜 스택이 비워질 수 있지? 하고 고뇌했는데 알고보니 코드 실행이 종료되면 전역 컨텍스트도 사라지게 되는거였습니다. (급하게 실행 컨텍스트 포스트를 수정하러 가는 중 🏃‍♀️🏃‍♀️🏃‍♀️) 이렇게 글로 정리해보며 기존에 잘못 알고 있던 것들도 바로 잡을 수 있어서 블로그를 운영하는 목적을 잘 달성하고 있는 것 같아서 기분이 좋습니다. (하지만 피그마 실력은 도통 나아지질 않습니다... 괴롭...)


뜬금 없지만 내일이 바로 추석입니다. 이 글을 읽으신 분들 모두 즐거운 추석 보내시길(혹은 보내셨길) 바라며 글을 마치겠습니다.🌝👋👋

게시글의 오류 지적, 내용 보충, 질문 등의 피드백은 언제나 환영입니다.
아래 댓글창 혹은 ysisys0202@gmail.com으로 남겨주세요.