← 글 목록

notes

Library - winston

winston 로거의 레벨·transport·파일 기록과 서버 로그 설정 메모.

  • 로거는 하나만 만들어 모듈로 공유하고, 환경에 따라 level 만 바꾼다.
  • 에러는 error.log, 전체는 combined.log, 개발 중엔 콘솔로 나눈다.
  • 비밀번호, 토큰 같은 민감한 값은 로그에 남기지 않는다.

winston 이 하는 일

console.log 대신 쓰는 로거다. 로그에 레벨을 붙이고, 포맷을 정하고, 콘솔이나 파일 같은 여러 출력으로 동시에 보낼 수 있다.

npm install winston
# 또는
yarn add winston

기본 설정

createLogger 로 로거를 만든다. level 아래로는 걸러지고, format 으로 모양을 잡고, transports 로 내보낸다.

기본 설정 예제
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// 개발 환경에서는 콘솔에도 출력
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

로그 레벨

winston 은 기본으로 npm 레벨을 쓴다. 숫자가 작을수록 중요하다. level 을 info 로 두면 error, warn, info 만 남고 나머지는 버려진다.

logger.error('에러 발생');
logger.warn('디스크 공간 부족');
logger.info('사용자 로그인');
logger.http('GET /api/users');
logger.debug('디버그 정보');

// 객체를 같이 넘기면 메타 필드로 들어간다
logger.info('사용자 정보', { userId: 123, action: 'login' });

포맷 바꾸기

한 줄짜리 텍스트로 보고 싶으면 printf 를 쓴다. 에러 객체를 넘길 일이 있으면 format.errors({ stack: true }) 를 같이 넣어 두면 스택이 남는다.

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.errors({ stack: true }),
    winston.format.timestamp(),
    winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
  )
});

transport 나누기

출력마다 level 과 format 을 따로 줄 수 있다. 자주 쓰는 조합은 에러 파일, 전체 파일, 콘솔, 그리고 날짜별 순환 파일이다.

여러 transport 예제
const logger = winston.createLogger({
  transports: [
    // 에러만 따로
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      format: winston.format.combine(winston.format.timestamp(), winston.format.json())
    }),
    // 전체 로그
    new winston.transports.File({
      filename: 'logs/combined.log',
      format: winston.format.combine(winston.format.timestamp(), winston.format.json())
    }),
    // 콘솔
    new winston.transports.Console({
      format: winston.format.combine(winston.format.colorize(), winston.format.simple())
    })
  ]
});

파일이 끝없이 커지는 걸 막으려면 winston-daily-rotate-file 을 붙인다. 날짜별로 파일을 나누고, 크기와 보관 기간을 정하고, 오래된 파일은 압축한다.

날짜별 순환 예제
const winston = require('winston');
require('winston-daily-rotate-file');

const logger = winston.createLogger({
  transports: [
    new winston.transports.DailyRotateFile({
      filename: 'logs/application-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      zippedArchive: true,
      maxSize: '20m',
      maxFiles: '14d'
    })
  ]
});

Express 에 붙이기

요청 로그는 라우트보다 앞에, 에러 핸들러는 모든 라우트 뒤에 둔다. 순서가 바뀌면 에러가 핸들러까지 안 온다.

Express 미들웨어 예제
const express = require('express');
const logger = require('./logger');
const app = express();

// 요청 로깅 (라우트보다 앞)
app.use((req, res, next) => {
  logger.http(`${req.method} ${req.url}`, {
    ip: req.ip,
    userAgent: req.headers['user-agent']
  });
  next();
});

// ... 라우트들

// 에러 핸들링 (맨 뒤)
app.use((err, req, res, next) => {
  logger.error('에러 발생', {
    error: err.message,
    stack: err.stack,
    url: req.url
  });
  res.status(500).send('서버 에러');
});

쓰면서 지키는 것

로거 인스턴스는 하나만 만들어서 logger.js 에서 export 하고, 다른 파일은 그걸 가져다 쓴다.

level 은 환경에 따라 바꾼다. 운영에서는 info, 개발에서는 debug 다.

에러를 남길 때는 메시지만 쓰지 않고 스택과 컨텍스트(사용자 id, 액션)를 같이 넣는다.

// logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  // format, transports ...
});

module.exports = logger;

// 다른 파일
logger.error('에러 발생', { error: err.message, stack: err.stack, userId: user.id, action: 'user_update' });

주의할 건 네 가지다.

  • 비밀번호, 토큰, 개인정보는 로그에 남기지 않는다.
  • 로그 파일을 얼마나 두고 언제 지울지 정해 둔다.
  • 레벨을 아무거나 쓰지 않는다. 에러가 아닌 걸 error 로 찍으면 알림이 의미 없어진다.
  • 운영에서는 debug 로그를 끈다.

Next.js 에서 쓰기

Route Handler 에서 같은 로거를 가져다 쓸 수 있다. 예전 예제는 핸들러 인자를 임의 타입으로 적었는데, Route Handler 는 표준 Request 를 받고 body 는 await req.json() 으로 꺼낸다.

winston 은 Node.js API 를 쓰니 Edge 런타임이 아니라 Node.js 런타임에서 돌려야 한다. 서버리스 배포에서는 파일을 쓸 수 없거나 금방 사라지니까 Console transport 로 내보내고 플랫폼 로그에서 본다.

Route Handler 예제
import { NextResponse } from 'next/server'
import logger from '@/app/util/logger/logger'

export const runtime = 'nodejs'

interface TestLogBody {
  level: string
  message: string
  meta: { userId: string }
}

export async function POST(req: Request) {
  try {
    const body = (await req.json()) as TestLogBody

    logger.info(body.message ?? '테스트 로그 메시지', {
      test: true,
      userId: body.meta?.userId,
      timestamp: new Date().toISOString(),
    })

    return NextResponse.json({
      success: true,
      message: '로그가 기록됐다',
      logDir: process.cwd() + '/logs',
    })
  } catch (error) {
    logger.error('로그 기록 실패', { error })
    return NextResponse.json(
      { success: false, error: '로그 기록에 실패했다' },
      { status: 500 },
    )
  }
}