Javascript

[JavaScript] 비동기 코드의 동작 원리 - 이벤트 루프(The Event Loop)

코딩 1.5 2023. 12. 29. 20:37

앞서 AJAX와 API 무엇인지 학습했고, 비동기 코드와 Promise에 대해서 배웠다. 이번 포스팅에서는 자바스크립트 이면에서 해당 코드들이 어떻게 동작하는지 이해해보자.

 

Javascript 런타임

Javascript 런타임은 기본적으로 컨테이너이다. 이 컨테이너에는 자바스크립트 코드를 실행하는 데 필요한 모든 요소(pieces)가 포함되어 있다. 각 요소에 대해서 알아보자.

 

Javascript Engine

Javascript 런타임의 핵심은 엔진(Engine)이다. 엔진(Engine)은 힙(Heap)과 콜 스택(Call Stack)으로 구성되어 있다. 힙(Heap)에서는 객체가 저장되고, 콜 스택(Call Stack)에서는 코드가 실행된다. 여기서 기억해야할 사실은 Javascript의 실행 쓰레드(Thread of Execution)는 하나만 존재한다는 것이다. (자바와 같은 경우는 동시에 여러 개의 코드를 실행할 수 있지만 자바스크립트는 그렇지 않다)

Web APIs

Web API는 기본적으로 자바스크립트 엔진에서 제공된다. 하지만 자바스크립트 언어 자체는 아니며 DOM, 타이머, Fetch API, Geolocation API 등이 존재한다. 

Callback Queue

데이터 구조로 이벤트가 발생했을 때 콜백(Callback) 함수를 즉시 실행할 수 있도록 해준다. 콜백 큐(CallBack Queue)는 기본적으로 실행될 모든 콜백의 정렬된 목록이다. 할일 목록(To do list)와 비슷한 개념으로 생각하면 된다.

Event Loop

Javacscript의 엔진에 있는 콜 스택(Call Stack)에 실행할 코드가 비어있으면 이벤트 루프(Event Loop)는 콜백 큐(Callback queue)에서 콜백(Callback)들을 가져다가 콜 스택(Call Stack)에 넣으며 실행시킨다. 이벤트 루프(Event Loop)는 자바스크립트가 비동기 코드를 실행 가능하게 하는 핵심 개념이며, 동시성 모델(concurrency model)을 가질 수 있는 이유이다. 여기서 동시성 모델이란 언어가 동시에 여러 가지를 처리하는 방법을 의미한다. 그렇다면 동시성이 없는 자바스크립트가 어떻게 이벤트 루프를 통해 동시성 모델울 가지며 비동기 코드를 실행할 수 있을까?

 

자바스크립트 런타임에 대한 설명

 

 

Event Loop의 동작 방식

Event Loop의 동작 방식을 이해하기 위해 아래와 같은 실제 코드 예제를 통해 알아보자.

코드 예제

 

첫번째 줄 코드인 '이미지를 선택'하는 코드를 실행하게 되면 아래의 그림과 같이 Call Stack에 실행 코드가 쌓이게 된다. 

첫번째 코드 실행으로 콜 스택의 스택이 쌓인 모습

 

'이미지에 속성을 설정'하는 두번째 줄 코드를 실행하면 이미지를 비동기적으로 로드하기 시작한다. 아래의 그림의 'WEB APIs'에서 이미지가 로딩되는 것을 확인할 수 있다. (첫번째 줄의 코드는 Callstack에서 사라진 뒤이다) DOM과 관련된 모든 건 Javascript가 아니라 Web API의 일부이다. 다시 말해, Web API 환경에서 DOM과 관련된 비동기 작업이 실행되는 것이다. Timer와 AJAX 그리고 다른 모든 비동기 작업도 마찬가지이다. 즉, 이미지 로딩은 Call Stack에서 같은 메인 스레드 실행이 아니라 Web API 환경에서 실행된다. (만약, 이미지 로딩이 Call Stack에서 실행된다면 이미지가 로딩될 때까지 자바스크립트 코드는 멈춰있어야 한다.) 이미지 로딩이 끝난 뒤에 무언가를 하고 싶다면 'load' 이벤트 리스너를 활용해야한다. 

두번째 줄 코드 실행으로 인한 이미지를 비동기적으로 로드하는 모습

 

세번째 줄 코드를 실행하면 아래의 그림처럼 이벤트 리스너의 콜백 함수를 WEB API에 등록한다. 해당 콜백 함수는 WEB API에서 대기하다가 이미지 로딩 완료되어 'load' 이벤트가 방출(emit)되면 실행된다. 이전에 배웠듯이 콜백과 함께 비동기적으로 코드를 처리하는 것을 확인할 수 있다.

세번째 줄 코드 실행으로 이벤트의 콜백 함수가 Web API에 등록된 것을 확인

 

다음 줄에서는 fetch API를 이용해 AJAX를 호출한다. 비동기식 Fetch API는 Web API에 등록된다. 그렇지 않으면 Call Stack을 차단하고 자바스크립트 실행 지연을 만들어내기 때문이다.

 

Web API에 Fetching가 등록된 모습

 

마지막으로 Fetch 함수가 반환한 Promise에 then 메서드를 Web API에 등록한다. 이는 Promise가 향후 처리된 값을 반응할 수 있도록 한다.

Web API에 then과 메소드가 등록된 모습

 

이제 모든 상위 레벨의 비동기적으로 코드 실행을 완료하였다. 아래의 이미지와 같이 백그라운드에 이미지 로딩과 Fetch API가 등록되어 있는 것을 확인할 수 있다.

 

이제 '이미지 로딩'이 완료되어 'load' 이벤트의 콜백 함수가 실행된다고 해보자. 해당 콜백 함수는 콜백 큐(CallBack Queue)에 들어가게 된다. 콜백 큐(Callback Queue)는 기본적으로 실행될 모든 콜백 함수의 정렬된 목록이다. 여기에선 예시가 없지만 기존에 Callback Queue에 콜백 함수가 존재하면, 새로운 콜백은 Queue의 맨 끝으로 간다. 이는 중요한 사실이다. 타이머를 5초를 설정했다고 가정해보자. 5초 후에 타이머의 콜백이 Callback Queue의 맨 끝에 놓이게 된다. 하지만, 앞서 콜백이 존재하고 해당 콜백을 모두 실행하는데 1초가 걸렸다고 가정한다면 타이머 콜백은 최종적으로 5초가 아니라 6초 후에 실행된다. 즉, 타이머 함수의 시간이 보장되지 않는다는 뜻이다. 유일한 보장은 타이머 콜백이 5초 전에는 실행되지 않는다는 것이다. 또한, 콜백 큐(CallBack Queue)에는 DOM 이벤트의 클릭과 키 프레스와 같은 콜백도 포함된다는 사실이다.

 

 

이제 Event Loop가 하는 역할을 알아볼 차례이다. Event Loop는 Call Stack을 살펴보고 Global Execution Context를 제외하고 비어있는지 아닌지 판단한다. Call Stack이 비어있다면 실행되는 코드가 없다는 뜻이므로 Callback Queue의 첫번째 콜백을 Call Stack에 등록한다. 이 과정을 Event Loop Tick이라고 한다. Event Loop는 매번 Callback Queue에서 콜백을 받는다. 즉, Event Loop는 Call Stack과 Callback Queue를 조정하여 콜백이 언제 실행될지 결정한다. 자바스크립트 언어 자체에는 시간 개념이 없다. 이에 비동기 코드 실행을 관리하고 어떤 코드가 다음에 실행될지 결정하는 것은 런타임의 Event Loop이다. 자바스크립트 엔젠 자체는 주어진 코드를 잘 실행할 뿐이다. 이를 통해 싱글 쓰레드인 자바스크립트가 어떻게 비동기 코드를 막힘없이 실행할 수 있는지 알 수 있게 되었다. 

 

하지만 아직 모든 개념이 정리된 것이 아니다. 아직 Promise가 남아 있다. Promise는 조금 다르게 동작한다. fetching data가 완료되었다고 가정해보자. 앞서 Promise를 다루기 위해 우리는 then에 콜백을 등록해 놓았고, 이 콜백은 Callback Queue에 바로 등록되지 않는다. Promise는 Microtasks Queue에서 관리한다. Microtasks Queue는 Callback Queue와 동일하게 작동하지만 Microtasks Queue는 Callback Queue보다 우선순위가 있다. Event Loop는 Callback Queue의 콜백을 확인하기 전에 먼저 Microtasks Queue에 콜백이 있는지 먼저 확인한다. Callback Queue가 비어있든 아니든 상관 없이 Microtasks Queue 콜백을 먼저 모두 실행시킨다. 만약, Microtasks Queue에 새로운 콜백이 추가되면 Callback Queue가 재실행되기 전에 먼저 실행된다. 이는 Microtasks Queue를 계속 추가하면 Callback Queue를 실행시키지 못하게하여 굶길 수 있다는 의미이다.