Node.js 교재 · 21편 · 로깅

Node 로깅 — winston·morgan으로 서버 로그

console.log 만 쓰다 보면 production 디버깅에서 죽는다. 두 라이브러리로 표준 가자.

로그 스트림이 구조화된 형식으로 흐르는 컨셉 일러스트

"문제 생기면 콘솔 보면 되지" — 사용자 1명 시절엔 OK. 100명 → 1000명 동시 요청 환경에서 console.log('user x clicked') 천 줄이 뒤섞이면 누가 무엇을 했는지 영원히 못 찾는다. 그게 production 디버깅의 현실.

답은 구조화 로깅(JSON 형식) + 레벨 분리(info/warn/error) + 요청 ID(같은 요청 추적). 표준 도구가 winston(서버 로그) + morgan(HTTP 액세스 로그).

1. console.log 의 한계

// ❌ production 디버깅 불가능한 코드 console.log('login'); console.log('user found', user.email); console.log('error', err);

문제 4가지 — ① 시간 정보 없음, ② 레벨 구분 없음(info 와 error 가 섞임), ③ 같은 요청의 로그를 묶을 ID 없음, ④ 검색·필터 불가능. 로그 도구에 보내도 의미가 없어진다.

2. winston — 표준 로거

$ npm install winston
// logger.js import winston from 'winston'; export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json(), // 구조화 JSON ), defaultMeta: { service: 'api' }, transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }), ], });
// 사용 logger.info('user logged in', { userId: 'abc123' }); logger.warn('rate limit close', { remaining: 5 }); logger.error('db connection failed', { err });

JSON 출력 예:

{"level":"info","message":"user logged in","userId":"abc123","timestamp":"2026-04-25T01:23:45Z","service":"api"} {"level":"error","message":"db connection failed","err":{...},"stack":"...","timestamp":"...","service":"api"}

이게 진짜 로그. 시간·레벨·메시지·메타데이터가 정확한 구조로. 로그 수집기(Datadog·Grafana Loki·CloudWatch)가 자동으로 파싱·검색·집계.

3. 레벨 — 5단계만 외우면 끝

레벨언제
error예상 못한 실패DB 끊김·외부 API 죽음
warn정상이지만 주의rate limit 90% 도달·재시도 1번
info주요 비즈니스 이벤트로그인·결제·가입
httpHTTP 요청 (morgan 담당)GET /api/users 200 23ms
debug개발용 상세변수 값·내부 분기

실전 — production 은 info 이상, 개발은 debug. 환경변수 LOG_LEVEL 로 분기.

4. morgan — HTTP 액세스 로그

모든 HTTP 요청을 한 줄씩 자동 기록. winston 과 결합해서 쓴다.

$ npm install morgan
import morgan from 'morgan'; import { logger } from './logger.js'; // winston 으로 morgan 출력 보내기 const stream = { write: (msg) => logger.http(msg.trim()), }; // 표준 Apache combined 포맷 app.use(morgan('combined', { stream })); // 또는 JSON 커스텀 morgan.token('id', (req) => req.id); app.use(morgan( ':id :method :url :status :response-time ms', { stream } ));

출력 예:

req-abc123 GET /api/users 200 23.5 ms req-abc124 POST /api/login 401 12.1 ms

5. 요청 ID — 같은 요청 추적

가장 강력한 패턴. 한 요청의 모든 로그를 한 ID 로 묶어 검색.

// requestId middleware import crypto from 'node:crypto'; app.use((req, res, next) => { req.id = req.headers['x-request-id'] || crypto.randomUUID(); res.setHeader('X-Request-Id', req.id); next(); }); // 핸들러에서 사용 router.post('/login', async (req, res) => { logger.info('login attempt', { requestId: req.id, email: req.body.email }); try { const user = await db.users.findByEmail(req.body.email); if (!user) { logger.warn('login failed: no user', { requestId: req.id }); return res.status(401).json({ error: '잘못된 자격' }); } // ... logger.info('login success', { requestId: req.id, userId: user.id }); } catch (err) { logger.error('login error', { requestId: req.id, err }); throw err; } });

나중에 사용자가 "어제 오후 3시쯤 로그인 실패했다" 라고 했을 때 — X-Request-Id 헤더 값 받아서 로그 수집기에서 requestId:req-abc 검색하면 그 요청의 모든 단계가 한 화면에. 이게 production 디버깅의 표준.

로그에 절대 넣지 말 것 — 비밀번호·신용카드 번호·주민번호·JWT 전체. 로그 수집기·백업·기록에 영구 남고 보안 사고가 된다. 자동으로 마스킹하는 pino-noir 같은 도구도 있지만, 기본은 로그 라인 작성 시 의식적으로 빼기.
pino 도 보세요 — winston 의 라이벌. JSON 출력만 + 비동기 + 매우 빠름. 고성능 서버엔 pino 가 우세. winston 은 transport 가 풍부(파일·DB·이메일·Slack)해서 다목적. 회사 코드 보고 맞는 쪽 사용.

요약 — 21편 좌표

여기까지 정리. console.log 졸업 — production 디버깅 불가능. winston 으로 구조화 JSON + 레벨 + 메타데이터, morgan 으로 HTTP 액세스 로그. 5단계 레벨(error·warn·info·http·debug). 요청 ID 미들웨어가 같은 요청 추적의 표준. 비밀번호·민감 정보는 로그에 절대 금지. pino 는 고성능 대안. 다음 편에서 Jest 테스트로 코드 안정성.

다음 편 예고 — 테스트 Jest

유닛 테스트·API 테스트 짜기. 22편.

© 2026 주나이테크(주) @JUNAITECH