본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.5.2 이벤트 루프란?

by Whiimsy 2024. 5. 14.

1.5.2 이벤트 루프란?

아래 내용은 자바스크립트 런타임 중 가장 유명한 V8 기준으로 작성됨. 이벤트 루프는 ECMAScript, 즉 자바스크립트 표준에 나와 있는 내용은 아님. 즉, 이벤트 루프란 자바스크립트 런타임 외부에서 자바스크립트의 비동기 실행을 돕기 위해 만들어진 장치. V8, Spider Monkey 같은 현대 자바스크립트 런타임 엔진에는 자바스크립트 코드를 효과적으로 실행하기 위한 여러 가지 장치들이 마련돼 있음.

호출 스택과 이벤트 루프

호출 스택(call stack)은 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택.

function bar() {
  console.log('bar')
}

function baz() {
  console.log('baz')
}

function foo() {
  console.log('foo')
  bar()
  baz()
}

foo()

 

이 코드는 foo를 호출하고, 내부에서 bar, baz를 순차적으로 호출하는 구조로 돼 있음.

  1. foo()가 호출 스택에 먼저 들어감.
  2. foo() 내부 console.log 호출 스택에 들어감.
  3. 2의 실행이 완료된 후 다음 코드로 넘어감. (아직 foo()는 존재)
  4. bar()가 호출 스택에 들어감.
  5. bar() 내부 console.log 호출 스택에 들어감.
  6. 5의 실행이 완료된 후에 다음 코드로 넘어감. (아직 foo(), bar()는 존재)
  7. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거됨. (아직 foo()는 존재)
  8. baz()가 호출 스택에 들어감.
  9. baz() 내부 console.log 호출 스택에 들어감.
  10. 9의 실행이 완료된 후에 다음 코드로 넘어감. (아직 foo(), baz()는 존재)
  11. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거됨. (아직 foo()는 존재)
  12. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거됨.
  13. 호출 스택 비워짐.

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

그렇다면 비동기 작업은?

function bar() {
  console.log('bar')
}

function baz() {
  console.log('baz')
}

function foo() {
  console.log('foo')
  setTimeout(bar(), 0) // setTimeout 추가
  baz()
}
  1. foo()가 호출 스택에 들어감.
  2. foo() 내부 console.log 호출 스택에 들어감.
  3. 2의 실행 완료 후 다음 코드로 넘어감. (아직 foo()는 존재)
  4. setTimeout(bar(), 0)이 호출 스택에 들어감.
  5. 4번에 대해 타이머 이벤트가 실행되며 태스크 큐로 들어가고, 그 대신 바로 스택에서 제거됨.
  6. baz()가 호출 스택에 들어감.
  7. baz() 내부 console.log 호출 스택에 들어감.
  8. 7의 실행 완료 후 다음 코드로 넘어감. (아직 foo(), baz()는 존재)
  9. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거됨. (아직 foo()는 존재)
  10. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거됨.
  11. 호출 스택 비워짐.
  12. 이벤트 루프가 호출 스택이 비워져 있다는 것 확인. 태스크 큐 확인하니 4번에 들어갔던 내용이 있어 bar()를 호출 스택에 들여보냄.
  13. bar() 내부 console.log 호출 스택에 들어감.
  14. 13 실행 후 다음 코드로 넘어감. (아직 bar() 존재)
  15. 더 이상 bar()에 남은 것 없으므로 호출 스택에서 제거됨.

위 코드로, setTimeout(() => {}), 0)이 정확하게 0초 뒤 실행된다는 것 보장하지 못한다는 것 이해 가능.

 

태스크 큐란 실행해야 할 태스크의 집합을 의미. 이벤트 루프는 이러한 태스크 큐를 한 개 이상 가지고 있음. 이름과는 다르게 태스크 큐는 자료 구조의 큐(queue)가 아니고 set 형태를 띄고 있음. 그 이유는 선택된 큐 중에서 실행 가능한 가장 오래된 태스크를 가져와야 하기 때문. 자료구조인 큐는 무조건 앞에 있는 것을 FIFO(First In First Out) 형식으로 꺼내와야 하지만 태스크 큐는 그렇지 않음. 태스크 큐에서 의미하는 '실행해야 할 태스크'라는 것은 비동기 함수의 콜백 함수나 이벤트 핸들러 등을 의미.

 

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

 

그럼 비동기 함수는 누가 수행하느냐. n초 뒤에 setTimeout을 요청하는 작업은 누가 처리할까. fetch를 기반으로 실행되는 네트워크 요청은 누가 보내고 응답을 받을 것인가. 이러한 작업들은 모두 자바스크립트 코드가 동기식으로 실행되는 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 수행됨. 이 별도의 스레드에서 태스크 큐에 작업을 할당해 처리하는 것은 브라우저나 Node.js의 역할. 즉, 자바스크립트 코드 실행은 싱글 스레드에서 이루어지지만 이러한 외부 Web API 등은 모두 자바스크립트 코드 외부에서 실행되고 콜백이 태스크 큐로 들어가는 것. 이벤트 루프는 호출 스택이 비고, 콜백이 실행 가능한 때가 오면 이것을 꺼내서 수행하는 역할. 만약 이러한 작업들도 모두 자바스크립트 코드가 실행되는 메인 스레드에서만 이루어진다면 절대로 비동기 작업을 수행할 수 없음.