지난 포스트에서 다양한 로깅 라이브러리들을 살펴봤는데, 그중에서도 Winston은 가장 성숙하고 기능이 풍부한 라이브러리입니다.
많은 개발자들이 Winston을 “복잡하다”고 생각하지만, 한 번 제대로 설정해두면 강력하고 유연한 로깅 시스템을 구축할 수 있습니다. 이 포스트에서는 Next.js 프로젝트에 Winston을 적용하는 실전 가이드를 제공하겠습니다.
Winston이란?
Winston은 2010년부터 개발되기 시작한 Node.js의 대표적인 로깅 라이브러리입니다. “A logger for just about everything”이라는 슬로건처럼, 거의 모든 로깅 요구사항을 만족시킬 수 있는 유연성을 제공합니다.
Winston의 핵심 철학
다양한 출력 방식 지원: 콘솔, 파일, 데이터베이스, HTTP 엔드포인트 등 어디든 로그를 전송할 수 있습니다.
로그 레벨 관리: error, warn, info, debug 등 상황에 맞는 로그 레벨을 제공합니다.
포맷팅 자유도: JSON, 일반 텍스트, 커스텀 포맷 등 원하는 형태로 로그를 출력할 수 있습니다.
설치 및 기본 설정
먼저 Winston을 Next.js 프로젝트에 설치해보겠습니다.
npm install winston
# 또는
yarn add winston
기본 로거 설정
lib/logger.js
파일을 생성하고 기본 Winston 로거를 설정해보겠습니다:
// lib/logger.js
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'nextjs-app' },
transports: [
// 에러 로그는 별도 파일에 저장
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
// 모든 로그는 combined.log에 저장
new winston.transports.File({
filename: 'logs/combined.log'
})
],
});
// 개발 환경에서는 콘솔에도 출력
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
export default logger;
로그 레벨과 출력 예시
Winston의 기본 로그 레벨은 다음과 같습니다 (우선순위 순):
// lib/logger.js에 추가
export const logExample = () => {
logger.error('데이터베이스 연결 실패', {
error: 'Connection timeout',
database: 'mongodb://localhost:27017'
});
logger.warn('API 응답 시간이 느립니다', {
responseTime: 2500,
endpoint: '/api/users'
});
logger.info('사용자가 로그인했습니다', {
userId: 123,
email: 'user@example.com',
ip: '192.168.1.1'
});
logger.debug('디버그 정보', {
requestBody: { name: 'John', age: 30 },
headers: { 'user-agent': 'Mozilla/5.0...' }
});
};
콘솔 출력 예시:
2024-01-15 10:30:45 error: 데이터베이스 연결 실패 {"error":"Connection timeout","database":"mongodb://localhost:27017","service":"nextjs-app"}
2024-01-15 10:30:46 warn: API 응답 시간이 느립니다 {"responseTime":2500,"endpoint":"/api/users","service":"nextjs-app"}
2024-01-15 10:30:47 info: 사용자가 로그인했습니다 {"userId":123,"email":"user@example.com","ip":"192.168.1.1","service":"nextjs-app"}
Next.js 특화 설정
환경별 로그 레벨 관리
// lib/logger.js 수정
const getLogLevel = () => {
switch (process.env.NODE_ENV) {
case 'production':
return 'warn';
case 'test':
return 'error';
default:
return 'debug';
}
};
const logger = winston.createLogger({
level: getLogLevel(),
// ... 나머지 설정
});
로그 디렉토리 자동 생성
// lib/logger.js에 추가
import fs from 'fs';
import path from 'path';
const logDir = 'logs';
// logs 디렉토리가 없으면 생성
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
실제 프로젝트 적용 예시
1. API 라우트 로깅
// pages/api/users/[id].js
import logger from '../../../lib/logger';
export default async function handler(req, res) {
const { id } = req.query;
const { method } = req;
// 요청 로깅
logger.info('API 요청 시작', {
method,
url: req.url,
userId: id,
userAgent: req.headers['user-agent'],
ip: req.connection.remoteAddress
});
try {
if (method === 'GET') {
const user = await getUserById(id);
if (!user) {
logger.warn('존재하지 않는 사용자 요청', { userId: id });
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
logger.info('사용자 조회 성공', {
userId: id,
responseTime: Date.now() - req.startTime
});
res.status(200).json(user);
}
} catch (error) {
logger.error('API 요청 처리 중 오류 발생', {
method,
url: req.url,
userId: id,
error: error.message,
stack: error.stack
});
res.status(500).json({ error: '서버 내부 오류' });
}
}
2. 미들웨어 로깅
// middleware.js
import { NextResponse } from 'next/server';
import logger from './lib/logger';
export function middleware(request) {
const start = Date.now();
// 요청 정보 로깅
logger.info('요청 시작', {
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent'),
referer: request.headers.get('referer')
});
const response = NextResponse.next();
// 응답 정보 로깅
response.headers.set('x-response-time', `${Date.now() - start}ms`);
logger.info('요청 완료', {
method: request.method,
url: request.url,
status: response.status,
responseTime: Date.now() - start
});
return response;
}
export const config = {
matcher: '/api/:path*'
};
3. 에러 경계 로깅
// components/ErrorBoundary.js
import React from 'react';
import logger from '../lib/logger';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logger.error('React 컴포넌트 에러 발생', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
props: this.props,
url: typeof window !== 'undefined' ? window.location.href : 'SSR'
});
}
render() {
if (this.state.hasError) {
return <h1>문제가 발생했습니다.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
고급 기능 활용
1. 커스텀 포맷터 만들기
// lib/logger.js에 추가
const customFormat = winston.format.printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level.toUpperCase()}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` | ${JSON.stringify(metadata)}`;
}
return msg;
});
// 로거 설정에서 사용
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
customFormat
),
// ... 나머지 설정
});
2. 로그 파일 로테이션
npm install winston-daily-rotate-file
// lib/logger.js 수정
import DailyRotateFile from 'winston-daily-rotate-file';
const logger = winston.createLogger({
transports: [
new DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d'
}),
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '30d'
})
]
});
3. 외부 서비스 연동
Slack으로 에러 알림 보내기:
npm install winston-slack-webhook-transport
// lib/logger.js에 추가
import SlackHook from 'winston-slack-webhook-transport';
if (process.env.NODE_ENV === 'production') {
logger.add(new SlackHook({
webhookUrl: process.env.SLACK_WEBHOOK_URL,
level: 'error',
formatter: (info) => {
return {
text: `🚨 프로덕션 에러 발생`,
attachments: [{
color: 'danger',
fields: [{
title: 'Error Message',
value: info.message,
short: false
}, {
title: 'Service',
value: info.service,
short: true
}, {
title: 'Timestamp',
value: info.timestamp,
short: true
}]
}]
};
}
}));
}
성능 최적화 팁
1. 비동기 로깅 활용
// lib/logger.js 수정
const logger = winston.createLogger({
// ... 기본 설정
transports: [
new winston.transports.File({
filename: 'logs/combined.log',
handleExceptions: true,
handleRejections: true,
maxsize: 5242880, // 5MB
maxFiles: 3
})
],
exitOnError: false
});
2. 조건부 로깅
// 디버그 로그는 개발 환경에서만
if (process.env.NODE_ENV === 'development') {
logger.debug('상세한 디버그 정보', {
largeObject: someComplexData
});
}
// 또는 로그 레벨 체크
if (logger.isDebugEnabled()) {
logger.debug('디버그 정보', computeExpensiveData());
}
3. 메모리 사용량 모니터링
// lib/logger.js에 추가
export const logMemoryUsage = () => {
const usage = process.memoryUsage();
logger.info('메모리 사용량', {
rss: `${Math.round(usage.rss / 1024 / 1024)} MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`,
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
external: `${Math.round(usage.external / 1024 / 1024)} MB`
});
};
// 주기적으로 메모리 사용량 로깅
setInterval(logMemoryUsage, 60000); // 1분마다
프로덕션 배포 고려사항
1. 환경변수 설정
// .env.production
LOG_LEVEL=warn
LOG_MAX_FILE_SIZE=50m
LOG_MAX_FILES=30
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
2. 로그 수집 시스템 연동
ELK 스택(Elasticsearch, Logstash, Kibana)과의 연동:
// lib/logger.js에 추가
import { ElasticsearchTransport } from 'winston-elasticsearch';
if (process.env.NODE_ENV === 'production') {
logger.add(new ElasticsearchTransport({
clientOpts: {
node: process.env.ELASTICSEARCH_URL,
auth: {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD
}
},
level: 'info'
}));
}
3. 보안 고려사항
민감한 정보 필터링:
// lib/logger.js에 추가
const filterSensitiveData = winston.format((info) => {
const sensitiveFields = ['password', 'token', 'apiKey', 'creditCard'];
const filterObject = (obj) => {
if (typeof obj !== 'object' || obj === null) return obj;
const filtered = { ...obj };
for (const key in filtered) {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
filtered[key] = '[REDACTED]';
} else if (typeof filtered[key] === 'object') {
filtered[key] = filterObject(filtered[key]);
}
}
return filtered;
};
return filterObject(info);
});
const logger = winston.createLogger({
format: winston.format.combine(
filterSensitiveData(),
winston.format.json()
),
// ... 나머지 설정
});
Winston의 장단점 분석
✅ 장점
풍부한 기능: Transport, Format, Filter 등 로깅에 필요한 모든 기능 제공 높은 확장성: 플러그인 생태계를 통한 무한 확장 가능 안정성: 10년 이상의 검증된 라이브러리 커뮤니티: 활발한 커뮤니티와 풍부한 문서
❌ 단점
복잡한 설정: 초기 학습 곡선이 높음 상대적으로 무거움: 단순한 로깅에는 오버스펙일 수 있음 설정 파일 관리: 복잡한 설정으로 인한 관리 부담
다른 라이브러리와의 비교
Winston vs Pino: Winston은 기능의 풍부함, Pino는 성능에 초점 Winston vs Console.log: Winston은 구조화된 로깅과 다양한 출력 방식 지원 Winston vs Consola: Winston은 프로덕션 환경, Consola는 개발 편의성에 특화
실전 팁과 베스트 프랙티스
1. 로그 구조화
// 일관된 로그 구조 사용
logger.info('API 호출', {
action: 'user.get',
userId: 123,
method: 'GET',
endpoint: '/api/users/123',
responseTime: 150,
status: 'success'
});
2. 상관관계 ID 사용
// 요청별로 고유 ID 생성하여 추적
import { v4 as uuidv4 } from 'uuid';
export default function handler(req, res) {
const correlationId = uuidv4();
req.correlationId = correlationId;
logger.info('요청 시작', {
correlationId,
method: req.method,
url: req.url
});
// 모든 후속 로그에 correlationId 포함
}
3. 에러 스택 추적
try {
// 비즈니스 로직
} catch (error) {
logger.error('처리 중 오류 발생', {
error: {
message: error.message,
stack: error.stack,
name: error.name
},
context: {
userId: req.user?.id,
action: 'user.update'
}
});
}
마무리
Winston은 처음에는 복잡해 보일 수 있지만, 한 번 제대로 설정해두면 강력하고 안정적인 로깅 시스템을 구축할 수 있습니다. 특히 팀 단위 개발이나 프로덕션 환경에서는 Winston의 진가가 발휘됩니다.
다음 포스트에서는 고성능 JSON 로깅에 특화된 Pino를 다뤄보겠습니다. Winston과는 완전히 다른 접근 방식으로 로깅 성능을 극대화하는 방법을 알아보세요.
1 thought on “Winston으로 Next.js 엔터프라이즈급 로깅 시스템 구축하기”