📊 에러 로깅과 모니터링
📖 정의
**에러 로깅(Error Logging)**은 애플리케이션에서 발생하는 에러와 중요한 이벤트를 기록하는 것이고, **모니터링(Monitoring)**은 실시간으로 애플리케이션 상태를 추적하는 것입니다. console.log는 개발 환경에서만 유용하며, 프로덕션에서는 구조화된 로깅, 에러 추적 도구(Sentry), 로그 레벨, 알림 시스템이 필수입니다.
🎯 비유로 이해하기
병원의 환자 모니터링
에러 로깅을 병원 시스템에 비유하면:
console.log = 간호사의 메모
├─ 개인 수첩에 기록
├─ 체계적이지 않음
├─ 퇴근하면 못 봄
└─ 응급 상황 대응 어려움
로깅 시스템 = 전자 의료 기록
├─ 모든 정보 중앙 저장
├─ 시간순 정렬
├─ 검색 가능
└─ 여러 의사가 동시 접근
에러 모니터링 = 환자 모니터
├─ 실시간 생체 신호
├─ 이상 시 즉시 알림
├─ 대시보드로 한눈에
└─ 24시간 감시
로그 레벨 = 응급도 분류
├─ ERROR = 응급 (즉시 대응)
├─ WARN = 주의 (관찰 필요)
├─ INFO = 정상 (기록용)
└─ DEBUG = 상세 (진단용)
비행기 블랙박스
개발 환경 = 지상 테스트
└─ console.log로 충분
프로덕션 환경 = 실제 비행
├─ 블랙박스 (로깅 시스템)
│ └─ 모든 이벤트 기록
├─ 계기판 (모니터링 대시보드)
│ └─ 실시간 상태 확인
└─ 관제탑 (알림 시스템)
└─ 문제 발생 시 즉시 통보
사고 발생 시:
1. 블랙박스 확인 (로그 분석)
2. 원인 파악 (스택 트레이스)
3. 재발 방지 (모니터링 강화)
⚙️ 작동 원리
1. 로그 레벨 (Log Levels)
ERROR (높음) - 즉시 대응 필요
├─ 서버 크래시
├─ 데이터베이스 연결 실패
├─ 결제 시스템 오류
└─ 예: "PaymentService: Credit card charge failed"
WARN - 주의 필요
├─ 성능 저하
├─ 디스크 공간 부족
├─ API 응답 느림
└─ 예: "Database: Connection pool near capacity (90%)"
INFO - 정상 작동 기록
├─ 서버 시작/종료
├─ 사용자 로그인
├─ 주요 기능 실행
└─ 예: "Server started on port 3000"
DEBUG - 개발/디버깅용
├─ 함수 호출 추적
├─ 변수 값 확인
├─ 상세한 실행 흐름
└─ 예: "UserService.findById called with id=123"
TRACE (낮음) - 매우 상세
└─ 모든 세부 정보
2. 에러 추적 흐름
애플리케이션에서 에러 발생
↓
에러 캐치 (try-catch)
↓
에러 정보 수집
├─ 에러 메시지
├─ 스택 트레이스
├─ 사용자 정보
├─ 요청 정보 (URL, params)
├─ 환경 정보 (브라우저, OS)
└─ 시간
↓
로깅 시스템으로 전송
├─ Sentry
├─ LogRocket
└─ CloudWatch
↓
분석 및 알림
├─ 에러 그룹화
├─ 빈도 계산
├─ 심각도 판단
└─ Slack/이메일 알림
↓
개발자 대응
└─ 원인 파악 및 수정
3. Sentry 작동 원리
클라이언트 (브라우저/서버)
├─ Sentry SDK 초기화
├─ 자동 에러 캐치
└─ 에러 발생 시 전송
↓
Sentry 서버
├─ 에러 수신
├─ 소스맵 적용 (압축 해제)
├─ 스택 트레이스 정리
├─ 중복 제거 (같은 에러 그룹화)
├─ 영향받은 사용자 수 계산
└─ 알림 규칙 확인
↓
대시보드
├─ 에러 목록
├─ 상세 정보
├─ 타임라인
├─ 영향 범위
└─ 해결 상태
↓
알림 (조건 충족 시)
├─ Slack
├─ 이메일
├─ Discord
└─ PagerDuty
💡 실제 예시
Step 1: 기본 로깅 시스템 구축
# 프로젝트 생성
mkdir logging-demo
cd logging-demo
npm init -y
# Winston 로깅 라이브러리 설치
npm install winston
# Express 및 기타 패키지
npm install express dotenv
// logger.js
// Winston으로 구조화된 로깅 시스템 구축
const winston = require('winston');
const path = require('path');
// 로그 포맷 정의
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
// 개발 환경용 포맷 (사람이 읽기 쉽게)
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0) {
msg += ` ${JSON.stringify(meta)}`;
}
return msg;
})
);
// Logger 생성
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
transports: [
// 에러 로그 파일
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 전체 로그 파일
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5
})
]
});
// 개발 환경에서는 콘솔에도 출력
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
// 헬퍼 함수
logger.logRequest = (req, res, duration) => {
logger.info('HTTP Request', {
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
});
};
logger.logError = (error, req = null) => {
const errorInfo = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
};
if (req) {
errorInfo.request = {
method: req.method,
url: req.originalUrl,
headers: req.headers,
body: req.body,
query: req.query,
params: req.params,
ip: req.ip
};
}
logger.error('Application Error', errorInfo);
};
module.exports = logger;
// server.js
require('dotenv').config();
const express = require('express');
const logger = require('./logger');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// 요청 로깅 미들웨어
app.use((req, res, next) => {
const start = Date.now();
// 응답이 끝나면 로그 기록
res.on('finish', () => {
const duration = Date.now() - start;
logger.logRequest(req, res, duration);
});
next();
});
// 정상 라우트
app.get('/', (req, res) => {
logger.info('Home page accessed');
res.json({ message: 'Welcome to Logging Demo' });
});
// 의도적으로 에러 발생
app.get('/error', (req, res) => {
logger.warn('Error endpoint accessed - this will throw an error');
// 에러 발생!
throw new Error('This is a test error');
});
// 데이터베이스 시뮬레이션
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
logger.debug('Fetching user', { userId });
// 사용자 조회 (시뮬레이션)
if (userId === '1') {
logger.info('User found', { userId, username: 'john_doe' });
res.json({ id: 1, username: 'john_doe' });
} else {
logger.warn('User not found', { userId });
res.status(404).json({ error: 'User not found' });
}
});
// 느린 API 시뮬레이션
app.get('/slow', async (req, res) => {
const delay = 3000;
logger.warn('Slow API called', { delay });
await new Promise(resolve => setTimeout(resolve, delay));
res.json({ message: 'This was slow' });
});
// 404 핸들러
app.use((req, res) => {
logger.warn('404 Not Found', {
method: req.method,
url: req.originalUrl
});
res.status(404).json({ error: 'Not Found' });
});
// 에러 핸들러
app.use((err, req, res, next) => {
// 에러 로깅
logger.logError(err, req);
// 클라이언트 응답
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
app.listen(PORT, () => {
logger.info('Server started', {
port: PORT,
environment: process.env.NODE_ENV || 'development'
});
});
module.exports = app;
# .env
PORT=3000
NODE_ENV=development
LOG_LEVEL=debug
# logs 디렉토리 생성
mkdir logs
# .gitignore에 추가
echo "logs/" >> .gitignore
# 서버 실행
node server.js
# 출력 예시:
# 2024-01-15 10:00:00 [info]: Server started {"port":"3000","environment":"development"}
# 테스트
curl http://localhost:3000
# 2024-01-15 10:00:05 [info]: Home page accessed
# 2024-01-15 10:00:05 [info]: HTTP Request {"method":"GET","url":"/","status":200,"duration":"5ms",...}
curl http://localhost:3000/error
# 2024-01-15 10:00:10 [warn]: Error endpoint accessed - this will throw an error
# 2024-01-15 10:00:10 [error]: Application Error {"message":"This is a test error","stack":"Error: This is a test error\n at..."}
# 로그 파일 확인
tail -f logs/combined.log
tail -f logs/error.log
Step 2: Sentry 통합 (프로덕션 에러 추적)
# Sentry 패키지 설치
npm install @sentry/node @sentry/tracing
// sentry.js
const Sentry = require('@sentry/node');
const Tracing = require('@sentry/tracing');
function initSentry(app) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 1.0, // 성능 모니터링: 100% 요청 추적
// Release 버전 (git commit hash 사용 권장)
release: process.env.APP_VERSION || '1.0.0',
// Express 통합
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app })
]
});
return Sentry;
}
module.exports = { initSentry };
// server.js (Sentry 추가)
require('dotenv').config();
const express = require('express');
const logger = require('./logger');
const { initSentry } = require('./sentry');
const app = express();
const Sentry = initSentry(app);
// Sentry 미들웨어 (최상단에 배치)
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(express.json());
// ... 기존 라우트 ...
// 에러 핸들러 (Sentry 에러 핸들러 먼저)
app.use(Sentry.Handlers.errorHandler());
// 커스텀 에러 핸들러
app.use((err, req, res, next) => {
// Winston으로도 로깅
logger.logError(err, req);
// Sentry에 추가 컨텍스트 전송
Sentry.withScope((scope) => {
scope.setUser({
id: req.user?.id,
username: req.user?.username,
email: req.user?.email
});
scope.setExtra('requestBody', req.body);
scope.setExtra('requestQuery', req.query);
Sentry.captureException(err);
});
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info('Server started with Sentry monitoring', {
port: PORT,
environment: process.env.NODE_ENV
});
});
# .env에 Sentry DSN 추가
cat >> .env << 'EOF'
SENTRY_DSN=https://abc123xyz456@o123456.ingest.sentry.io/7890123
APP_VERSION=1.0.0
EOF
# Sentry DSN 얻기:
# 1. https://sentry.io 가입
# 2. 새 프로젝트 생성 (Node.js)
# 3. DSN 복사
// 에러 발생 시 Sentry 대시보드에서 확인 가능:
// - 에러 메시지와 스택 트레이스
// - 발생 시간과 빈도
// - 영향받은 사용자 수
// - 요청 정보 (URL, headers, body)
// - 사용자 정보 (로그인한 경우)
// - 환경 정보 (Node 버전, OS 등)
Step 3: 프론트엔드 에러 추적 (React)
# React 프로젝트에서
npm install @sentry/react @sentry/tracing
// src/sentry.js
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
export function initSentry() {
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.REACT_APP_VERSION || '1.0.0',
integrations: [
new BrowserTracing(),
new Sentry.Replay({
maskAllText: true,
blockAllMedia: true
})
],
// 성능 모니터링
tracesSampleRate: 1.0,
// 세션 재생 (에러 발생 시 화면 녹화)
replaysSessionSampleRate: 0.1, // 10% 세션 녹화
replaysOnErrorSampleRate: 1.0, // 에러 발생 시 100% 녹화
// 에러 필터링 (무시할 에러)
beforeSend(event, hint) {
// 크롬 익스텐션 에러 무시
if (event.exception?.values?.[0]?.value?.includes('chrome-extension')) {
return null;
}
// 네트워크 에러 필터링
if (hint.originalException?.message?.includes('NetworkError')) {
return null;
}
return event;
}
});
}
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import * as Sentry from '@sentry/react';
import { initSentry } from './sentry';
import App from './App';
// Sentry 초기화
initSentry();
const root = ReactDOM.createRoot(document.getElementById('root'));
// ErrorBoundary로 앱 감싸기
root.render(
<React.StrictMode>
<Sentry.ErrorBoundary
fallback={({ error, componentStack, resetError }) => (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>앗, 문제가 발생했습니다! 😢</h1>
<p>죄송합니다. 오류가 발생했습니다.</p>
<button onClick={resetError}>다시 시도</button>
{process.env.NODE_ENV === 'development' && (
<details style={{ marginTop: '20px', textAlign: 'left' }}>
<summary>에러 상세 정보</summary>
<pre>{error.toString()}</pre>
<pre>{componentStack}</pre>
</details>
)}
</div>
)}
showDialog
>
<App />
</Sentry.ErrorBoundary>
</React.StrictMode>
);
// src/App.js
import React, { useState } from 'react';
import * as Sentry from '@sentry/react';
function App() {
const [count, setCount] = useState(0);
// 의도적인 에러
const causeError = () => {
throw new Error('This is a test error from React');
};
// API 호출 에러
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('API request failed');
const data = await response.json();
console.log(data);
} catch (error) {
// Sentry로 전송
Sentry.captureException(error);
alert('데이터를 불러오는 데 실패했습니다.');
}
};
// 커스텀 에러 보고
const reportIssue = () => {
Sentry.captureMessage('사용자가 이슈를 보고했습니다', 'warning');
};
// 사용자 정보 설정 (로그인 시)
const setUser = () => {
Sentry.setUser({
id: '12345',
username: 'john_doe',
email: 'john@example.com'
});
};
// Breadcrumb (에러 발생 전 사용자 행동 추적)
const handleClick = () => {
Sentry.addBreadcrumb({
category: 'user-action',
message: 'User clicked button',
level: 'info'
});
setCount(count + 1);
};
return (
<div style={{ padding: '40px' }}>
<h1>Sentry Error Tracking Demo</h1>
<div style={{ margin: '20px 0' }}>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment (with breadcrumb)</button>
</div>
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<button onClick={causeError}>
Throw Error
</button>
<button onClick={fetchData}>
API Error
</button>
<button onClick={reportIssue}>
Report Issue
</button>
<button onClick={setUser}>
Set User
</button>
</div>
{/* 성능 측정 */}
<Sentry.Profiler name="MyComponent">
<ExpensiveComponent />
</Sentry.Profiler>
</div>
);
}
function ExpensiveComponent() {
// 무거운 계산
const result = Array.from({ length: 1000000 }, (_, i) => i * 2)
.reduce((a, b) => a + b, 0);
return <div>Result: {result}</div>;
}
export default Sentry.withProfiler(App);
# .env
REACT_APP_SENTRY_DSN=https://xyz789@sentry.io/123456
REACT_APP_VERSION=1.0.0
# 빌드 및 배포
npm run build
# Sentry에서 확인 가능:
# - JavaScript 에러
# - 네트워크 에러
# - 사용자 행동 (Breadcrumbs)
# - 세션 재생 (에러 발생 시 화면 녹화)
# - 성능 메트릭
Step 4: 로그 분석 및 알림 설정
// alerting.js
// Slack 알림 통합
const axios = require('axios');
const logger = require('./logger');
class AlertManager {
constructor() {
this.slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
this.errorThreshold = 10; // 10분에 10개 에러 시 알림
this.errorCount = 0;
this.resetInterval = 10 * 60 * 1000; // 10분
// 주기적으로 카운터 리셋
setInterval(() => {
this.errorCount = 0;
}, this.resetInterval);
}
async sendSlackAlert(message, severity = 'error') {
if (!this.slackWebhookUrl) {
logger.warn('Slack webhook URL not configured');
return;
}
const color = {
error: '#FF0000',
warning: '#FFA500',
info: '#0000FF'
}[severity] || '#808080';
const payload = {
attachments: [
{
color: color,
title: `🚨 ${severity.toUpperCase()}: Application Alert`,
text: message,
footer: 'Logging System',
ts: Math.floor(Date.now() / 1000)
}
]
};
try {
await axios.post(this.slackWebhookUrl, payload);
logger.info('Slack alert sent', { severity, message });
} catch (error) {
logger.error('Failed to send Slack alert', { error: error.message });
}
}
onError(error, context = {}) {
this.errorCount++;
// 임계값 초과 시 알림
if (this.errorCount >= this.errorThreshold) {
const message = `Error threshold exceeded: ${this.errorCount} errors in ${this.resetInterval / 60000} minutes\n\nLatest error: ${error.message}`;
this.sendSlackAlert(message, 'error');
// 카운터 리셋 (스팸 방지)
this.errorCount = 0;
}
}
onSlowResponse(url, duration) {
if (duration > 3000) { // 3초 이상
const message = `Slow API detected:\nURL: ${url}\nDuration: ${duration}ms`;
this.sendSlackAlert(message, 'warning');
}
}
onServerStart(port) {
const message = `Server started successfully on port ${port}`;
this.sendSlackAlert(message, 'info');
}
}
module.exports = new AlertManager();
// server.js에 알림 추가
const alertManager = require('./alerting');
// 서버 시작 시 알림
app.listen(PORT, () => {
logger.info('Server started', { port: PORT });
alertManager.onServerStart(PORT);
});
// 에러 핸들러에 알림 추가
app.use((err, req, res, next) => {
logger.logError(err, req);
alertManager.onError(err, { url: req.originalUrl });
res.status(500).json({ error: 'Internal Server Error' });
});
// 느린 응답 모니터링
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
alertManager.onSlowResponse(req.originalUrl, duration);
});
next();
});
# .env에 Slack Webhook URL 추가
cat >> .env << 'EOF'
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX
EOF
# Slack Webhook 설정:
# 1. Slack 워크스페이스 → Apps
# 2. "Incoming Webhooks" 검색 및 추가
# 3. 채널 선택 (#alerts)
# 4. Webhook URL 복사
Step 5: 대시보드 및 시각화
# ELK Stack (Elasticsearch, Logstash, Kibana) 간단한 대안
# Winston → File → 분석 도구
# 또는 클라우드 로깅 서비스:
# - AWS CloudWatch
# - Google Cloud Logging
# - Datadog
# - New Relic
# 간단한 로그 분석 스크립트
cat > analyze-logs.js << 'EOF'
const fs = require('fs');
const readline = require('readline');
async function analyzeLogs(filePath) {
const stats = {
total: 0,
byLevel: {},
byUrl: {},
errors: []
};
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
try {
const log = JSON.parse(line);
stats.total++;
// 레벨별 집계
stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
// URL별 집계
if (log.url) {
stats.byUrl[log.url] = (stats.byUrl[log.url] || 0) + 1;
}
// 에러 수집
if (log.level === 'error') {
stats.errors.push({
message: log.message,
timestamp: log.timestamp
});
}
} catch (err) {
// JSON 파싱 실패 무시
}
}
return stats;
}
// 실행
analyzeLogs('./logs/combined.log').then(stats => {
console.log('📊 Log Analysis Report\n');
console.log(`Total Logs: ${stats.total}\n`);
console.log('By Level:');
Object.entries(stats.byLevel).forEach(([level, count]) => {
console.log(` ${level}: ${count}`);
});
console.log('\nTop URLs:');
Object.entries(stats.byUrl)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([url, count]) => {
console.log(` ${url}: ${count}`);
});
console.log(`\nTotal Errors: ${stats.errors.length}`);
if (stats.errors.length > 0) {
console.log('\nRecent Errors:');
stats.errors.slice(-5).forEach(err => {
console.log(` [${err.timestamp}] ${err.message}`);
});
}
});
EOF
node analyze-logs.js
🤔 자주 묻는 질문
Q1. console.log와 로깅 라이브러리의 차이는?
A:
// console.log (개발용)
console.log('User logged in');
console.log('Error:', error);
// 문제점:
// ❌ 로그 레벨 없음 (모두 같은 중요도)
// ❌ 구조화되지 않음
// ❌ 파일 저장 안 됨
// ❌ 검색/필터링 어려움
// ❌ 프로덕션에서 성능 저하
// ❌ 중앙 집중식 관리 불가
// Winston (프로덕션용)
logger.info('User logged in', { userId: 123 });
logger.error('Login failed', { error: error.message, userId: 123 });
// 장점:
// ✅ 로그 레벨 (error, warn, info, debug)
// ✅ 구조화된 데이터 (JSON)
// ✅ 파일/데이터베이스 저장
// ✅ 검색/필터링 쉬움
// ✅ 환경별 설정 (dev/prod)
// ✅ 로그 회전 (파일 크기 관리)
// ✅ 여러 전송 대상 (파일, 콘솔, 외부 서비스)
// 비교:
// 개발 환경
if (process.env.NODE_ENV === 'development') {
console.log('Quick debug'); // OK
}
// 프로덕션 환경
logger.info('User action', { action: 'login', userId: 123 }); // 권장
// 규칙:
// - 개발: console.log 사용 가능 (빠른 디버깅)
// - 프로덕션: 반드시 로깅 라이브러리 사용
// - 모든 console.log는 배포 전 제거 또는 logger로 변경
Q2. 어떤 정보를 로깅해야 하나요?
A:
// ✅ 로깅해야 할 것
// 1. 시스템 이벤트
logger.info('Server started', { port: 3000, environment: 'production' });
logger.info('Database connected', { host: 'mongodb.example.com' });
logger.warn('Database connection lost, reconnecting...');
// 2. 사용자 행동 (비즈니스 로직)
logger.info('User logged in', { userId: 123, method: 'email' });
logger.info('Order placed', { userId: 123, orderId: 456, amount: 99.99 });
logger.info('Payment processed', { orderId: 456, paymentMethod: 'card' });
// 3. 에러 및 예외
logger.error('Payment failed', {
error: error.message,
orderId: 456,
userId: 123,
stack: error.stack
});
// 4. 성능 관련
logger.warn('Slow query detected', {
query: 'SELECT * FROM users',
duration: 5000, // ms
threshold: 1000
});
// 5. 보안 이벤트
logger.warn('Multiple failed login attempts', {
userId: 123,
ip: '192.168.1.1',
attempts: 5
});
logger.error('Unauthorized access attempt', {
url: '/admin',
ip: '192.168.1.1'
});
// 6. 외부 API 호출
logger.info('External API called', {
service: 'PaymentGateway',
endpoint: '/charge',
duration: 500
});
// ❌ 로깅하면 안 되는 것
// 1. 민감한 정보
// ❌ logger.info('User login', { password: '12345' });
// ✅ logger.info('User login', { userId: 123 });
// ❌ logger.info('Payment', { cardNumber: '1234-5678-9012-3456' });
// ✅ logger.info('Payment', { cardLast4: '3456' });
// 2. 개인 정보
// ❌ logger.info('User data', { ssn: '123-45-6789' });
// ✅ logger.info('User data', { userId: 123 });
// 3. 너무 많은 로그 (성능 저하)
// ❌ 반복문 안에서 매번 로깅
for (let i = 0; i < 1000000; i++) {
logger.debug('Processing item', { i }); // 너무 많음!
}
// ✅ 요약만 로깅
logger.info('Processed items', { count: 1000000, duration: 5000 });
// 민감한 정보 마스킹 헬퍼
function maskSensitive(data) {
const masked = { ...data };
if (masked.password) masked.password = '***';
if (masked.cardNumber) masked.cardNumber = masked.cardNumber.slice(-4).padStart(16, '*');
if (masked.email) masked.email = masked.email.replace(/(.{2}).*(@.*)/, '$1***$2');
return masked;
}
logger.info('User data', maskSensitive({ email: 'john@example.com', password: 'secret' }));
// Output: { email: 'jo***@example.com', password: '***' }
Q3. 로그 파일이 너무 커지는데 어떻게 관리하나요?
A:
// Winston 로그 로테이션 설정
const winston = require('winston');
require('winston-daily-rotate-file');
// 1. 일별 로그 파일
const dailyRotateTransport = new winston.transports.DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m', // 파일 크기 제한
maxFiles: '14d', // 14일 보관
zippedArchive: true // 압축 저장
});
// 2. 크기 기반 로테이션
const logger = winston.createLogger({
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5, // 5개 파일 유지
tailable: true // 최신 로그를 파일명에 번호 없이 저장
})
]
});
// 3. 수동 로그 정리 스크립트
// cleanup-logs.js
const fs = require('fs');
const path = require('path');
function cleanupOldLogs(directory, daysToKeep = 7) {
const now = Date.now();
const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
fs.readdirSync(directory).forEach(file => {
const filePath = path.join(directory, file);
const stats = fs.statSync(filePath);
if (now - stats.mtime.getTime() > maxAge) {
fs.unlinkSync(filePath);
console.log(`Deleted old log: ${file}`);
}
});
}
cleanupOldLogs('./logs', 7);
// 4. Cron job 설정 (Linux/Mac)
// 매일 자정에 오래된 로그 삭제
// crontab -e
// 0 0 * * * node /path/to/cleanup-logs.js
// 5. 로그 레벨 조정 (프로덕션)
// .env
LOG_LEVEL=info // debug 비활성화 → 로그 양 감소
// 6. 샘플링 (고트래픽 환경)
// 모든 요청을 로깅하지 않고 일부만
let requestCount = 0;
app.use((req, res, next) => {
requestCount++;
// 10번에 1번만 로깅
if (requestCount % 10 === 0) {
logger.info('HTTP Request (sampled)', {
method: req.method,
url: req.url
});
}
next();
});
// 7. 외부 로깅 서비스 사용
// AWS CloudWatch, Datadog 등은 자동으로 관리
Q4. 프로덕션 에러를 어떻게 빠르게 발견하나요?
A:
// 실시간 에러 감지 전략
// 1. Sentry 알림 설정
// Sentry 대시보드:
// - Alerts → New Alert Rule
// 조건 예시:
// "새로운 이슈가 발생하면"
// "1시간에 10명 이상의 사용자가 영향받으면"
// "에러 발생률이 1% 초과하면"
// 알림 채널:
// - Email
// - Slack
// - Discord
// - PagerDuty (온콜)
// - Webhook
// 2. Health Check 엔드포인트
app.get('/health', (req, res) => {
// 데이터베이스 연결 확인
const dbHealthy = checkDatabase();
// 외부 서비스 확인
const servicesHealthy = checkExternalServices();
if (dbHealthy && servicesHealthy) {
res.status(200).json({ status: 'healthy' });
} else {
res.status(503).json({
status: 'unhealthy',
database: dbHealthy,
services: servicesHealthy
});
}
});
// 3. Uptime 모니터링 (외부 서비스)
// - UptimeRobot (무료)
// - Pingdom
// - StatusCake
// 1분마다 /health 엔드포인트 체크
// 다운되면 즉시 알림
// 4. 에러율 모니터링
let totalRequests = 0;
let errorRequests = 0;
app.use((req, res, next) => {
totalRequests++;
res.on('finish', () => {
if (res.statusCode >= 500) {
errorRequests++;
// 에러율 계산
const errorRate = (errorRequests / totalRequests) * 100;
if (errorRate > 5) { // 5% 초과
alertManager.sendSlackAlert(
`🚨 High error rate detected: ${errorRate.toFixed(2)}%`,
'error'
);
}
}
});
next();
});
// 주기적으로 리셋
setInterval(() => {
totalRequests = 0;
errorRequests = 0;
}, 60000); // 1분마다
// 5. 로그 분석 자동화
// - 매시간 로그 분석
// - 이상 패턴 감지
// - 알림 발송
setInterval(async () => {
const recentErrors = await getRecentErrors();
if (recentErrors.length > 100) {
alertManager.sendSlackAlert(
`⚠️ High error volume: ${recentErrors.length} errors in the last hour`,
'warning'
);
}
}, 3600000); // 1시간마다
// 6. 대시보드 활용
// Grafana, Kibana 등으로 실시간 시각화
// - 요청 수
// - 에러율
// - 응답 시간
// - 메모리 사용량
// 7. Slack Bot 통합
// 실시간 에러를 Slack 채널로 전송
// 팀원들이 즉시 확인 가능
Q5. 어떤 로깅 도구를 선택해야 하나요?
A:
// 상황별 추천
// 1. 소규모 프로젝트 (개인/스타트업)
// - Winston (Node.js)
// - 파일 기반 로깅
// - 무료
// 장점: 간단, 빠른 구축
// 단점: 수동 관리 필요
// 2. 중규모 프로젝트 (팀 5-20명)
// - Winston + Sentry
// - Sentry (에러 추적)
// - AWS CloudWatch (로그 중앙화)
// 장점: 에러 자동 알림, 팀 협업
// 비용: Sentry 무료 ~ $26/월
// 3. 대규모 프로젝트 (엔터프라이즈)
// - ELK Stack (Elasticsearch, Logstash, Kibana)
// - Datadog
// - New Relic
// - Splunk
// 장점: 강력한 분석, 시각화, APM
// 비용: $15-$100+/호스트/월
// 4. 서버리스 환경 (AWS Lambda, Vercel)
// - CloudWatch Logs (AWS)
// - Vercel Analytics
// - Sentry (에러만)
// 장점: 인프라 관리 불필요
// 비용: 사용량 기반
// 추천 조합:
// 무료 스타트:
// - Winston (로깅)
// - Sentry Free (에러 추적)
// - UptimeRobot (모니터링)
// 저예산 ($50/월 이하):
// - Winston → AWS CloudWatch
// - Sentry Team ($26/월)
// - Better Uptime
// 프로덕션 권장 ($100-500/월):
// - Datadog ($15/호스트/월)
// - Sentry Team
// - PagerDuty (온콜)
// 선택 기준:
// 1. 팀 크기
// 2. 트래픽
// 3. 예산
// 4. 기술 스택
// 5. 규정 준수 (로그 보관 기간 등)
// 시 작 추천:
// 1. Winston으로 시작
// 2. Sentry 무료 추가
// 3. 트래픽 증가 시 CloudWatch/Datadog 도입
🎓 다음 단계
에러 로깅과 모니터링을 마스터했다면, 다음을 학습해보세요:
- 환경변수 관리 - Sentry DSN 안전하게 관리하기
- REST API 서버 만들기 - API에 로깅 추가하기
- CI/CD란? - 자동화된 배포와 모니터링
고급 주제
# APM (Application Performance Monitoring)
- New Relic
- Datadog APM
- Elastic APM
# 분산 추적 (Distributed Tracing)
- Jaeger
- Zipkin
- OpenTelemetry
# 로그 집계 (Log Aggregation)
- ELK Stack
- Graylog
- Loki
# 실시간 모니터링
- Prometheus + Grafana
- InfluxDB + Telegraf
# 인프라 모니터링
- Nagios
- Zabbix
- Icinga
🎬 마무리
에러 로깅과 모니터링의 핵심:
- 구조화된 로깅: Winston으로 체계적 관리
- 에러 추적: Sentry로 실시간 감지
- 로그 레벨: ERROR, WARN, INFO, DEBUG
- 알림: Slack/Email로 즉시 통보
- 분석: 패턴 파악 및 개선
- 보안: 민감 정보 마스킹
"보이지 않는 것은 관리할 수 없습니다." 프로덕션 환경에서는 console.log를 넘어 체계적인 로깅 시스템이 필수입니다. 에러가 발생하기 전에 미리 감지하고, 발생했을 때 빠르게 대응할 수 있도록 모니터링을 구축하세요! 📊✨
사용자가 문제를 신고하기 전에, 여러분이 먼저 알아야 합니다!