Node.js 로그 처리
Node.js 애플리케이션을 개발하고 운영할 때, 발생하는 이벤트, 오류, 상태 변화 등을 기록하는 것은 매우 중요합니다. 이러한 기록을 '로그(Log)'라고 하며, 로그를 체계적으로 관리하는 것을 '로깅(Logging)'이라고 합니다. 이 글에서는 Node.js 애플리케이션에서 로그를 효과적으로 관리하고 처리하는 방법에 대해 설명합니다.
Node.js 로그 처리
Node.js 애플리케이션을 개발하고 운영할 때, 발생하는 이벤트, 오류, 상태 변화 등을 기록하는 것은 매우 중요합니다. 이러한 기록을 ‘로그(Log)’라고 하며, 로그를 체계적으로 관리하는 것을 ‘로깅(Logging)’이라고 합니다.
로깅은 다음과 같은 상황에서 필수적입니다.
- 디버깅: 개발 중 예상치 못한 동작이 발생했을 때, 로그를 통해 코드의 실행 흐름과 변수의 상태를 추적하여 원인을 쉽게 파악할 수 있습니다.
- 오류 추적: 운영 환경에서 에러가 발생했을 때, 로그는 에러의 발생 위치, 시간, 관련 데이터 등을 알려주어 신속한 대응을 가능하게 합니다.
- 성능 모니터링: 사용자의 요청 처리 시간, 데이터베이스 쿼리 시간 등을 로그로 남겨 애플리케이션의 성능 병목 지점을 찾아내고 최적화할 수 있습니다.
- 보안 감사: 비정상적인 접근 시도나 중요한 데이터의 변경 이력 등을 로그로 기록하여 보안 사고를 추적하고 분석하는 데 사용할 수 있습니다.
Node.js 환경에서는 console.log()
를 사용하여 간단히 로그를 출력할 수 있지만, 실제 운영 환경의 애플리케이션에서는 다음과 같은 한계가 있습니다.
- 로그 레벨 부재: 모든 로그가 동일한 수준으로 취급되어, 정보성 메시지와 심각한 오류를 구분하기 어렵습니다.
- 출력 형식 제한: 로그 출력 형식을 자유롭게 커스터마이징하기 어렵습니다.
- 저장 및 관리의 어려움: 로그가 콘솔에만 출력되므로, 파일로 저장하거나 외부 서비스로 전송하는 기능이 기본적으로 제공되지 않습니다.
이러한 한계를 극복하기 위해, Node.js 생태계에는 Winston
, Pino
, Bunyan
등 다양한 로깅 라이브러리가 존재합니다. 이 중 Winston
은 가장 널리 사용되는 라이브러리 중 하나로, 다양한 기능과 유연한 확장성을 제공합니다.
이번 단원에서는 Winston
을 활용하여 Node.js 애플리케이션의 로깅 시스템을 구축하는 방법을 단계별로 학습합니다. 로그 레벨 설정, 다양한 출력 형식 지정, 로그 파일 자동 분할 저장 등 실무에서 유용하게 사용되는 기능들을 중심으로 다룰 것입니다.
1. Winston 기본 사용법
Winston
을 사용하여 기본적인 로그 시스템을 구축하는 방법을 알아봅니다. 콘솔과 파일에 동시에 로그를 기록하고, 로그 레벨에 따라 다른 처리를 하며, 원하는 형식으로 로그를 출력하는 방법을 학습합니다.
1) 관련 패키지 설치
Winston과 파일 관리 기능인 winston-daily-rotate-file
을 설치합니다.
1
$ yarn add winston winston-daily-rotate-file
2) Winston의 핵심 개념
Winston
로거는 크게 Level
, Format
, Transport
세 가지 핵심 요소로 구성됩니다.
로그 레벨 (Levels)
로그의 중요도를 나타내는 등급입니다. Winston은 기본적으로 npm
의 로그 레벨을 따르며, 심각도 순서는 다음과 같습니다.
error (0)
> warn (1)
> info (2)
> http (3)
> verbose (4)
> debug (5)
> silly (6)
로거에 특정 레벨을 설정하면, 해당 레벨과 그보다 높은 심각도를 가진 로그만 기록됩니다. 예를 들어, 레벨을 info
로 설정하면 info
, warn
, error
로그만 기록되고 http
, debug
등은 무시됩니다.
출력 형식 (Formats)
로그 메시지가 어떤 형태로 출력될지를 결정합니다. winston.format
객체를 통해 다양한 형식을 조합(combine
)하여 사용할 수 있습니다.
timestamp()
: 로그 기록 시간을 자동으로 추가합니다.json()
: 로그를 JSON 형식으로 출력합니다.simple()
:{level}: {message}
형태의 간단한 형식으로 출력합니다.colorize()
: 로그 레벨에 따라 색상을 입혀 가독성을 높입니다. (주로 콘솔 출력용)printf()
:C
의printf
처럼, 사용자가 직접 출력 형식을 정의할 수 있는 강력한 기능입니다.errors({ stack: true })
: 에러 객체를 로그로 남길 때, 스택 트레이스(호출 스택)를 함께 기록해 디버깅을 용이하게 합니다.
출력 대상 (Transports)
로그를 어디에 기록할지를 지정합니다. transports
배열에 여러 개의 출력 대상을 설정하여 하나의 로그를 여러 곳에 동시에 보낼 수 있습니다.
winston.transports.Console
: 콘솔(터미널)에 로그를 출력합니다.winston.transports.File
: 지정된 파일에 로그를 저장합니다.winston-daily-rotate-file
: 실무에서 매우 유용한 transport로, 날짜별 또는 용량별로 로그 파일을 자동으로 분리하고, 오래된 로그는 압축하거나 삭제하는 등의 고급 관리 기능을 제공합니다.
Winston 기본 설정 및 사용
아래 예제는 Winston
의 핵심 개념을 종합하여 로거를 설정하고 사용하는 방법을 보여줍니다.
실습: /.env
# ... 이전 내용 생략 ...
# 로그 출력 수준
LOG_LEVEL=DEBUG
# 로그 저장 폴더
LOG_PATH=logs
실습: /04-로그처리/01_winston1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 1. 필요한 모듈 참조.
import dotenv from 'dotenv';
// $ yarn add winston winston-daily-rotate-file
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
// 이 함수가 호출되는 순간 .env 파일의 내용이 process.env에 저장된다.
dotenv.config();
// 2. 로거(Logger) 생성
const logger = winston.createLogger({
/**
* 로그 레벨
* - error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
* - level을 지정하면 해당 레벨 이하의 로그만 출력됨. (기본값: info)
* - 개발 환경에서는 'debug'나 'silly'로 설정하여 모든 로그를 확인하고,
* 운영 환경에서는 'info'나 'warn'으로 설정하여 필요한 로그만 기록하는 것이 일반적.
*/
level: process.env.LOG_LEVEL?.toLowerCase() || 'debug',
/**
* 로그 출력 형식(Format) 설정
* - winston.format.combine(): 여러 포맷을 조합하여 사용.
*/
format: winston.format.combine(
// 타임스탬프를 'YYYY-MM-DD HH:mm:ss' 형식으로 추가
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
// 에러 객체가 주어졌을 경우, 스택 트레이스를 포함하여 출력
winston.format.errors({ stack: true }),
// 로그 메시지를 printf 형식으로 커스터마이징
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
// 만약 메타데이터(meta) 객체가 있다면, JSON 문자열로 변환하여 추가
const metaString = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
// 에러 객체가 있다면 스택 트레이스를, 없다면 일반 메시지를 사용
return `${timestamp} [${level.toUpperCase()}]: ${stack || message}${metaString}`;
})
),
/**
* 로그 출력 대상(Transports) 설정
* - 여러 개의 transport를 배열로 지정하여 로그를 다양한 곳으로 동시에 출력 가능.
*/
transports: [
// 1) 콘솔에 로그 출력
new winston.transports.Console({
// 콘솔 출력 시에는 색상을 입혀 가독성을 높일 수 있음.
format: winston.format.combine(
winston.format.colorize({ all: true }) // 모든 레벨에 색상 적용
)
}),
// 2) 파일에 모든 레벨의 로그 저장 (일별 파일 생성)
new DailyRotateFile({
// 로그 파일명 형식. %DATE%는 날짜로 자동 치환됨.
filename: `${process.env.LOG_PATH || 'logs'}/app-%DATE%.log`,
datePattern: 'YYYY-MM-DD', // 날짜 형식
zippedArchive: true, // 로그 파일이 생성된 후 이전 로그 파일을 압축할지 여부
maxSize: '20m', // 로그 파일의 최대 크기
maxFiles: '14d' // 로그 파일을 보관할 최대 일수 (14일)
}),
// 3) 에러 로그만 별도의 파일에 저장 (일별 파일 생성)
new DailyRotateFile({
level: 'error', // 'error' 레벨의 로그만 이 파일에 기록
filename: `${process.env.LOG_PATH || 'logs'}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d' // 에러 로그는 좀 더 길게 보관 (30일)
})
]
});
// 3. 로그 메시지 출력 테스트
console.log('\n==============================');
logger.error('이것은 에러 메시지입니다.');
logger.warn('이것은 경고 메시지입니다.');
logger.info('애플리케이션이 시작되었습니다. (정보)');
logger.http('사용자 요청이 있었습니다. (HTTP)');
logger.verbose('상세한 정보를 출력합니다. (Verbose)');
logger.debug('디버깅용 메시지입니다.', { userId: 'testuser', action: 'login' });
logger.silly('가장 낮은 레벨의 로그입니다. (Silly)');
console.log('==============================\n');
// 4. 에러 객체 로깅 테스트
try {
throw new Error('의도적으로 발생시킨 에러 객체');
} catch (e) {
// 에러 객체를 직접 전달하면, format에 설정된 `errors({ stack: true })`에 의해
// 스택 트레이스가 함께 기록됨.
logger.error('예외(catch)가 발생했습니다.', e);
}
2. 설정 파일 분리를 통한 로거 모듈화
애플리케이션 규모가 커지면 여러 파일에서 동일한 로거를 사용해야 합니다. 이때 모든 파일에서 로거를 개별적으로 설정하는 것은 비효율적이고 유지보수가 어렵습니다.
따라서 로거 설정을 별도의 모듈 파일로 분리하고, 다른 파일에서는 이 모듈을 가져와(import) 사용하는 것이 좋습니다. 이렇게 하면 로그 정책이 변경될 때 설정 파일 하나만 수정하면 되므로 관리가 매우 용이해집니다.
1) 로그 설정 모듈 작성
helpers/LogHelper.js
와 같이 재사용 가능한 모듈을 만듭니다. 내용은 첫 번째 예제와 거의 동일하지만, 마지막에 설정된 logger
객체를 export
하여 다른 파일에서 사용할 수 있도록 합니다.
실습: /helpers/LogHelper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 1. 필요한 모듈 참조.
import dotenv from 'dotenv';
// $ yarn add winston winston-daily-rotate-file
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
// 이 함수가 호출되는 순간 .env 파일의 내용이 process.env에 저장된다.
dotenv.config();
// 2. 로거(Logger) 생성
const logger = winston.createLogger({
/**
* 로그 레벨
* - error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
* - level을 지정하면 해당 레벨 이하의 로그만 출력됨. (기본값: info)
* - 개발 환경에서는 'debug'나 'silly'로 설정하여 모든 로그를 확인하고,
* 운영 환경에서는 'info'나 'warn'으로 설정하여 필요한 로그만 기록하는 것이 일반적.
*/
level: process.env.LOG_LEVEL?.toLowerCase() || 'debug',
/**
* 로그 출력 형식(Format) 설정
* - winston.format.combine(): 여러 포맷을 조합하여 사용.
*/
format: winston.format.combine(
// 타임스탬프를 'YYYY-MM-DD HH:mm:ss' 형식으로 추가
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
// 에러 객체가 주어졌을 경우, 스택 트레이스를 포함하여 출력
winston.format.errors({ stack: true }),
// 로그 메시지를 printf 형식으로 커스터마이징
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
// 만약 메타데이터(meta) 객체가 있다면, JSON 문자열로 변환하여 추가
const metaString = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
// 에러 객체가 있다면 스택 트레이스를, 없다면 일반 메시지를 사용
return `${timestamp} [${level.toUpperCase()}]: ${stack || message}${metaString}`;
})
),
/**
* 로그 출력 대상(Transports) 설정
* - 여러 개의 transport를 배열로 지정하여 로그를 다양한 곳으로 동시에 출력 가능.
*/
transports: [
// 1) 콘솔에 로그 출력
new winston.transports.Console({
// 콘솔 출력 시에는 색상을 입혀 가독성을 높일 수 있음.
format: winston.format.combine(
winston.format.colorize({ all: true }) // 모든 레벨에 색상 적용
)
}),
// 2) 파일에 모든 레벨의 로그 저장 (일별 파일 생성)
new DailyRotateFile({
// 로그 파일명 형식. %DATE%는 날짜로 자동 치환됨.
filename: `${process.env.LOG_PATH || 'logs'}/app-%DATE%.log`,
datePattern: 'YYYY-MM-DD', // 날짜 형식
zippedArchive: true, // 로그 파일이 생성된 후 이전 로그 파일을 압축할지 여부
maxSize: '20m', // 로그 파일의 최대 크기
maxFiles: '14d' // 로그 파일을 보관할 최대 일수 (14일)
}),
// 3) 에러 로그만 별도의 파일에 저장 (일별 파일 생성)
new DailyRotateFile({
level: 'error', // 'error' 레벨의 로그만 이 파일에 기록
filename: `${process.env.LOG_PATH || 'logs'}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d' // 에러 로그는 좀 더 길게 보관 (30일)
})
]
});
// 생성한 로거 객체를 내보냄 (export)
// -> 이 객체를 require하여 애플리케이션의 다른 파일에서 사용 가능
export default logger;
2) 모듈화된 로거 사용하기
이제 다른 파일에서는 log_helper.js
에서 내보낸 logger
객체를 import
하여 바로 사용할 수 있습니다. 코드가 훨씬 간결해지고, 로거의 사용에만 집중할 수 있습니다.
실습: /04-로그처리/02_winston2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 생성해 둔 로그 헬퍼 모듈 참조
// -> `LogHelper.js`에서 export한 logger 객체를 `logger`라는 이름으로 받는다.
import logHelper from "../helpers/LogHelper.js";
// 2. 로그 출력
// -> `LogHelper.js`에 설정된 내용에 따라, level에 맞는 로그가
// 콘솔과 파일에 각각 출력된다.
logHelper.error("이것은 에러 로그입니다.");
logHelper.warn("이것은 경고 로그입니다.");
logHelper.info("이것은 정보 로그입니다.");
logHelper.debug("이것은 디버그 로그입니다.");
logHelper.verbose("이것은 Verbose 로그입니다.");
console.log("로그 기록이 완료되었습니다.");