Winston 로거
Node.js 애플리케이션에서 console.log로 로그를 찍는 건 개발 중에는 편하지만, 프로덕션에서는 쓸 수 없다. 이유는 명확하다. 로그 레벨 구분이 안 되고, 파일로 저장이 안 되고, 포맷이 일정하지 않고, 로그가 쌓이면 디스크를 잡아먹는다. 이 문제들을 한 번에 해결하는 게 Winston이다.
Winston은 Node.js에서 가장 많이 사용되는 로깅 라이브러리다. 로그 레벨 분리, 다중 출력 대상(transport), 커스텀 포맷, 파일 로테이션까지 지원한다. NestJS에서는 nest-winston 패키지를 통해 NestJS의 기본 로거를 Winston으로 교체할 수 있다.
핵심 개념
Winston의 구조는 세 가지 축으로 이루어져 있다: 레벨, 포맷, 트랜스포트.
로그 레벨
Winston은 npm 로그 레벨을 기본으로 사용한다. 숫자가 낮을수록 심각도가 높다.
{ error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 }
트랜스포트에 level: 'info'를 설정하면 info 이상(error, warn, info)만 기록된다. 이게 핵심이다. 프로덕션에서는 info 이상만 파일에 저장하고, 개발 환경에서는 debug까지 콘솔에 출력하는 식으로 환경별 로그 수준을 제어할 수 있다.
트랜스포트 (Transport)
트랜스포트는 로그가 어디로 출력되는지를 정의한다. Winston의 가장 강력한 기능 중 하나는 하나의 로거에 여러 트랜스포트를 동시에 연결할 수 있다는 점이다.
{ error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 }
이 설정이면 모든 로그는 콘솔과 combined.log에 기록되고, error 레벨 로그는 error.log에 추가로 기록된다. 트랜스포트마다 독립적으로 레벨을 설정할 수 있어서, 에러 로그만 별도 파일로 분리하는 패턴이 가능하다.
기본 제공 트랜스포트:
| 트랜스포트 | 설명 |
|---|---|
Console | 콘솔 출력 |
File | 파일 저장 |
Http | HTTP 엔드포인트로 전송 |
Stream | Node.js 스트림으로 출력 |
포맷 (Format)
포맷은 로그 메시지가 어떤 형태로 출력되는지를 정의한다. Winston은 winston.format에 다양한 포맷터를 제공하고, combine()으로 여러 포맷을 조합할 수 있다.
import * as winston from 'winston';
const logger = winston.createLogger({
transports: [
// 콘솔에 출력
new winston.transports.Console(),
// 파일에 저장
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
combine()에 전달되는 포맷터들은 순서대로 적용된다. timestamp()가 로그 객체에 timestamp 필드를 추가하고, label()이 label 필드를 추가하고, printf()가 최종 문자열을 생성한다. 파이프라인처럼 동작한다고 생각하면 된다.
주요 포맷터:
| 포맷터 | 역할 |
|---|---|
timestamp() | 타임스탬프 추가 |
label() | 라벨 추가 (앱 이름, 모듈명 등) |
printf() | 커스텀 출력 형식 정의 |
colorize() | 레벨별 색상 적용 (콘솔용) |
json() | JSON 형식 출력 |
errors({ stack: true }) | 에러 스택트레이스 포함 |
splat() | %s, %d 같은 문자열 보간 지원 |
로그 파일 로테이션
프로덕션에서 로그 파일이 무한히 커지면 디스크가 터진다. winston-daily-rotate-file은 날짜별로 로그 파일을 분리하고, 오래된 파일을 자동으로 삭제하거나 압축한다.
const { combine, timestamp, label, printf, colorize } = winston.format;
const customFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [${label}] ${level}: ${message}`;
});
const logger = winston.createLogger({
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
label({ label: 'my-app' }),
customFormat,
),
});
npm install winston-daily-rotate-file
이 설정의 핵심 옵션들:
| 옵션 | 설명 |
|---|---|
datePattern | 파일명에 포함될 날짜 패턴. 'YYYY-MM-DD'면 일별, 'YYYY-MM-DD-HH'면 시간별 분리 |
dirname | 로그 파일 저장 디렉토리 |
filename | 파일명 패턴. %DATE%가 datePattern으로 치환됨 |
maxFiles | 보관할 최대 파일 수. '30d'처럼 일 수로도 지정 가능 |
maxSize | 파일당 최대 크기. '20m'이면 20MB 초과 시 새 파일 생성 |
zippedArchive | true면 로테이션된 파일을 gzip으로 압축 |
maxFiles: 30과 zippedArchive: true를 같이 사용하면, 30일치 로그만 유지하면서 오래된 로그는 압축해서 저장한다. 디스크 공간을 효율적으로 관리할 수 있다.
파일 구조 예시
위 설정으로 며칠간 운영하면 이런 구조가 된다:
logs/
├── 2024-11-06.log
├── 2024-11-05.log.gz
├── 2024-11-04.log.gz
└── error/
├── 2024-11-06.error.log
└── 2024-11-05.error.log.gz
당일 로그는 평문이고, 이전 날짜 로그는 gzip으로 압축된다.
NestJS 통합 (nest-winston)
NestJS에는 기본 로거(Logger)가 내장되어 있지만, 파일 저장이나 로테이션 같은 기능이 없다. nest-winston 패키지를 사용하면 NestJS의 로거를 Winston으로 교체할 수 있다.
import * as DailyRotateFile from 'winston-daily-rotate-file';
const logger = winston.createLogger({
transports: [
new DailyRotateFile({
level: 'info',
datePattern: 'YYYY-MM-DD',
dirname: './logs',
filename: '%DATE%.log',
maxFiles: 30, // 30일치 보관
zippedArchive: true, // 오래된 로그 gzip 압축
}),
new DailyRotateFile({
level: 'error',
datePattern: 'YYYY-MM-DD',
dirname: './logs/error',
filename: '%DATE%.error.log',
maxFiles: 30,
zippedArchive: true,
}),
],
});
모듈 등록
WinstonModule.forRoot()으로 앱 모듈에 등록한다.
logs/
├── 2024-11-06.log
├── 2024-11-05.log.gz
├── 2024-11-04.log.gz
└── error/
├── 2024-11-06.error.log
└── 2024-11-05.error.log.gz
환경 분기가 핵심이다. 프로덕션에서는 파일 트랜스포트로 info와 error 로그를 분리 저장하고, 개발 환경에서는 콘솔에 색상 있는 로그를 출력한다. 두 환경에서 같은 logger.info() 호출을 사용하면서 출력 대상만 달라지는 것이다.
서비스에서 사용
WINSTON_MODULE_PROVIDER 토큰으로 주입받아 사용한다.
npm install nest-winston winston winston-daily-rotate-file
WINSTON_MODULE_NEST_PROVIDER를 사용하면 NestJS의 LoggerService 인터페이스에 맞는 래퍼가 주입된다. NestJS 내부 로그(부트스트랩, 라우트 매핑 등)도 Winston으로 출력하려면 이쪽을 사용해야 한다.
import { Module } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';
const isProduction = process.env.NODE_ENV === 'production';
@Module({
imports: [
WinstonModule.forRoot({
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
}),
),
transports: isProduction
? [
new DailyRotateFile({
level: 'info',
datePattern: 'YYYY-MM-DD',
dirname: './logs',
filename: '%DATE%.log',
maxFiles: 30,
zippedArchive: true,
}),
new DailyRotateFile({
level: 'error',
datePattern: 'YYYY-MM-DD',
dirname: './logs/error',
filename: '%DATE%.error.log',
maxFiles: 30,
zippedArchive: true,
}),
]
: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
}),
),
}),
],
}),
],
})
export class AppModule {}
이렇게 하면 NestJS가 내부적으로 출력하는 모든 로그(모듈 초기화, 라우트 매핑, 에러 등)도 Winston 포맷으로 통일된다.
console.log vs Winston 비교
| 항목 | console.log | Winston |
|---|---|---|
| 로그 레벨 | ❌ 없음 (log, warn, error 정도) | ✅ 7단계 레벨 (error~silly) |
| 파일 저장 | ❌ 직접 구현 필요 | ✅ File 트랜스포트 |
| 로테이션 | ❌ 없음 | ✅ daily-rotate-file |
| 포맷 커스텀 | ❌ 제한적 | ✅ printf, json, combine 등 |
| 다중 출력 | ❌ stdout만 | ✅ 콘솔 + 파일 + HTTP 동시 |
| 메타데이터 | ❌ 없음 | ✅ 객체로 추가 정보 전달 |
| 환경별 설정 | ❌ 직접 분기 | ✅ 트랜스포트 분기 |
다른 Node.js 로거와 비교
Winston 외에도 Node.js에서 많이 사용되는 로거가 있다.
Pino
Pino는 성능에 집중한 로거다. JSON 출력이 기본이고, Winston보다 5~10배 빠르다고 주장한다. 로그를 JSON으로 구조화해서 ELK 스택 같은 로그 수집 시스템과 연동하기 좋다. NestJS에서는 nestjs-pino 패키지로 통합할 수 있다.
import { Inject, Injectable } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
@Injectable()
export class SomeService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
doSomething() {
this.logger.info('작업 시작');
try {
// 비즈니스 로직
this.logger.info('작업 완료');
} catch (error) {
this.logger.error('작업 실패', { error: error.message, stack: error.stack });
}
}
}
Winston은 커스텀 포맷과 다양한 트랜스포트가 강점이고, Pino는 순수 성능과 구조화된 로깅이 강점이다. 로그 볼륨이 극도로 많은 서비스에서는 Pino가 유리하고, 유연한 설정이 필요한 일반적인 서비스에서는 Winston이 더 편하다.
Morgan
Morgan은 HTTP 요청 전용 로거다. Express 미들웨어로 동작하며, 요청/응답 정보를 자동으로 기록한다. 애플리케이션 전체 로깅을 담당하는 Winston과는 역할이 다르다. 보통 Morgan으로 HTTP 로그를 찍고, Winston에 스트림으로 연결하는 조합으로 사용한다.
// main.ts에서 NestJS 전체 로거 교체
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
await app.listen(3000);
}
실전 팁
메타데이터 활용
Winston은 로그 메시지와 함께 구조화된 메타데이터를 전달할 수 있다. 디버깅할 때 매우 유용하다.
// Pino 기본 사용
import pino from 'pino';
const logger = pino({ level: 'info' });
logger.info({ userId: 123 }, 'User logged in');
// 출력: {"level":30,"time":1699228461000,"msg":"User logged in","userId":123}
JSON 포맷을 사용하면 이 메타데이터가 구조화된 형태로 저장되어서, 나중에 로그 검색 시스템에서 userId로 필터링하는 게 가능해진다.
에러 로깅 시 스택트레이스 포함
에러를 로깅할 때 메시지만 기록하면 디버깅이 어렵다. 반드시 스택트레이스를 포함시켜야 한다.
import morgan from 'morgan';
// Morgan의 출력을 Winston으로 스트리밍
app.use(
morgan('combined', {
stream: { write: (message) => logger.http(message.trim()) },
}),
);
errors({ stack: true }) 포맷을 추가하면 Error 객체를 직접 전달해도 스택이 포함된다:
// 단순 문자열만
logger.info('사용자 로그인');
// 메타데이터와 함께
logger.info('사용자 로그인', {
userId: 'user-123',
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
});
환경별 로그 레벨 제어
개발 환경에서는 debug 레벨까지, 스테이징에서는 info까지, 프로덕션에서는 warn 이상만 기록하는 식으로 제어할 수 있다.
try {
await riskyOperation();
} catch (error) {
// ❌ 이렇게 하면 안 됨
logger.error(error.message);
// ✅ 스택트레이스 포함
logger.error('작업 실패', {
error: error.message,
stack: error.stack,
context: { operationId: 'op-456' },
});
}
환경 변수로 로그 레벨을 주입하면 코드 변경 없이 배포 시점에 로그 수준을 조절할 수 있다. 프로덕션에서 디버깅이 필요할 때 LOG_LEVEL=debug로 재시작하면 된다.
정리
- 레벨/포맷/트랜스포트 3축 구조로, 환경별 출력 대상과 로그 수준을 코드 변경 없이 분리할 수 있다
- daily-rotate-file로 날짜별 파일 분리, maxFiles 보관, gzip 압축까지 디스크 관리를 자동화한다
- NestJS에서는 nest-winston + WINSTON_MODULE_NEST_PROVIDER로 프레임워크 내부 로그까지 Winston 포맷으로 통일할 수 있다