📊 에러 로깅과 모니터링
📖 정의
**에러 로깅(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 복사