이벤트 루프에 대해서 설명해주세요.
마이크로태스크 큐와 매크로태스크 큐에 대해서 아시나요?
위 두 개의 질문은 프론트엔드 개발자 면접에서 들을 수 있는 질문입니다.
질문을 받고 어디서부터 어디까지 설명할 수 있나요?
이벤트 루프를 이해하기 전 간단하게 실행 컨텍스트에 대해 짚고 넘어가보겠습니다.
const foo = () => {}
const bar = () => {}
foo();
bar();
함수를 호출하게 되면 함수 코드가 평가되어 함수 실행 컨텍스트가 생성됩니다.
이때 생성된 함수 실행 컨텍스트는 콜 스택(call stack, 혹은 실행 컨텍스트 스택)에 push 되고 함수 코드가 실행되게 됩니다.
함수가 호출된 순서대로 순차적으로 실행되는 이유는 함수가 호출된 순서대로 실행 컨텍스트가 call stack에 push되기 때문입니다. 따라서 함수의 실행 순서는 call stack으로 관리되게 됩니다.
자바스크립트 엔진은 단 하나의 call stack(콜 스택, 실행 컨텍스트 스택)을 갖고 있습니다.
단 하나의 call stack을 갖는다는 말은 함수를 실행할 수 있는 창구가 단 하나 뿐이기 때문에 동시에 2개 이상의 함수를 실행할 수 없다는 것을 의미합니다.
한 번에 하나의 태스크(task)만 실행할 수 있는 것을 싱글 스레드(single thread) 방식으로 동작한다고 합니다.
하지만 자바스크립트 코드를 작성해 결과를 보다보면 동시에 여러 개의 함수들이 실행되는 것처럼 느껴질 때가 있습니다. 이는 싱글 스레드의 방식으로 인한 블로킹(blocking, 작업 중단)을 보다 효과적으로 처리하기 위한 비동기 처리 때문입니다.
비동기(asynchronous) 처리란 현재 실행 중인 태스크가 종료 되지 않은 상태라 하더라도 다음 태스크를 곧바로 실행하는 방식을 말합니다. 비동기 처리 방식으로 동작하는 함수로는 타이머 함수(setTimeout(), setInterval()), HTTP 요청과 이벤트 핸들러(eventHandler)가 있습니다.
💡 자바스크립트는 싱글 스레드, 브라우저는 멀티 스레드
자바스크립트는 싱글 스레드 방식으로 동작합니다.
이때 싱글 스레드 방식으로 동작하는 것은 브라우저가 아닌 브라우저에 내장된 자바스크립트 엔진이기 때문에 브라우저는 멀티 스레드로 동작한다는 것에 주의해야 합니다.
만약 모든 자바스크립트 코드가 자바스크립트 엔진에서 싱글 스레드 방식으로 동작한다면 자바스크립트는 비동기로 동작할 수 없기 때문입니다.
HTML 요소가 애니메이션 효과를 통해 움직이면서 이벤트를 처리하거나 HTTP 요청을 통해 서버로부터 데이터를 갖고 오며 렌더링하기도 합니다. 이런 자바스크립트의 동시성(concurrency)을 지원할 수 있도록 도와주는 것이 이벤트 루프입니다.
이벤트 루프는 브라우저에 내장돼 있는 기능 중 하나로 브라우저 환경을 그림으로 표현하면 아래와 같습니다.
구글의 v8 자바스크립트 엔진은 크게 heap과 call stack, 2개의 영역으로 구분할 수 있습니다.
📝 용어 정리
1️⃣ Heap
- 객체가 저장되는 메모리 공간
- 콜 스택의 요소는 힙에 저장된 객체를 참조
- 구조화 X
2️⃣ Call Stack- 소스코드 평가 과정에서 생성된 실행 컨텍스트가 추가/제거되는 스택 자료구조
- 실행 컨텍스트 스택이라고도 함
자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행시킵니다. 비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 혹은 Node.js가 담당하게 됩니다.
예를 들면, 비동기 방식인 setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당하지만 호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 브라우저/Node.js가 담당하게 된다는 의미입니다.
이러한 과정을 위해 브라우저는 태스크 큐와 이벤트 루프를 제공하고 있습니다.
📝 용어 정리
1️⃣ Task Queue
- task queue === event queue === callback queue
- 비동기 함수의 콜백 함수/이벤트 핸들러가 일시적으로 보관되는 영역
2️⃣ MicroTask Queue- 태스크 큐와는 별개
- 프로미스의 후속 처리 메서드의 콜백 함수가 일시적으로 보관
- 태스크 큐보다 우선순위가 높음 ⭐️
3️⃣ Event Loop- 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지 & 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인
- 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있다면 순차적(FIFO, 선입선출)으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동
싱글 스레드, 동기와 비동기 처리, 이벤트 루프, 매크로태스크 큐, 마이크로태스크 큐...
많은 키워드가 지나갔는데 더 확실히 차이를 살펴보고 정립하기 위해서 하나의 예시를 살펴봅시다.
// 출력하고자 하는 내용
① 1+1
② 1초 뒤 2+2
③ 3+3
위와 같은 지문을 출력하고 싶다고 할 때 일반 프로그래밍 언어들은 아래 파이썬과 같이 출력하고 싶은 순서대로 작성할 수 있습니다. 보통 위에서 아래 방향으로 코드가 실행되기 때문입니다.
CODES | OUTPUT |
---|
좌측 사진과 같이 코드를 작성하고 확인을 해보면 우측과 같이 순서대로 결과가 터미널에 찍히는 것을 확인할 수 있습니다.
그럼 자바스크립트는 어떻게 될까요?
CODES | OUTPUT |
---|
동일하게 다른 프로그래밍 언어들과 마찬가지로 출력되길 원하는 순서대로 코드를 작성하면 좌측 사진과 같습니다. 하지만 작성한 순서대로 결과가 콘솔에 찍히지 않는 것을 볼 수 있습니다. 시간이 더 걸리는 코드가 나중에 출력되고 있어요.
코드의 작성된 순서와 관계 없이 더 빨리 처리되는 코드부터 처리를 해주네?
자바스크립트는 병렬 처리를 해주는 코드인가봐 🫢
하는 생각이 들 수 있지만 아닙니다.
다시 한 번 위에서 설명된 내용들을 토대로 코드를 살펴보겠습니다.
위에서 작성한 자바스크립트 코드는 아래와 같은 공간들에 배치되게 됩니다.
바로 처리할 수 없는, 어느 정도의 시간이 소요되는 작업의 경우에는 call stack에 쌓이지 않고 web API라는 코드들의 대기실과 같은 공간으로 보내집니다.
즉시 처리할 수 없는 코드들은 대기실로 좌천된다고 생각하면 편할 것 같네요.
call stack은 모든 코드들이 일사불란하게 처리돼야 하는 굉장히 바쁜 공간입니다.
출퇴근의 지하철과 같은 공간이죠. 바빠 죽겠는데 누가 가운데에 덩그러니 서 있으면 방해되잖아요.
이렇게 오래 걸려 공간을 차지하게 될 코드들을 따로 web API라는 공간에 모아둠으로써 모든 코드들이 call stack에서는 순서대로 빠르게 처리될 수 있게 됩니다.
Call Stack이 이름 그대로 stack의 자료 구조를 나타내는 것이라면
후입선출이어야 하는데 왜 선입선출인 것처럼 동작하나요?
예시에서 console.log(1+1)
, console.log(2+2)
, console.log(3+3)
를 사용했고, 결과 값이 후입선출이라면 역순으로 6, 4, 2가 나와야 하는 것 아니냐는 의문이 생길 수 있을 것 같습니다.
콜 스택(call stack)의 후입선출(LIFO)은 주로 함수 호출과 관련이 있습니다.
즉, 함수가 호출되면 해당 함수의 실행 컨텍스트가 call stack의 맨 위에 푸시(push)되고, 함수가 종료되면 해당 실행 컨텍스트가 팝(pop)되어 콜 스택에서 제거됩니다.
하지만 console.log
는 함수 호출이라기보다는 브라우저나 실행 환경에서 제공하는 API이며, 이 API를 사용하여 결과를 콘솔에 출력합니다.
따라서 console.log
는 단순히 순차적으로 실행되는 문장이기 때문에 콜 스택에 푸시되지 않습니다. 이러한 코드들은 실행되는 즉시 완료되며 그 결과가 콘솔에 출력되게 됩니다.
따라서 이 경우 콜 스택의 동작 방식인 후입선출(LIFO)과는 직접적인 관련이 없습니다.
코드의 실행은 작성된 순서대로 순차적으로 진행되며, 결과가 콘솔에 출력되는 것은 브라우저나 실행 환경이 처리하는 부분이므로 결과가 콘솔에 출력되는 순서는 실행 컨텍스트의 푸시와 팝과는 무관합니다. 브라우저나 실행 환경의 내부 로직에 따라 결정되게 때문입니다.
다시 예시로 돌아오자면 :
바로 처리할 수 있는 코드들이 call stack에서 처리되고 난 다음에서야 callback queue에 있는 코드들이 실행될 기회가 옵니다.
web API에서는 시간이 걸리는 코드들이 모여 있는 대기 장소라고 설명했습니다.
예시에서의 setTimeout은 코드에 적힌 대로 1초 뒤에 실행돼야 하기 때문에 1초 동안 web API에서 대기해야 합니다.
onClick과 같은 이벤트 리스너들은 해당 이벤트가 발생할 때까지 대기해야 하고요.
대기가 끝난 web API의 코드들은 callback Queue로 보내지게 됩니다.
Queue에서 다시 한 번 차례대로 대기를 해야 합니다. Call Stack의 모든 코드들이 실행되어 공간이 비워져야만 callback Queue의 코드들이 실행될 수 있기 때문입니다.
이벤트 루프(event loop)는 web API에서 대기가 완료되어 실행할 준비가 된 코드들을 순서대로 대기시킨 callback Queue(task Queue)의 코드들의 실행 타이밍을 확인해주는 역할을 합니다.
call stack이 비워졌는지 확인하고 비워졌을 때에만 callback queue의 코드들을 call stack으로 올려보내 실행될 수 있도록 해줍니다.
Event Loop에 대해서 설명해주세요.
이벤트 루프는 큐에 할당된 함수를 순서에 맞춰 호출 스택(call stack)에 할당해주는 역할을 합니다.
이벤트 루프는 비동기 작업을 관리하는 매커니즘으로 프로그램이 외부 이벤트를 감지하고 적절히 처리할 수 있도록 도와줍니다.
call stack이 다 비워지면 callback queue에 존재하는 함수를 하나씩 call stack으로 옮기는 역할을 합니다.
그럼 매크로태스크 큐와 마이크로태스크 큐는 뭘까요?
위의 callback Queue(Task Queue)을 세부적으로 매크로태스크 큐와 마이크로태스크 큐로 나눌 수 있습니다.
마이크로태스크 큐와 매크로태스크 큐 모두 콜백함수가 들어간다는 점에서 공통점을 갖지만 어떤 함수를 실행하는지에 대한 차이가 있습니다.
이벤트 루프는 마이크로태스크 큐의 모든 태스크들을 처리한 다음, 태스크 큐의 태스크들을 처리합니다.
MACRO | MICRO |
---|
이름을 보면 짐작해볼 수 있지만 비교적 적게 걸리는 건 마이크로태스크 큐에서, 비교적 오래 걸리는 규모가 큰 코드는 매크로태스크 큐에서 처리를 하게 됩니다.
또한 명칭은 큐(Queue)이지만 자료구조의 큐와는 다릅니다.
엄밀히 말하자면 우선순위 큐 (Priority Queue) 라고 할 수 있는데,
이벤트 루프가 각각의 큐에서 태스크를 꺼내는 조건이 “제일 오래된 태스크”(FIFO, 선입선출) 이기 때문입니다.
마이크로태스크 큐(혹은 task queue)와 마이크로태스크 큐에 대해서 설명해주세요.
| Macrotask Queue(Task Queue)
매크로태스크 큐에는 주로 비교적 오랜 시간이 소요되는 함수들의 처리에 사용됩니다.
대표적인 예로는 setTimeout, setInterval, XMLHttpRequest, 이벤트 핸들러 등이 있습니다.
이러한 작업들은 브라우저나 Node.js 환경에서 비교적 오래 걸리는 작업으로 처리되며, 주로 콜 스택이 완전히 비어있을 때 실행됩니다.
| Microtask Queue
마이크로태스크 큐는 일반적으로 더 빠른 작업에 사용됩니다.
대표적으로 Promise의 처리, MutationObserver 콜백, queueMicrotask 등이 해당됩니다.
마이크로태스크 큐의 작업은 현재 실행 중인 작업이 완료된 직후에 처리되며, 이러한 특성 때문에 매크로태스크 큐보다 우선순위가 높다고 볼 수 있습니다.
아래 GIF를 보면 더 잘 이해가 될 것 같습니다 🙂
References
📚 BOOKS
🎥 VIDEOS
👩🏻💻 BLOGS
🌠 IMAGES