Node.js 이벤트 루프 완벽 가이드: 비동기 작업 이해하기

작성일 :

Node.js 이벤트 루프 완벽 가이드: 비동기 작업 이해하기

Node.js는 서버 측 자바스크립트 환경으로, 비동기 I/O 및 이벤트 기반 아키텍처 덕분에 높은 성능을 자랑합니다. 이러한 비동기성이 Node.js의 핵심이며, 이를 실현하는 중심에 '이벤트 루프'가 있습니다. 이번 글에서는 이벤트 루프의 작동 방식과 비동기 작업 처리 메커니즘을 깊이 있게 살펴보도록 하겠습니다.

이벤트 루프란 무엇인가?

이벤트 루프는 Node.js가 비동기 작업을 처리할 수 있게 해주는 내부 메커니즘입니다. Node.js는 단일 스레드로 구동되지만, 이벤트 루프를 통해 동시에 여러 작업을 처리할 수 있습니다. 이벤트 루프는 기본적으로 아래의 단계로 작동합니다.

  1. 테스크 큐 확인: 먼저 대기 중인 테스크들이 있는지 확인합니다.
  2. 콜백 실행: 테스크 큐에서 콜백 함수를 가져와 실행합니다.
  3. I/O 작업 확인: 네트워크 I/O, 파일 시스템 I/O 등의 비동기 요청 상태를 확인합니다.
  4. 타이머 확인: setTimeout이나 setInterval 등의 타이머 작업을 확인하고 실행합니다.
  5. 다음 큐 대기: 모든 작업이 완료되면, 새로운 작업이 들어올 때까지 대기합니다.

이러한 반복적인 과정을 통해 Node.js는 자바스크립트 코드의 비동기 작업을 효율적으로 처리합니다.

비동기 작업 처리 방식

Node.js에서 비동기 작업을 처리하는 방법에는 여러 가지가 있습니다. 대표적인 방법들은 다음과 같습니다.

콜백 함수

Node.js의 초기 비동기 처리 방식은 주로 콜백 함수입니다. 콜백 함수는 특정 작업이 완료되었을 때 호출되는 함수를 말합니다. 예를 들어 파일을 읽는 비동기 작업을 살펴보겠습니다.

javascript
const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

위 예제에서 fs.readFile 함수는 파일을 비동기로 읽고, 파일이 읽혀지면 마지막 인자로 전달된 콜백 함수가 호출됩니다. 이와 같이 콜백 함수는 Node.js 비동기 작업 처리의 기본 방법입니다.

프로미스(Promise)

콜백 함수의 단점인 콜백 헬을 해결하기 위해 ES6에서 도입된 프로미스는 좀 더 직관적으로 비동기 작업을 처리할 수 있습니다. 다음은 프로미스를 사용한 예제입니다.

javascript
const fs = require('fs').promises;

fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log(data);
  })
  .catch(err => {
    console.error(err);
  });

프로미스 객체는 비동기 작업의 결과를 '약속'하는 객체로, thencatch를 통해 성공과 실패를 처리할 수 있습니다. 이를 통해 비동기 코드를 보다 가독성 있게 작성할 수 있습니다.

async/await

ES8에서는 비동기 작업을 동기 작업처럼 작성할 수 있게 해주는 async/await 구문이 도입되었습니다. 다음은 같은 예제를 async/await를 이용해 작성한 코드입니다.

javascript
const fs = require('fs').promises;

async function readFile() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readFile();

async 함수는 항상 프로미스를 반환하며, await 키워드는 프로미스의 완료를 기다립니다. 이를 통해 비동기 작업을 마치 동기 작업처럼 다룰 수 있어 코드의 가독성과 유지보수성을 높여줍니다.

이벤트 루프의 단계별 진행

이벤트 루프는 다양한 비동기 작업들을 어떻게 처리하는지 단계별로 이해하는 것이 중요합니다. 아래는 이벤트 루프의 주요 단계입니다.

타이머 단계(Timers Phase)

이 단계에서는 setTimeoutsetInterval로 예약된 콜백 함수들이 실행됩니다. 타이머가 만료된 콜백이 이 큐에 쌓이며, 이벤트 루프는 이 콜백들을 순차적으로 실행합니다.

I/O 콜백 단계(I/O Callbacks Phase)

여기서는 대부분의 비동기 I/O 콜백이 실행됩니다. 네트워크 요청, 파일 시스템 작업 등이 이 단계에서 처리됩니다. 예를 들어 파일이 성공적으로 읽혀졌을 때 호출되는 콜백 함수가 여기서 실행됩니다.

대기 단계(Idling, Prepare Phase)

이 단계는 내부적으로 Node.js가 처리하는 작업으로, 대부분의 사용자 코드는 여기서 실행되지 않습니다.

폴링 단계(Poll Phase)

폴링 단계에서는 새로운 I/O 이벤트를 대기하거나, 폴링 큐에 있는 이벤트들을 처리합니다. 이 단계가 완료되지 않으면 타이머 단계로 넘어가지 않습니다.

체크 단계(Check Phase)

setImmediate로 예약된 콜백 함수들이 이 단계에서 실행됩니다. setImmediate는 이벤트 루프의 '현재' 단계 이후 실행되도록 예약됩니다.

닫기 단계(Close Callbacks Phase)

close 이벤트가 발생했을 때 호출되는 콜백 함수가 여기서 실행됩니다. 예를 들어, socket.on('close', ...)와 같은 콜백이 이 단계에서 처리됩니다.

결론

Node.js의 이벤트 루프와 비동기 처리 메커니즘은 단일 스레드 환경에서 고성능 서버를 구현할 수 있게 해주는 중요한 요소입니다. 콜백 함수, 프로미스, 그리고 async/await와 같은 다양한 비동기 작업 처리 방법을 통해 코드의 가독성과 유지보수성을 높일 수 있습니다. 이러한 기법들을 잘 이해하고 활용하면 보다 효율적이고 강력한 Node.js 애플리케이션을 작성할 수 있을 것입니다.