자바 스크립트의 작동 방식
Event Loop를 논하기에 앞서서 자바스크립트의 동작 방식에 대해서 알 필요가 있다. 자바스크립트는 기본적으로
싱글 스레드
로 작동하며, 한 번에 하나의 작업을 처리한다. 이 작업들은 콜 스택
이라는 곳에 실행 되며, stack
이라는 구조 특성상 가장 마지막에 들어온 작업이 먼저 실행되는 LIFO(Last-In, First-out)
구조로 실행된다.
자바 스크립트는 이러한 구조가진 싱글 스레드로 작동하게 된다. 문제는 실행 시간이 긴 작업이나 네트워크 요청과 같은 비동기 작업이 발생하면, 작업이 완료될 때까지 콜 스택이 비워지지 않아 다음 작업들이 대기하게 된다. 이는 곧, 브라우저가 멈추는
블로킹(Blocking)
현상을 초래하여 사용자 경험을 저해한다. 그런데 사실 이러한 동작방식 때문에,
블록킹
을 경험해보는 일은 거의 없다. 사실 자바 스크립트가 블로킹 없이 작업을 처리할 수 있는 이유는 브라우저가 제공하는 WebAP
I와 Event Loop
를 통해서 비동기 작업을 처리하기 떄문이다. Web API
Web API는 타이머, 네트워크 요청, 파일 입출력, 이벤트 처리등 브라우저에서 제공하는 다양한 API를 포괄하는 총칭이다. Web API는 브라우저에서 멀티 스레드로 구현되어 있기 때문에, 자바 스크립트의 스레드를 차단하지 않고 다른 스레드를 사용하여 동시에 작업을 처리할 수 가 있다. 따라서 동기적인 작업은 자바스크립트의
callstack
에서 처리하고, 비동기 작업들 ( setTimeout()
, fetch()
, XMLHttpRequest
, DOM이벤트
)는 브라우저의 WebAPI에서 처리한다. 물론 모든 WebAPI가 비동기로 처리되는 것은 아니고, 언급한 api 외에도 수많은 WebAPI가 있다. 아래 링크에서 더 많은 종류를 확인할 수 있다.
Web API 목록
WebAPI 자체적으로는 많은 기능들이 있지만, 현재 블로그의 주제 범위안에서 어떻게 비동기 함수가 WebAPI에 위임되는 과정을 보면 다음과 같다.
.gif?table=block&id=2399c76c-6cb4-80dc-898e-c22d289d72f1&spaceId=b4216657-966f-4c29-ae8c-42f6c4adb66d&expirationTimestamp=1757527200000&signature=7ss7DmTfUpHuim-CERU1grx4bvMVJo7Pu5F22KAMyxA)
과정:
- 콜스택에 한 작업씩 등록이 된다. → 이 때 한 작업이 완료가 되어아 다음 작업이 실행된다.
- 비동기 프레임이 콜스택에 잠시 올라갔다가 브라우저의 WebAPI로 위임한다.
- WebAPI는 위임받은 비동기 작업을 백그라운드에서 독립적으로 처리한다. 이 과정에서는 자바 스크립트의 콜스택과는 분리 되어 진행된다.
- 위임받은 비동기 작업이 완료되면, 해당 작업과 연결된
콜백함수
는콜백 큐( Callback Queue)
의 대기열에 추가 된다.
- Call stack이 비워져 있을때, Callback Queue에 등록된 함수가 push되고 실행되어 종료된다.
Callback QueueCallback Queue
는 queue자료구조의 특성상FIFO(First-In, First-Out)
으로 처리된다.C
allback Queue
는 사실 두개의 대기열을 가지고 있는데, 일반 함수를 처리하는Task Queue
와 좀 더 높은 우선순위를 가진Microtask Queue
가 있다. Promise의.then()
,.catch()
,.fianlly()
콜백이나queueMicrotask()
로 등록된 콜백은Microtask Queue
에 등록되어 이벤트 루프에 의해 먼저 처리된다.
이러한 방식으로 Web API는 자바스크립트 엔진의 부담을 줄이고, 자바스크립트가 싱글 스레드임에도 불구하고 동시에 여러 비동기 작업을 처리할 수 있게 해준다.
Event Loop
이벤트 루프는 자바스크립트 런타임의 핵심적인 부분으로
끊임없이 콜스택이 비어있는지
, 콜백 큐에 대기중인 함수가 있는 지
감시하며 루프를 돌기 때문에 이벤트 루프라고 한다. 이 이벤트 루프가 Callback Queue
에 등록된 함수들을 Callstack
으로 push해주는 역할을 한다. 어찌보면 생각보다 단순하게 작동하는 Even Loop
를 더 잘 이해하기 위해선 Run-to-Completion
동작 방식을 알아야한다.Run-to-Completion
Run-to-Completion
은 자바 스크립트의 싱글 스레드 특성에서 파생된 개념인데, 한 번 콜 스택에 들어간 함수는 중간에 멈추거나 다른 작업에 의해 방해받지 않고 끝까지 실행된다
는 것을 의미한다. 따라서 아무리 급한 비동기 작업의 콜백 함수가 콜백 큐에 대기하고 있어도, 콜 스택에 현재 실행 중인 함수가 있다면 그 함수가 완료될 때까지 기다려야 한다.하지만 이러한 특성 때문에, 자바스크립트는 예측 가능한 방식으로 작동하고, 동시성으로 인한 여러가지 문제를 피할 수 있게 되는 것이다. 이러한 특성을 고려해보면, 왜
setTimeout(callback, delay)
가 왜 정확한 시간에 맞게 실행되지 않는 이유를 알 수 있다. {3,4} console.log('1'); setTimeout(function () { console.log('2'); }, 0); console.log('3')
위의 예시코드를 보면, 지연시간이 0초 이니까 콘솔로그는
1 → 2 → 3
로 찍히겠구나 하며 직관적으로 생각이 들지만, 실제로 코드를 돌려보면 1 → 3 → 2
순서로 찍히게 된다. 왜 그렇게 작동하는 지 동작 과정을 한 번 순서대로 생각해보자..gif?table=block&id=2399c76c-6cb4-8093-a95d-c4d58f213230&spaceId=b4216657-966f-4c29-ae8c-42f6c4adb66d&expirationTimestamp=1757527200000&signature=tBSdkcXNzBKquinc7Lzr7jSIZlhCybj0W6mCCNgf-24)
동작과정:
console.log(’1’)
이 콜 스택에 푸시되어 실행된다.
setTimeout()
이 실행된다. 이setTimeout
은Web API
이므로 콜 스택에 등록되었다가 제거되고, 내부콜백 함수
와delay
정보가 Web API에 위임이 된다.
console.log(’3’)
이 콜 스택에 푸시되어 실행된다.
- Web API에서 타이머가 만료 (
0ms
)되면, 콜백 함수는 콜백 큐로 이동해서 대기한다.
- 이벤트 루프는 콜 스택이 비어있는 것을 확인하고, 콜백 큐에 대기 중인 콜백 함수를 발견한다.
- 이벤트 루프는 콜백 큐의 함수를 꺼내서 콜 스택으로 push한다.
- 콜 스택에 푸시된 콜백 함수가 실행된다. →
2
출력
여기서 중요한 것은, setTimeout의 delay시간이 0ms이더라도, 콜백 함수는 즉시 콜 스택으로 들어가지 못하고 콜 스택이 비워지기를 기다려야 한다는 것이다. 그래서 콜 스택에 먼저 들어가있던 작업들이 오래걸린다면, setTimeout의 콜백함수는 0초로 설정했다고 하더라도 훨씬 더 오랜 시간을 기다리게 될 수도 있는 것이다.
따라서 setTimeout의 delay는 이 시간이 지나면 실행해주세요! 가 아니라, 최소한 이 시간 이후에 실행해주세요 가 되는 것이다. 따라서 해당 시점에 콜스택의 상황에 따라 실행되는 시간이 달라질 수 있다는 것이다.
결론
이번 블로그에서 어떻게 자바스크립트가 싱글 스레드임에도 불구하고 WebAPI와 이벤트 루프를 통해서 non-blocking으로 작동할 수 있는지 알아보았다. 각 동작 방식을 이해하면 자바 스크립트로 비동기 코드를 작성하고 디버깅하는 데 좀 더 도움이 되지 않을까 생각한다.