본문 바로가기

[리액트] 1.5 이벤트 루프와 비동기 통신의 이해

출처 김용찬, 모던 리액트 Deep Dive, 위키북스, 2023년

 

자바스크립트는 싱글 스레드에서 작동한다. 즉, 자바스크립트는 한 번에 하나의 작업만 동기 방식으로 처리할 수 있다.

자바스크립트에서 하나의 코드가 실행하는 데 오래 걸리면 뒤이은 코드가 실행되지 않는 것을 "Run-to-completion"이라고 한다. 

최신 기술
Node.js에 새롭게 추가된 Worker나 브라우저에서 제공하는 WebWorker를 활용하면 동시에 여러 작업을 처리할 수 있다. 

 

 동기 synchronous 란?

직렬 방식으로 작업을 처리하는 것.

동기 방식에서는 하나의 요청이 시작된 이후 무조건 응답을 받은 이후에야 다른 작업을 처리할 수 있다. 그동안 다른 작업은 모두 대기한다.

- 장점: 개발자가 동시성을 고민할 필요가 없다.

- 단점: 한 번에 다양한 많은 작업을 처리할 수 없다. 사용자 경험에 좋지 않다.

 

동기 asynchronous 란?

병렬 방식으로 작업을 처리하는 것.

비동기 방식에서는 하나의 요청을 시작한 후 응답이 오든지 안 오든지 상관없이 다음 작업이 이루어진다.

- 장점: 한 번에 여러 작업이 실행될 수 있다.


 

그런데 싱글 스레드 기반의 자바스크립트에서도 많은 양의 비동기 작업이 이루어진다. 비동기 처리를 도와주는 이벤트 루프를 비롯한 다양한 개념 덕분이다.

코드 순서대로 코드가 실행되지 않는 예

 

호출 스택과 이벤트 루프

호출 스택 call stack은 자바스크립트에서 수행해야 할 코드나 함수를 순서대로 담아두는 스택이다.

 

<예시 1> 동기 함수의 호출 스택

함수들이 호출 스택에 쌓였다가 더 이상 실행될 코드가 없으면 호출 스택에서 제거된다.

 

✨ 이벤트 루프란?

이벤트 루프는 ECMAScript 자바스크립트 표준에 나와 있는 내용은 아니다. 이벤트 루프는 자바스크립트 엔진이 처리해주지 못하는 비동기 작업을 지원하기 위해 만들어진 장치이다. 이벤트 루프는 런타임 환경에서 관리된다.

이벤트 루프의 역할은 호출 스택에 실행 중인 코드가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 것이다.

호출 스택이 비었다면 태스크 큐에 대기 중인 작업이 있는지 확인하고, 실행 가능한 오래된 작업부터 순차적으로 꺼내와서 자바 스크립트 엔진으로 실행한다.

 

여기서 짚고 넘어가야 할 점은 '코드를 실행하는 것'과 '호출 스택이 비어 있는지 확인하는 것' 모두 단일 스레드에서 일어난다는 것이다. 두 작업은 동시에 일어날 수 없으며 한 스레드에서 순차적으로 일어난다.

 

<예시 2> 비동기 함수의 호출 스택

타이머 이벤트 setTimeout은 태스크 큐에 들어갔다가 호출 스택이 비면 그때 실행된다.

 

위와 같은 비동기 함수에는 태스크 큐라는 새로운 개념이 등장한다. 

 

태스크 큐 Task Queue 란?

태스크 큐란 실행해야 할 태스크의 집합을 의미한다.

이벤트 루프는 태스크 큐를 한 개 이상 가지고 있다. 

태스크 큐는 자료 구조의 큐가 아니고 set의 형태를 띠고 있다. 큐 중에서 실행 가능한 가장 오래된 태스크를 가져온다. 실행해야 할 태스크는 비동기 함수의 콜백 함수나 이벤트 핸들러 등이다.

 

 비동기 함수는 누가 수행할까?

비동기 함수는 자바스크립트 코드가 동기식으로 실행되는 메인 스레드에서 수행되지 않는다.

비동기 함수는 태스크 큐가 할당되는 별도의 스레드에서 수행된다. 

별도의 스레드를 관리하는 건 런타임 환경(브라우저나 Node.js 등)의 역할이다. 자바스크립트 코드 실행은 싱글 스레드에서 이루어지지만 외부 Web API 등은 모두 자바스크립트 코드 외부에서 실행되고 나서 콜백이 태스크 큐로 들어가는 것이다. 만약 이러한 작업들도 모두 자바스크립트 코드가 실행되는 메인 스레드에서만 이루어진다면 절대로 비동기 작업을 수행할 수 없을 것이다.

 

교재 오류
타이머 이벤트 함수 setTimeout에 함수 호출 bar()를 넣으면 바로 실행되어 버려서 기대한 결과를 얻을 수 없다. (아래 사진 참조)
bar() 대신에 함수 참조 bar 를 전달해야 비동기 작업이 이루어진다.

 

* 참고용 - 잘못된 결과

setTimeout에 함수 호출 bar()을 넣으니 함수 bar가 바로 실행되어서 foo - bar - baz 순서로 출력된 모습

 

태스크 큐와 마이크로 태스크 큐

마이크로 태스크 큐라는 것도 있다.

 

 마이크로 큐란?

이벤트 루프는 하나의 마이크로 태스크 큐를 가지고 있다. 마이크로 태스크 큐는 태스크 큐와는 다른 태스크를 처리한다. 여기에 들어가는 마이크로 태스크로는 대표적으로 Promise가 있다. 이 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다. 마이크로 태스크 큐가 빌 때까지 기존 태스크 큐의 실행은 뒤로 밀어진다.

 

  • 태스크 큐: setTimeout, setInterval, setImmediate
  • 마이크로 태스크 큐: process.nextTick, Promises, queueMicroTask, MutationObserver

마이크로 태스크 큐에 들어가는 Promise가 태스크 큐에 들어가는 setTimeout보다 우선순위를 가진다.

 

 

 렌더링은 언제 실행될까?

태스크 큐를 실행하기에 앞서 마이크로 태스크 큐를 실행하고, 이 마이크로 태스크 큐를 실행한 뒤에 렌더링이 일어난다. 각 마이크로 태스크 큐 작업이 끝날 때마다 한 번씩 렌더링할 기회를 얻는다.

동기 코드와 마이크로 태스크는 렌더링에 영향을 주므로 특정 렌더링이 자바스크립트 내 무거운 작업과 관련이 있다면 이를 어떤 식으로 분리해서 사용자 경험을 좋게 만들지 고민해볼 필요가 있다.

 

코드별 렌더링 시기

  • 동기 코드는 연산이 모두 끝난 뒤 렌더링 기회를 얻는다. 따라서 한 번에 100,000이라는 숫자가 표시된다.
  • 태스크 큐(setTimeout)은 setTimeout 콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 1부터 100,000까지 순차적으로 렌더링된다.
  • 마이크로 태스크 큐(queueMicroTask)는 렌더링이 전혀 일어나지 않다가 100,000까지 다 끝난 이후에 한 번에 렌더링이 일어난다.
  • 모든 것을 동시에 실행했을 경우 동기 코드와 마이크로 태스크 큐만 한 번에 100,000까지 올라가고, 태스크 큐만 순차적으로 렌더링된다.

 


소감

자바스크립트 언어를 잘 쓰려면 비동기와 이벤트 루프를 잘 이해하고 있어야 한다고 생각한다. 이 둘을 마스터하기까지는 아직 시간이 좀 걸릴 것 같지만 이번 장을 읽으면서 이벤트 루프의 역할과 태스크 큐에 대해 알게 되어 재밌었다. 자바스크립트의 비동기 작업을 가능하게 하는 것이 런타임 환경이라는 점도 새로 배웠다.