본문 바로가기

카테고리 없음

모던 리액트 딥 다이브 1.5 이벤트 루프와 비동기 통신의 이해

1.5 이벤트 루프와 비동기 통신의 이해

 

싱글 스레드 자바스크립트

자바스크립트는 싱글 스레드에서 작동한다.

 

즉, 기본적으로 자바스크립트는 코드를 한 줄 한 줄 실행하며, 한 번에 하나의 작업만 동기 방식으로만 처리할 수 있다.

그러나 이러한 싱글 스레드 기반의 JS에서도 많은 양의 비동기 작업이 이루어지고 있다.

 

이러한 비동기 작업이 어떻게 처리되는지 이해하고 비동기 처리를 도와주는 이벤트 루프를 비롯한 다양한 개념에 대해 알아보자.

 

동기(synchronous)

직렬 방식으로 작업을 처리하는 것을 의미한다. 이 요청이 시작된 이후에는 무조건 응답을 받은 이후에야 비로소 다른 작업을 처리할 수 있고, 그동안 다른 모든 작업은 대기한다.

 

비동기(asynchronous)

병렬 방식으로 작업을 처리하는 것을 의미한다. 요청을 시작한 후 이 응답이 오건 말건 상관없이 다음 작업이 이루어지며, 따라서 한 번에 여러 작업이 실행될 수 있다.


이벤트 루프

  • 위 코드의 출력 순서는 1, 4, 2, 3이다.
  • 동기식으로 작동하는 JS에서 어떻게 이런 비동기 코드를 처리할 수 있는 것인지 이해하려면 이벤트 루프라는 개념을 이해해야 한다.

호출 스택(Call Stack)과 이벤트 루프

  • 이 코드는 foo를 호출하고, 내부에서 bar, baz를 순차적으로 호출하는 구조로 돼 있다. 이 코드들은 대략 다음과 같은 순서로 호출 스택에 쌓이고 비워지게 된다.

  • 호출 스택이 비어 있는지 여부를 확인하는 것이 바로 이벤트 루프다.
  • 이벤트 루프는 단순히 이벤트 루프만의 단일 스레드 내부에서 이 호출 스택 내부에 수행해야 할 작업이 있는지 확인하고, 수행해야 할 코드가 있다면 JS 엔진을 이용해 실행한다.
  • 알아둘 점은 ‘코드를 실행하는 것’과 ‘호출 스택이 비어 있는지 확인하는 것’ 모두가 단일 스레드에서 일어난다는 점이다. 즉 두 작업은 동시에 일어날 수 없으며 한 스레드에서 순차적으로 일어난다.

 

그렇다면 비동기 작업은 어떻게 실행될까?

 

태스크 큐

 

  • 위 코드를 보면, foo, baz, bar 순으로 출력된다. setTimeout이 정확하게 0초 뒤에 실행된다는 것을 보장하지 못한다.
  • 여기서부터는 태스크 큐라는 새로운 개념이 등장하는데, 태스크 큐란 실행해야 할 태스크의 집합을 의미한다.
  • 이벤트 루프는 이러한 태스크 큐를 한 개 이상 가지고 있다.
  • 여기서 ‘실행해야 할 태스크’라는 것은 비동기 함수의 콜백 함수나 이벤트 핸들러 등을 의미한다.
  • 호출 스택 내부에서는 다음과 같은 일이 발생한다.

정리하자면

  • 이벤트 루프의 역할은 호출 스택에 실행 중인 코드가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 역할을 한다.
  • 호출 스택이 비었다면 태스크 큐에 대기 중인 작업이 있는지 확인하고, 이 작업을 실행 가능한 오래된 것부터 순차적으로 꺼내와서 실행하게 된다.
  • 이 작업 또한 마찬가지로 태스크 큐가 빌 때까지 이루어진다.

 

마지막으로 궁금해지는 것은 저 비동기 함수는 누가 수행하느냐다.

n초 뒤에 setTimeout을 요청하는 작업은 누가 처리할까?

fetch를 기반으로 실행되는 네트워크 요청은 누가 보내고 응답을 받을 것인가?

 

  • 이러한 작업들은 모두 JS 코드가 동기식으로 실행되는 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 수행된다.
  • 이 별도의 스레드에서 태스크 큐에 작업을 할당해 처리하는 것은 브라우저나 Node.js의 역할이다.
    • 즉, JS 코드 실행은 싱글 스레드에서 이루어지지만 이러한 외부 Web API 등은 모두 JS 외부에서 실행되고 콜백이 태스크 큐로 들어가는 것이다.
  • 만약 이러한 작업들도 모두 JS 코드가 실행되는 메인 스레드에서만 이루어진다면 절대로 비동기 작업을 수행할 수 없을 것이다.

 

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

 

태스크 큐와 별개로 마이크로 태스크 큐가 있다. 이벤트 루프는 하나의 마이크로 태스크 큐를 갖고 있는데, 기존의 태스크 큐와는 다른 태스크를 처리한다.

  • 대표적으로 Promise가 있다. 이 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다.즉, setTimeout과 setinterval은 Promise보다 늦게 실행된다.
  • 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미루어진다.

 

  • 코드를 실행하면 bar, baz, too 순으로 실행된다. 확실히 Promise가 우선권이 있음을 알 수 있다.

렌더링이 실행되는 시점

 

  • 태스크 큐를 실행하기 에 앞서 먼저 마이크로 태스크 큐를 실행하고, 이 마이크로 태스크 큐를 실행한 뒤에 렌더링이 일어난다.
  • 각 마이크로 태스크 큐 작업이 끝날 때마다 한 번씩 렌더링할 기회를 얻게 된다.
  • 위 예제 코드의 결과를 정리하면 다음과 같다.


이벤트 루프

  • 비동기 작업을 관리하고 실행 흐름을 조율하는 역할을 하는 것
  • 호출 스택에 실행중인 코드가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하고 수행해야 할 코드가 있다면 자바스크립트 엔진을 이용해 실행한다.

콜 스택

  • 수행해야 할 코드나 함수를 순차적으로 담아두는 스택.

태스크 큐

  • 실행해야 할 태스크의 집합.
  • 자료구조 큐가 아닌 set 형태, 비동기 함수의 콜백 함수나 이벤트 핸들러가 포함된다.
  • Ex. setTimeout, setInterval

마이크로 태스크 큐

  • 기존 태스크 큐보다 우선권을 가진다. 마이크로 태스크 큐가 빌때까지는 태스크 큐의 실행은 미뤄진다.
  • 마이크로 태스크 큐를 실행한 뒤에 렌더링이 일어난다.
  • Ex. Promise

Web API

  • 브라우저에서 제공하는 API. 메인 스레드가 아닌 별도에 환경에서 실행된다.
  • 자바스크립트 코드 외부에서 실행되며 비동기 작업이 완료되면, 콜백을 태스크 큐, 마이크로 태스크 큐에 추가한다.
  • Ex. setTimeout, DOM 이벤트, fetch

예시 코드와 이벤트 루프 흐름

console.log("Start");

setTimeout(() => {
  console.log("Timeout callback");
}, 1000);

console.log("End");
  • console.log("Start")가 콜 스택에 push됨. 즉시 실행되어 “Start” 출력. 콜 스택에서 제거.
  • setTimeout이 콜 스택에 push됨. 자바스크립트 엔진(콜 스택)은 setTimeout을 Web API로 넘김.
    • Web API에서 타이머를 실행.
  • console.log("End")가 콜 스택에 push됨. 즉시 실행되어 "End" 출력. 콜 스택에서 제거.
  • setTimeout 타이머 종료 후, 콜백 함수가 태스크 큐로 이동
  • 이미 "End"까지 실행되어 콜 스택이 비어 있으므로, 이벤트 루프가 태스크 큐에서 콜백 함수를 가져와 실행
  • "Timeout callback" 출력

정리하자면 JS 코드를 실행하는 것 자체는 싱글 스레드로 이루어져서 비동기를 처리하기 어렵지만

자바스크립트 코드를 실행하는 것 이외에 태스크 큐, 이벤트 루프, 마이크로 태스크 큐, 브라우저/Node.js API 등이

적절한 생태계를 이루고 있기 때문에 싱글 스레드로는 불가능한 비동기 이벤트 처리가 가능해졌다.