Node.js - 기본 개념 (4): 이벤트, 예외처리
소스는 assu10/nodejs.git 에 있습니다.
- 이벤트(중요)
- 예외 처리
- 자주 발생하는 에러들
1. 이벤트 (중요)
Node.js - 파일시스템 에서 스트림에 대해 볼 때 on('data', callback)
과 같은 코드를 사용하였다.
on('data', callback)
는 data, end 라는 이벤트가 발생할 때 콜백 함수를 호출하도록 이벤트를 등록한 것이다.
readStream.on('data', chunk => {
data.push(chunk);
console.log('data: ', chunk, chunk.length);
});
createReadStream
의 경우 내부적으로 알아서 data
, end
이벤트를 호출하지만 직접 이벤트를 만들 수도 있다.
event.js
const EventEmitter = require('events');
const myEvent = new EventEmitter();
myEvent.addListener('event1', () => {
console.log('이벤트1');
});
// event2 에 여러 개의 이벤트 리스너 등록
myEvent.on('event2', () => {
console.log('이벤트2');
});
myEvent.on('event2', () => {
console.log('이벤트2 추가');
});
// 한 번만 실행됨
myEvent.once('event3', () => {
console.log('이벤트3');
});
myEvent.emit('event1'); // 이벤트 호출
myEvent.emit('event2');
myEvent.emit('event3');
myEvent.emit('event3'); // 실행 안 됨
myEvent.on('event4', () => {
console.log('이벤트4');
});
myEvent.removeAllListeners('event4');
myEvent.emit('event4'); // 실행 안 됨
const listener = () => {
console.log('event5');
};
myEvent.on('event5', listener);
myEvent.removeListener('event5', listener);
myEvent.emit('event5'); // 실행 안 됨
console.log(myEvent.listenerCount('event2'));
이벤트1
이벤트2
이벤트2 추가
이벤트3
2
on(이벤트명, 콜백)
- 이벤트명과 이벤트 발생 시의 콜백을 연결하는데 이러한 동작을 이벤트 리스닝이라고 함
- event2 처럼 이벤트 하나에 여러 개의 리스너를 연결할 수도 있음
addListener(이벤트명, 콜백)
- on 과 같은 기능
emit(이벤트명)
- 이벤트 호출
- 이벤트명을 인수로 넣으면 미리 등록해두었던 콜백이 실행됨
once(이벤트명, 콜백)
- 한 번만 실행되는 이벤트
removeAllListeners(이벤트명)
- 이벤트에 연결된 모든 이벤트 리스너 제거
removeListener(이벤트명, 리스너)
- 이벤트에 연결된 리스터를 하나씩 제거
off(이벤트명, 콜백)
- 노드 10 에서 추가된 메서드로 removeListener 와 같은 기능
listenerCount(이벤트명)
- 등록된 리스터의 개수 조회
on('data')
도 겉으론 이벤트를 호출하지 않지만 내부적으로 chunk 를 전달할 때마다 data 이벤트를 emit 하고 있고, 완료되었을 경우 end 이벤트를 emit 한 것이다.
직접 이벤트를 만들 수 있어서 다양한 동작을 구현할 수 있으므로 실무에서 많이 사용된다.
2. 예외 처리
멀티 스레드 프로그램에서는 스레드 하나가 멈추면 그 일은 다른 스레드가 대신 하지만 노드의 메인 스레드는 하나뿐이므로 메인 스레드가 에러로 인해 멈추면 스레드를 갖고 있는 프로세스가 멈춘다는 뜻이고, 전체 서버도 멈춘다는 뜻이다.
에러가 발생하면 에러 로그가 기록되더라도 작업은 계속 진행될 수 있어야 한다.
아래는 try/catch
로 에러를 처리하는 예시이다.
error1.js
setInterval(() => {
console.log('START');
try {
throw new Error('ERROR');
} catch (err) {
console.error(err);
}
}, 1000);
START
Error: ERROR
at Timeout._onTimeout (/Users/assu/Developer/01_nodejs/mynode/chap03-event/src/2-error1.js:4:11)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
START
Error: ERROR
at Timeout._onTimeout (/Users/assu/Developer/01_nodejs/mynode/chap03-event/src/2-error1.js:4:11)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
// 계속 반복
아래는 노드 자체에서 잡아주는 에러 처리이다.
error2.js
const fs = require('fs');
setInterval(() => {
fs.unlink('./ddd.js', err => {
if (err) {
console.error(err);
}
});
}, 1000);
[Error: ENOENT: no such file or directory, unlink './ddd.js'] {
errno: -2,
code: 'ENOENT',
syscall: 'unlink',
path: './ddd.js'
}
[Error: ENOENT: no such file or directory, unlink './ddd.js'] {
errno: -2,
code: 'ENOENT',
syscall: 'unlink',
path: './ddd.js'
}
// 계속 반복
아래는 프로미스의 에러이다.
error3.js
const fs = require('fs').promises;
setInterval(() => {
fs.unlink('./ddd.js');
}, 1000);
node:internal/process/promises:246
triggerUncaughtException(err, true /* fromPromise */);
^
[Error: ENOENT: no such file or directory, unlink './ddd.js'] {
errno: -2,
code: 'ENOENT',
syscall: 'unlink',
path: './ddd.js'
}
// 프로세스 멈춤
아래는 예측이 불가능한 에러 처리 방법이다.
error4.js
process.on('uncaughtException', err => {
console.error('예측치 못한 에러', err);
});
setInterval(() => {
throw new Error('ERROR');
}, 1000);
setTimeout(() => {
console.log('실행됨');
}, 2000);
예측치 못한 에러 Error: ERROR
at Timeout._onTimeout (/Users/assu/Developer/01_nodejs/mynode/chap03-event/src/2-error4.js:6:9)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
실행됨
예측치 못한 에러 Error: ERROR
at Timeout._onTimeout (/Users/assu/Developer/01_nodejs/mynode/chap03-event/src/2-error4.js:6:9)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
예측치 못한 에러 Error: ERROR
at Timeout._onTimeout (/Users/assu/Developer/01_nodejs/mynode/chap03-event/src/2-error4.js:6:9)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
// 계속 반복
언뜻보면 uncaughtException
이벤트 리스너가 모든 에러를 처리할 수 있을 것처럼 보이지만 노드 공식 문서에서는 uncaughtException
이벤트를 최후의 수단으로 사용할 것을 명시하고 있다.
노드는 uncaughtException
이벤트 발생 수 다음 동작이 제대로 동작하는지를 보증하지 않기 때문에 복구 작업 코드를 넣었더라도 그것이 동작하는지 확신할 수 없다.
따라서 uncaughtException
는 단순히 에러 내용을 기록하는 정도로 사용하고, 에러를 기록한 후 process.exit() 로 프로세스를 종료하는 것이 좋다.
3. 자주 발생하는 에러들
node: command not found
- 노드를 설치했지만 이 에러가 발생하면 환경 변수가 제대로 설정되지 않은 것
ReferenceError: 모듈 is not defined
- 모듈을 require 했는지 확인
Error: Cannot find module 모듈명
- 해당 모듈을 require 했지만 설치는 하지 않은 상태, npm i 로 설치 필요
Error: Can't set headers after they are sent
- 요청에 대한 응답을 보낼 때 응답을 두 번 이상 보낸 경우, 요청에 대한 응답은 한 번만 보내야 함
FATAL ERROR: CALL_END_REPLY_LAST Allocation failed - JavaScript heap out of memory
- 코드 실행 시 메모리가 부족하여 스크립트가 정상 작동하지 않은 경우
- 코드가 잘못되었을 확률이 높으므로 코드 점검 필요
- 코드가 정상이라면 노드 실행 시
node --max-old-space-size=4096 파일명
으로 노드 메모리 늘려서 해결 (4096 은 4GB)
UnhandledPromiseRejectionWarning: Unhandled promise rejection
- 프로미스 사용 시 catch 메서드 붙이지 않은 경우 발생
EACCESS 혹은 EPERM
- 노드가 작업을 수행하는데 권한이 충분하지 않음
- 파일/폴더 수정/삭제/생성 권한 확인 필요
- 맥/리눅스면 명령어 앞에 sudo 붙이는 것도 방법
ECONNREFUSED
- 요청을 보냈으나 연결이 성립하지 않은 경우
- 요청을 받는 서버의 주소가 올바른지, 서버가 내려가있지는 않은지 확인 필요
ETARGET
- package.json 에 기록한 패키지 버전이 존재하지 않을 때 발생
ETIMEOUT
- 요청을 보냈으나 응답이 일정 시간 이내에 오지 않은 경우 발생
- 요청을 받는 서버의 상태 점검 필요
ENOENT: no such file or directory
- 지정한 폴더나 파일이 존재하지 않는 경우
본 포스트는 조현영 저자의 Node.js 교과서 2판을 기반으로 스터디하며 정리한 내용들입니다.