콜백과 이벤트 핸들링의 진화
`Node.js` 개발자들에게 익숙한 콜백은, 과거 `Node`에서 이벤트 처리의 핵심이었습니다. `JavaScript`가 일급 객체를 지원하므로 콜백을 자유롭게 전달할 수 있었죠. 하지만 시간이 흐르면서, 프로미스(Promise)와 `async/await` 구문이 등장하여 비동기 코드 처리 방식이 크게 달라졌습니다.
ES6 이전의 동기와 비동기 코드
`ES6` 이전에는 비동기 및 동기 코드를 혼합해 사용하는 것이 일반적이지 않았습니다. 예를 들어, `fileSize` 함수는 문자열 인수를 받고, 비동기 `fs.stat`을 사용해 파일 크기를 반환합니다. 하지만 동기적 에러 처리(예: 잘못된 인수 타입)와 비동기적 에러 처리가 혼합되어 있어 이해하기 어려운 코드가 되곤 했습니다.
javascriptCopy code
function fileSize(fileName, cb) {
if (typeof fileName !== 'string') {
return cb(new TypeError('argument should be string')); // 동기적
}
fs.stat(fileName, (err, stats) => {
if (err) { return cb(err); } // 비동기적
cb(null, stats.size); // 비동기적
});
}
프로미스의 도입과 그 장점
`Node.js`의 이벤트 아키텍처에서 프로미스의 도입은 비동기 프로그래밍 방식에 혁신을 가져왔습니다. `ES6` 이후 `JavaScript`에서는 프로미스(Promise)를 기본적으로 제공하게 되었는데, 이는 콜백 방식의 복잡성과 "콜백 지옥" 문제를 해결하는 데 큰 도움이 되었습니다.
`readFileAsArray` 함수를 예로 들어보면, 프로미스를 사용하면서 코드의 가독성과 유지 보수성이 크게 향상되었습니다. 이 함수는 파일을 읽어 각 줄을 배열로 반환하는 기능을 하는데, 프로미스를 사용하면 비동기 처리의 성공(then)과 실패(catch)를 명확하게 구분할 수 있습니다.
javascriptCopy code
readFileAsArray('./numbers.txt')
.then(lines => {
const numbers = lines.map(Number);
const oddNumbers = numbers.filter(n => n % 2 === 1);
console.log('Odd numbers count:', oddNumbers.length);
})
.catch(console.error);
이처럼 프로미스를 사용하면 에러 처리가 쉬워지고, 비동기 코드의 흐름이 더 명확해집니다. `then` 메소드는 성공적인 비동기 연산의 결과를 다루고, `catch` 메소드는 에러를 처리합니다.
Async/Await의 등장
프로미스는 많은 문제를 해결했지만, 여전히 `.then`과 `.catch` 체이닝이 코드를 다소 장황하게 만들 수 있습니다. 이에 대한 해결책으로 `async/await` 문법이 도입되었습니다. 이 문법을 사용하면 비동기 코드를 동기 코드처럼 간결하고 명료하게 작성할 수 있습니다.
예를 들어, `countOdd` 함수는 파일에서 홀수의 개수를 세는 함수입니다. `async/await`를 사용하면 다음과 같이 코드를 작성할 수 있습니다.
javascriptCopy code
async function countOdd() {
try {
const lines = await readFileAsArray('./numbers');
const numbers = lines.map(Number);
const oddCount = numbers.filter(n => n % 2 === 1).length;
console.log('Odd numbers count:', oddCount);
} catch(err) {
console.error(err);
}
}
countOdd();
여기서 `await` 키워드는 프로미스의 완료를 기다리며, 비동기 함수가 마치 동기 함수처럼 작동하도록 합니다. `try/catch` 블록은 에러를 잡아내고 처리합니다.
이벤트 에미터(EventEmitter)의 중요성과 활용
`Node.js`에서 이벤트 에미터는 객체 간의 통신과 이벤트 관리에 핵심적인 역할을 합니다. 이벤트 에미터는 `Node.js`의 비동기 이벤트 기반 아키텍처의 중심이며, 이는 `Node.js`가 효율적인 이벤트 처리를 가능하게 하는 주요 요소입니다.
이벤트 에미터의 기본 개념
`Node.js`의 모든 이벤트 기반 아키텍처는 `EventEmitter` 클래스를 기반으로 합니다. 이 클래스를 사용하여, 개발자는 커스텀 이벤트를 생성하고, 이에 대한 리스너를 등록할 수 있습니다. 이벤트 에미터의 기본적인 사용법은 다음과 같습니다:
클래스 상속: `EventEmitter`를 상속받아 새로운 클래스를 만듭니다. 이를 통해 해당 클래스의 인스턴스는 이벤트를 발생시키고 감지할 수 있는 능력을 갖춥니다.\
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
이벤트 리스너 등록: .`on()` 메소드를 사용하여 특정 이벤트에 대한 리스너(콜백 함수)를 등록합니다. 이벤트가 발생하면 이 리스너가 호출됩니다.
myEmitter.on('event', () => {
console.log('An event occurred!');
});
이벤트 발생: .emit() 메소드를 사용하여 이벤트를 발생시킵니다. 이 때 등록된 리스너들이 실행됩니다.
myEmitter.emit('event');
이벤트 에미터의 사용법
이벤트 에미터는 단순한 이벤트 발생과 리스닝 외에도 다양한 고급 기능을 제공합니다:
- 동기 vs 비동기: 이벤트 에미터는 동기적으로도, 비동기적으로도 작동할 수 있습니다. 이벤트 리스너 내에서 비동기 작업을 수행할 수 있으며, 이는 이벤트 처리의 유연성을 높여줍니다.
- 에러 처리: 에러 이벤트('error')는 특별히 처리되며, 리스너가 등록되지 않은 에러 이벤트는 예외를 발생시킵니다. 이를 통해 예외 상황을 효과적으로 관리할 수 있습니다.
myEmitter.on('error', (err) => {
console.error('An error occurred:', err);
});
- 한 번만 실행되는 이벤트 리스너: `.once()` 메소드를 사용하여 이벤트 리스너를 한 번만 실행되도록 설정할 수 있습니다. 이는 일회성 이벤트 처리에 유용합니다.
myEmitter.once('event', () => {
console.log('This will only happen once.');
});
리스너의 추가와 제거: `.addListener()` 또는 `.on()`으로 리스너를 추가하고, `.removeListener()` 또는 `.off()`로 리스너를 제거할 수 있습니다. 이를 통해 동적으로 이벤트 리스너를 관리할 수 있습니다.
작업 실행 모니터링 클래스 예시
`WithLog` 클래스는 `EventEmitter`를 상속받아, 특정 작업의 시작과 끝에 이벤트를 발생시키는 기능을 구현합니다. 이 클래스를 통해 작업 실행 전후의 상태를 로깅할 수 있습니다.
const EventEmitter = require('events');
class WithLog extends EventEmitter {
execute(taskFunc) {
console.log('Before executing');
this.emit('begin');
taskFunc();
this.emit('end');
console.log('After executing');
}
}
const withLog = new WithLog();
withLog.on('begin', () => console.log('About to execute'));
withLog.on('end', () => console.log('Done with execute'));
withLog.execute(() => console.log('*** Executing task ***'));
이 코드에서 `WithLog` 클래스는 `execute` 메소드를 통해 제공된 함수를 실행하기 전과 후에 `begin`과 `end` 이벤트를 발생시킵니다. 이벤트 리스너는 해당 이벤트가 발생할 때마다 콘솔에 로그를 출력합니다.
비동기 작업과 이벤트 에미터
이벤트 에미터는 비동기 작업을 모니터링하는 데에도 사용할 수 있습니다. 예를 들어, 파일 시스템의 비동기 작업을 추적하는 `WithTime` 클래스를 구현할 수 있습니다.
const fs = require('fs');
class WithTime extends EventEmitter {
execute(asyncFunc, ...args) {
console.time('execute');
this.emit('begin');
asyncFunc(...args, (err, data) => {
if (err) {
return this.emit('error', err);
}
this.emit('data', data);
console.timeEnd('execute');
this.emit('end');
});
}
}
const withTime = new WithTime();
withTime.on('begin', () => console.log('About to execute'));
withTime.on('data', (data) => console.log('Data:', data));
withTime.on('end', () => console.log('Done with execute'));
withTime.execute(fs.readFile, __filename);
이 코드에서 `WithTime` 클래스는 `execute` 메소드를 통해 파일 읽기와 같은 비동기 함수를 실행하고, 시작(begin), 데이터 수신(data), 완료(end), 에러(error) 등의 다양한 이벤트를 발생시킵니다. 각 이벤트에 따라 적절한 로깅 또는 에러 처리를 수행할 수 있습니다.
에러 이벤트의 처리
`WithTime` 클래스의 예를 들어보겠습니다. 이 클래스는 비동기 함수를 실행하고, 이 과정에서 발생하는 이벤트를 처리합니다.
class WithTime extends EventEmitter {
execute(asyncFunc, ...args) {
console.time('execute');
asyncFunc(...args, (err, data) => {
if (err) {
return this.emit('error', err); // 에러 이벤트 발생
}
console.timeEnd('execute');
});
}
}
const withTime = new WithTime();
// 잘못된 파일 경로로 인한 에러 발생
withTime.execute(fs.readFile, ''); // BAD CALL
위 코드에서 잘못된 파일 경로를 제공하면 `fs.readFile`은 에러를 반환합니다. 이 경우, `WithTime` 클래스는 에러 이벤트('error')를 발생시킵니다. 이 이벤트가 처리되지 않으면 `Node.js`는 에러를 발생시키고 프로그램이 종료됩니다.
에러 리스너의 등록
에러를 적절히 처리하기 위해서는 에러 이벤트에 대한 리스너를 등록해야 합니다.
withTime.on('error', (err) => {
console.error('An error occurred:', err);
});
이 리스너는 `error` 이벤트가 발생했을 때 실행되며, 에러 정보를 로깅합니다. 이를 통해 애플리케이션이 비정상적으로 종료되는 것을 방지할 수 있습니다.
uncaughtException 리스너의 사용
모든 에러를 캐치하지 못할 경우, `process` 객체의 `uncaughtException` 이벤트 리스너를 사용하여 처리할 수 있습니다.
process.on('uncaughtException', (err) => {
console.error('Something went unhandled:', err);
process.exit(1); // 강제 종료
});
이 리스너는 처리되지 않은 예외가 발생할 때 실행되며, 로그를 남기고 프로세스를 종료합니다. 이는 마지막 수단으로 사용되어야 하며, 정상적인 에러 처리 흐름을 대체하는 용도로 사용되어서는 안 됩니다.
리스너의 우선 순위와 관리
이벤트 에미터에서는 리스너의 등록 순서에 따라 실행 순서가 결정됩니다. 만약 특정 리스너를 먼저 실행하고 싶다면, `prependListener` 메소드를 사용할 수 있습니다.
withTime.prependListener('data', (data) => {
console.log(`Characters: ${data.toString().length}`);
});
withTime.on('data', (data) => {
console.log(`Length: ${data.length}`);
});
'Backend > Node.js' 카테고리의 다른 글
Node.js child_process (1) | 2024.01.08 |
---|---|
Node.JS) Module Caching 을 알아보자 (0) | 2023.09.14 |
Node.js TDD 단위테스트, 통합테스트 (0) | 2023.07.14 |
Node.js 작동방식 및 Event loop (0) | 2023.06.26 |
Node.js (0) | 2023.06.26 |