“Event Loop”는 무엇인가? (Javascript)

#event loop

Written by Paul
Video preview
우리가 이벤트 루프가 무엇인지 알아보기 전에

자바스크립트의 주요 구성 요소는 무엇인가요?

  • 호출 스택(Call Stack): 프레임의 스택
  • 태스크 큐(Task Queue): 실행 대기 중인 큐 (비동기 함수들, 예를 들어 콜백들이 이 큐에 쌓임)
  • 힙 메모리(Heap Memory): 구조화되지 않은 메모리 공간
즉, 호출 스택의 작업이 모두 끝난 후 태스크 큐에 쌓인 일들이 호출 스택이 비어 있을 때 실행됩니다.
결과적으로 이벤트 핸들러는 먼저 실행되지 않을 수 있습니다.

자바스크립트에서 비동기는 어떻게 작동할까요?

자바스크립트는 싱글 스레드(Single Threaded) 입니다.
즉, 하나의 함수가 완전히 실행될 때까지 다른 함수는 현재 실행 중인 함수를 방해할 수 없습니다.
그럼, 자바스크립트는 비동기 함수를 어떻게 처리할까요?
자바스크립트는 비동기 작업을 이렇게 처리합니다:
while(queue.waitForMessage()){ queue.processNextMessage(); }
자바스크립트는 큐에 메시지가 오기를 기다리고 있습니다.
그 후 queue.waitForMessage()true가 되면(즉, 큐에 메시지가 오면) 자바스크립트는 해당 메시지를 실행합니다.
만약 큐에 이전에 쌓인 메시지가 있다면, 그 메시지는 앞의 메시지가 모두 실행된 후에 실행됩니다.
자, 그럼 다음과 같은 자바스크립트 코드를 보겠습니다!
console.log('Hi!'); // 동기 setTimeout(() => { console.log('Yay!'); }, 4000); // 비동기 console.log('Bye!'); // 동기
위의 콘솔 로그에서는 "Hi!"와 "Bye!"가 동기적으로 실행됩니다.
결과적으로 console.log('Hi!');는 호출 스택에 첫 번째로 쌓이고, 실행이 끝난 후 호출 스택에서 빠져나옵니다. 그래서 콘솔에는 "Hi!"가 먼저 출력됩니다.
그 후 setTimeout 함수가 호출 스택에 쌓이지만, 호출 스택에서 빠져나가면서 아무런 일이 일어나지 않습니다. 대신 "Bye!"가 콘솔에 출력됩니다. 그 후 4초가 지나면 콘솔에 "Yay!"가 출력됩니다.
왜 이런 일이 발생할까요?
우리는 이런 순서를 예상할 수 있었습니다.
Hi! → 4초 기다림 → Yay! → Bye
여기서 우리는 이벤트 루프를 볼 수 있습니다.
이벤트 루프의 주요 역할은 호출 스택을 감시하는 것입니다.
즉, 이벤트 루프에 쌓인 이벤트들은 호출 스택을 지켜보고 있다가 호출 스택이 비어 있으면, 그 이벤트들이 호출 스택에 쌓여 실행됩니다.
위 코드를 다시 살펴봅시다.
모든 동기 함수들(즉, console.log들)이 호출 스택에서 빠져나가면, setTimeout의 콜백 함수는 4000ms 후에 이벤트 루프에서 실행됩니다.
그럼, setTimeout 함수가 0ms 지연 시간을 갖는다면 어떻게 될까요?
console.log('Hi!'); // 동기 setTimeout(() => { console.log('Yay!'); }, 0); // 비동기 console.log('Bye!'); // 동기
결과는 똑같습니다!
Hi! -> Bye! -> Yay!
그 이유는 간단합니다.
지연 시간이 0ms여도 비동기 콜백이 동기처럼 실행되지는 않기 때문입니다.

웹 API

웹 API는 비동기입니다.
setTimeout 함수도 웹 API의 예입니다. 그래서 마지막에 실행됩니다. fetch 함수도 웹 API의 예시입니다.
console.log('Hi! again.'); fetch(.......).then(() => { console.log('Fetched!'); }); console.log('Bye! again.');
위 코드는 fetch 함수를 사용하는 예시입니다.
위와 같은 코드의 결과는 이렇게 출력됩니다.
Hi! again. -> Bye! again. -> Fetched!
먼저 console.log('Hi! again')는 호출 스택에 쌓여 실행됩니다.
그 후 fetch 함수가 호출 스택에 쌓이지만, fetch는 웹 API이므로 XHR이 끝날 때까지 실행되지 않습니다.
하지만 fetch는 호출 스택에서 빠져나가며 그 뒤에 있는 console.log('Bye! again')가 실행됩니다.
그 후 XHR이 완료되면 fetch의 콜백 함수인 console.log('Fetched!')가 태스크 큐에 쌓이고, 이벤트 루프는 호출 스택이 비어 있음을 감지합니다.
그 콜백 함수는 호출 스택에 쌓이고, 마지막으로 console.log('Fetched!')가 실행됩니다.
좀 더 어려운 예시를 살펴봅시다.
console.log('Oops!'); button.addEventListener('click', () => { console.log('hehe did you click me?'); }); setTimeout(() => { console.log('I waited 3000ms!'); }, 3000); console.log('Yay!');
위 코드가 어떻게 실행될지 예상할 수 있나요?
결과는 이렇게 출력됩니다!
Oops! Yay! I waited 3000ms!
hehe did you click me?는 버튼을 클릭할 때만 실행됩니다.
따라서 "Oops!"와 "Yay!"가 먼저 출력되고, 버튼 클릭 이벤트의 로그는 3000ms 후에 실행됩니다.
이 3000ms는 정확히 3초를 의미하나요?
답은 아니요.
3000ms는 최소 3초의 대기 시간이며, 이 시간 이후에 모든 함수가 실행되는 것은 아닙니다.
따라서 setTimeout에 0ms를 설정하면, setTimeout은 비동기 함수로 실행됩니다.
그 이유는 최소 대기 시간이 0ms이기 때문입니다. 이는 콜백이 0ms 내에 실행된다는 의미가 아니라, 비동기 함수로 실행되어야 함을 뜻합니다.

비동기 함수의 장점

그렇다면, 웹 브라우저의 렌더링은 비동기인가요?
네! 렌더링 자체도 웹 API의 일종입니다.
브라우저 렌더링은 비동기 이벤트입니다.
우리는 이를 "렌더 큐(Render Queue)"라고도 부릅니다. 렌더 큐는 다른 비동기 웹 API들보다 우선순위가 높습니다. (하지만 동기 함수보다는 낮은 우선순위를 가집니다.)
브라우저는 매 16ms마다 렌더링을 큐에 쌓고, 큐가 비어 있으면 렌더링을 처리합니다.
하지만 호출 스택에 동기 이벤트가 많이 쌓이면 브라우저 렌더링이 지연됩니다.
왜냐하면 호출 스택이 다른 동기 함수들을 실행한 후에야 렌더링을 처리하기 때문입니다.
따라서 만약 계산이 너무 많은 동기 함수로 처리된다면, 브라우저가 멈추게 됩니다.
그 이유는 브라우저 렌더링 함수가 모든 동기 계산 함수가 완료될 때까지 지연되기 때문입니다.
하지만 큰 작업을 비동기 함수로 처리하면, 브라우저는 정상적으로 작동합니다.
왜냐하면 렌더 함수는 다른 비동기 함수들보다 우선순위가 높기 때문에, 렌더 함수는 다른 비동기 함수들 사이에서 처리될 수 있기 때문입니다. (매 16ms마다)

이벤트 루프를 차단하지 마세요!

이벤트 루프를 차단하지 마세요!
즉, 무거운 작업들은 비동기 함수로 처리해야 합니다. (동기 함수로 처리하지 마세요.)
비동기 함수는 이벤트 루프를 차단하지 않습니다.
이들은 자바스크립트의 백그라운드에서 실행되고, 작업이 끝나면 그 콜백이 호출 스택에 쌓여 실행됩니다.

웹 API와 자바스크립트 엔진

console.log()와 같은 함수는 동기 함수입니다. (네이티브 자바스크립트 함수)
이들은 자바스크립트 함수 자체입니다. 따라서 이벤트가 아닙니다.
하지만 실제 웹 애플리케이션에서는 많은 이벤트들을 처리합니다.
우리는 배열을 반복하거나 계산하는 것만으로 실제 웹 애플리케이션을 만들 수 없습니다.
그렇다면 이러한 웹 API 이벤트들이 어떻게 실행될까요?
그 답은 자바스크립트 엔진에 있습니다.
예를 들어, 구글의 V8 엔진이 있습니다.
현재 Chrome 브라우저는 V8 자바스크립트 엔진을 사용하고 있습니다.
위 링크에서 자바스크립트 엔진에 대한 자세한 내용을 확인할 수 있습니다.
이 자바스크립트
엔진에서 실행되는 이벤트들은 모두 비동기 함수입니다.
이 함수들은 브라우저 API와 함께 실행됩니다.
setTimeout 함수도 자바스크립트 엔진에서 실행됩니다.
그리고 이벤트 리스너도 마찬가지입니다.
이 자바스크립트 엔진들을 이해하면 이벤트 루프를 더 잘 이해할 수 있습니다.
← Go home