"문제 생기면 콘솔 보면 되지" — 사용자 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 | 주요 비즈니스 이벤트 | 로그인·결제·가입 |
| http | HTTP 요청 (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편.