logo

Node.js 이벤트 루프

[번역] The Node.js Event Loop

nodejs, javascript, translation


소개

이벤트 루프는 Node.js를 이해하는 데 가장 중요한 측면 중 하나입니다.

왜 그렇게 중요합니까? 이윤 즉, Node.js가 어떻게 비동기적이고 논 블로킹 I/O를 가질 수 있는지를 설명하고, 나아가 기본적으로 Node.js를 성공하게 만든 “킬러 앱”이기 때문입니다.

Node.js JavaScript 코드는 단일 스레드에서 실행되며 한 번에 한 가지 일을 수행합니다.

이는 동시성 문제에 대해 걱정하지 않게끔 프로그래밍 방법을 많이 단순화하기 때문에 실제로 매우 유용한 제한 사항입니다.

여러분은 코드를 작성하는 방법에 주의를 기울이고 동기식 네트워크 호출이나 무한 루프와 같이 스레드를 차단할 수 있는 모든 것만 피하면 됩니다.

일반적으로 대부분 브라우저에는 모든 프로세스를 격리하기 위해 전 브라우저 탭에 이벤트 루프가 있고 무한 루프 또는 전체 브라우저를 차단케 하는 과도한 처리가 있는 웹 페이지를 방지합니다.

이 환경은 여러 개로 동시에 발생하는 이벤트 루프를 관리합니다. (예를 들어 API 호출 처리) 게다가 웹 워커(Web Worker)는 자체 이벤트 루프에서도 실행됩니다.

여러분은 주로 코드가 단일 이벤트 루프에서 실행될 것임을 고려해야 하고, 코드를 블로킹하지 않으려면 이 점을 염두에 두고 코드 작성을 해야 합니다.

이벤트 루프 블로킹 Blocking the event loop

이벤트 루프로 제어 권한을 반환하는 데 너무 오래 걸리는 JavaScript 코드는 페이지에서 실행을 차단하고 UI 스레드도 차단하여 사용자가 클릭하거나 페이지를 스크롤 할 수 없게 합니다.

JavaScript의 거의 모든 I/O 기본 요소(primitives)는 논 블로킹입니다. 이를테면 네트워크 요청, 파일 시스템 작업 등 블로킹 예외이며, 이것이 JavaScript가 많은 콜백, 그리고 최근에는 프로미스(promises) 및 async/await 기반으로 된 이유입니다.

콜 스택 The call stack

콜 스택은 LIFO(후입 선출) 스택입니다.

이벤트 루프는 콜 스택을 지속해서 확인하여 실행해야 하는 함수가 있는지 확인합니다.

이렇게 하는 동안, 이 기능은 (코드에서) 찾은 함수를 콜 스택에 추가하고 각 함수를 순서대로 실행합니다.

디버거 또는 브라우저 콘솔에서 익숙한 에러 스택 트레이스(error stack trace)를 알고 계십니까? 브라우저는 콜 스택에서 함수 이름을 검색하여 현재 호출이 시작된 함수를 알려줍니다:

exception-call-stack

간단한 이벤트 루프 설명

예를 들어 보겠습니다:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  bar()
  baz()
}

foo()

// foo
// bar
// baz

이 코드를 실행시키면 처음엔 foo()를 호출합니다. 그리고 foo() 안의 bar()를 호출하고 다음으론 baz()를 호출합니다.

이 시점에서 콜 스택은 다음과 같습니다:

call-stack-first-example

모든 반복 속에 이벤트 루프는 콜 스택에 무언가가 있는지 확인하고 다음과 같이 실행합니다:

execution-order-first-example

콜 스택이 빌 때까지 계속합니다.

큐 함수 실행 Queuing function execution

위의 예는 평범해 보이며 특별한 점은 없습니다: JavaScript는 실행할 작업을 찾고 순서대로 실행합니다.

스택이 지워질 때까지 함수를 지연시키는 방법을 알아봅시다.

setTimeout(( ) = > {}, 0)의 사용 사례는 함수를 호출하는 것입니다. 하지만 코드의 다른 모든 함수가 실행된 후 실행합니다.

예를 들면 다음과 같습니다:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  baz()
}

foo()

아마 놀랍게도 이 코드는 다음과 같이 프린트됩니다:

foo
baz
bar

이 코드가 실행되면 처음 foo()가 호출됩니다. foo() 내부에서는 먼저 setTimeout를 호출하는데, 인수로 bar를 전달하고 타이머로는 0을 전달하여 즉시 실행하도록 지시합니다. 그 뒤 baz()를 호출합니다.

이 시점에서 콜 스택은 다음과 같습니다:

call-stack-second-example

다음은 프로그램에 있는 모든 함수에 대한 실행 순서입니다.

execution-order-second-example

왜 이런 일이 생깁니까?

메시지 큐 The Message Queue

setTimeout()이 호출되면 브라우저 또는 Node.js는 타이머를 시작합니다. 타이머가 만료되면 (이 경우 0을 제한 시간으로 지정하면) 즉시 메시지 큐에 콜백 함수가 저장됩니다.

또한 메시지 큐는 클릭이나 키보드 이벤트 또는 fetch 응답과 같은 사용자 시작 이벤트(user-initiated events)도 코드가 응답하기 전에 대기하는 곳입니다. onLoad와 같은 DOM 이벤트도 있습니다.

루프는 콜 스택에 우선순위를 부여하고, 먼저 콜 스택에서 찾은 모든 것을 처리하며, 콜 스택에 아무것도 없으면 메시지 큐에 있는 것들을 가져옵니다.

브라우저에서 제공하고 자신의 스레드에 존재하기 때문에 setTimeout, fetch 등과 같은 함수를 기다릴 필요가 없습니다. 예를 들어 setTimeout 시간 초과를 2초로 설정하면 2초 동안 기다릴 필요가 없습니다. - 대기는 다른 곳에서 일어납니다.

ES6 잡 큐 ES6 Job Queue

ECMAScript 2015는 프로미스에서 사용하는 잡 큐의 개념을 도입했습니다. (ES6/ES2015에 도입됨) 이는 콜 스택의 끝에 놓이지 않고 최대한 빨리 비동기 함수의 결과를 실행하는 방법입니다.

현재 함수가 종료되기 전에 해결된 프로미스는 현재 함수 직후에 실행됩니다.

놀이공원에서 롤러코스터를 타는 것과 같은 좋은 비유를 찾았습니다: 메시지 큐는 여러분을 다른 모든 사람 뒤에, 차례를 기다려야 할 큐의 뒤쪽에 둡니다. 반면에 잡 큐는 이전 작업 완료 후 바로 다른 작업에 탑승할 수 있는 패스트패스 티켓입니다.

예제:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise((resolve, reject) =>
    resolve('baz 뒤, bar 바로 앞에 있어야 합니다')
  ).then(resolve => console.log(resolve))
  baz()
}

foo()

// foo
// baz
// baz 뒤, bar 바로 앞에 있어야 합니다
// bar

이는 setTimeout() 또는 다른 플랫폼 API를 통해 프로미스(그리고 프로미스로 만들어진 Async/await)와 일반적인 오래된 비동기 함수간에 큰 차이입니다.

마지막으로, 위의 예제에 대한 콜 스택은 다음과 같습니다:

call-stack-third-example

출처

The Node.js Event Loop