API 에러 추적 개선하기: errorId 패턴으로 디버깅 시간 단축
"서버 오류가 발생했습니다" 메시지만으로는 어떤 에러인지 특정할 수 없었습니다. errorId 패턴으로 디버깅 시간을 90% 단축한 방법을 공유합니다.
1. 문제 상황
1.1 일반적인 에러 응답의 한계
// ❌ 일반적인 에러 응답
return NextResponse.json(
{ error: '서버 오류가 발생했습니다.' },
{ status: 500 }
);
문제:
1. 사용자가 "서버 오류" 메시지를 보고 문의함
2. 개발자가 로그를 확인해야 함
3. 같은 시간대에 여러 에러가 있으면 어떤 것인지 특정 불가
4. 문의 시점과 에러 발생 시점이 다르면 찾기 어려움
1.2 실제 시나리오
사용자: "데이터를 저장하려고 하니까 오류가 났어요"
개발자: "언제 발생했나요?"
사용자: "10분 전쯤이요"
개발자: (로그 확인) "10분 전에 에러가 5개 있는데..."
→ 어떤 에러가 이 사용자의 것인지 특정할 수 없음
1.3 원하는 상황
사용자: "에러 코드가 'a1b2c3d4'라고 나왔어요"
개발자: (로그 검색) "errorId: a1b2c3d4" → 정확한 에러 즉시 확인
→ 디버깅 시간 90% 단축
2. 해결 방법: errorId 패턴
2.1 핵심 아이디어
1. 에러 발생 시 고유 ID 생성
2. 서버 로그에 해당 ID와 함께 상세 정보 기록
3. 클라이언트에 간단한 메시지 + ID만 응답
4. 사용자 문의 시 ID로 정확한 로그 검색
2.2 기본 구현
// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// 비즈니스 로직...
return NextResponse.json({ success: true });
} catch (error) {
// 1. 고유 에러 ID 생성 (8자리 UUID)
const errorId = crypto.randomUUID().slice(0, 8);
// 2. 서버 로그에 상세 정보 기록
console.error('Create item error:', {
errorId,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
// 추가 컨텍스트
requestBody: body,
});
// 3. 클라이언트에 간단한 응답 (보안)
return NextResponse.json(
{
error: '서버 오류가 발생했습니다.',
errorId, // 추적용 ID만 제공
},
{ status: 500 }
);
}
}
2.3 로그 출력 예시
{
"level": "error",
"message": "Create item error:",
"errorId": "a1b2c3d4",
"error": "Unique constraint failed on the constraint: `Item_name_key`",
"stack": "Error: Unique constraint...\n at ...",
"timestamp": "2026-01-11T12:34:56.789Z",
"requestBody": { "name": "테스트", "type": "A" }
}
3. 전체 구현
3.1 에러 핸들러 유틸리티
// lib/core/error-handler.ts
import { NextResponse } from 'next/server';
interface ErrorContext {
operation: string;
[key: string]: unknown;
}
/**
* API 에러 처리 유틸리티
*/
export function handleApiError(
error: unknown,
context: ErrorContext
): NextResponse {
const errorId = crypto.randomUUID().slice(0, 8);
// 구조화된 로그
console.error(`[${context.operation}] Error:`, {
errorId,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
...context,
});
// 클라이언트 응답
return NextResponse.json(
{
error: '서버 오류가 발생했습니다.',
errorId,
},
{ status: 500 }
);
}
3.2 API Route에서 사용
// app/api/items/route.ts
import { handleApiError } from '@/lib/core/error-handler';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// 비즈니스 로직...
return NextResponse.json({ item });
} catch (error) {
return handleApiError(error, {
operation: 'createItem',
userId: session?.user?.id,
requestBody: body,
});
}
}
export async function GET(request: NextRequest) {
try {
// 조회 로직...
return NextResponse.json({ items });
} catch (error) {
return handleApiError(error, {
operation: 'getItems',
searchParams: Object.fromEntries(request.nextUrl.searchParams),
});
}
}
3.3 에러 유형별 응답
// lib/core/error-handler.ts
export function handleApiError(
error: unknown,
context: ErrorContext
): NextResponse {
const errorId = crypto.randomUUID().slice(0, 8);
// Prisma 에러 처리
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// P2002: Unique constraint violation
if (error.code === 'P2002') {
console.warn(`[${context.operation}] Unique constraint:`, {
errorId,
code: error.code,
meta: error.meta,
});
return NextResponse.json(
{ error: '이미 존재하는 데이터입니다.', errorId },
{ status: 409 }
);
}
// P2025: Record not found
if (error.code === 'P2025') {
return NextResponse.json(
{ error: '데이터를 찾을 수 없습니다.', errorId },
{ status: 404 }
);
}
}
// 기본 500 에러
console.error(`[${context.operation}] Error:`, {
errorId,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
...context,
});
return NextResponse.json(
{ error: '서버 오류가 발생했습니다.', errorId },
{ status: 500 }
);
}
4. 클라이언트 에러 표시
4.1 에러 컴포넌트
// components/shared/ui/ErrorMessage.tsx
interface ErrorMessageProps {
message: string;
errorId?: string;
}
export function ErrorMessage({ message, errorId }: ErrorMessageProps) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700">{message}</p>
{errorId && (
<p className="text-red-500 text-sm mt-2">
오류 코드: <code className="font-mono">{errorId}</code>
<br />
<span className="text-gray-500">
(문의 시 이 코드를 알려주세요)
</span>
</p>
)}
</div>
);
}
4.2 API 호출에서 사용
async function createItem(data: ItemData) {
const response = await fetch('/api/items', {
method: 'POST',
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
// errorId가 있으면 함께 표시
throw new ApiError(result.error, result.errorId);
}
return result;
}
class ApiError extends Error {
constructor(message: string, public errorId?: string) {
super(message);
this.name = 'ApiError';
}
}
// React에서 사용
function ItemForm() {
const [error, setError] = useState<ApiError | null>(null);
const handleSubmit = async (data: ItemData) => {
try {
await createItem(data);
} catch (err) {
if (err instanceof ApiError) {
setError(err);
}
}
};
return (
<form onSubmit={handleSubmit}>
{error && (
<ErrorMessage message={error.message} errorId={error.errorId} />
)}
{/* ... */}
</form>
);
}
5. 로그 검색
5.1 로컬 개발
# Docker logs에서 검색
docker logs payroll-web 2>&1 | grep "a1b2c3d4"
# PM2 logs에서 검색
pm2 logs --lines 1000 | grep "a1b2c3d4"
5.2 클라우드 환경
# AWS CloudWatch
filter @message like /a1b2c3d4/
# Datadog
errorId:a1b2c3d4
# GCP Cloud Logging
jsonPayload.errorId="a1b2c3d4"
5.3 로그 구조화 (권장)
// 구조화된 JSON 로그 출력
console.error(JSON.stringify({
level: 'error',
operation: 'createItem',
errorId: 'a1b2c3d4',
message: 'Unique constraint failed',
timestamp: '2026-01-11T12:34:56.789Z',
context: {
userId: 'user-123',
requestBody: { name: '테스트' }
}
}));
6. Before/After 비교
6.1 에러 응답
// ❌ Before: 에러 추적 불가
return NextResponse.json(
{ error: '서버 오류가 발생했습니다.' },
{ status: 500 }
);
// ✅ After: errorId로 추적 가능
return NextResponse.json(
{ error: '서버 오류가 발생했습니다.', errorId: 'a1b2c3d4' },
{ status: 500 }
);
6.2 디버깅 과정
❌ Before:
1. 사용자 문의 접수
2. 대략적인 시간대 파악
3. 해당 시간대 모든 에러 검토
4. 추측으로 원인 파악 시도
5. 디버깅 시간: 30분~1시간
✅ After:
1. 사용자가 errorId 제공
2. 로그에서 errorId 검색
3. 정확한 에러 컨텍스트 확인
4. 디버깅 시간: 1~5분
6.3 사용자 경험
❌ Before:
"서버 오류가 발생했습니다."
→ 사용자: "뭐가 잘못된 건지 모르겠네..."
✅ After:
"서버 오류가 발생했습니다.
오류 코드: a1b2c3d4
(문의 시 이 코드를 알려주세요)"
→ 사용자: 명확한 문의 가능
7. 핵심 개념 정리
7.1 errorId 생성 방법 비교
| 방법 | 예시 | 장점 | 단점 |
|---|---|---|---|
| UUID 앞 8자리 | a1b2c3d4 |
충돌 확률 매우 낮음 | 길이 고정 |
| nanoid | V1StGXR8 |
더 짧음, 커스텀 가능 | 의존성 추가 |
| 타임스탬프 기반 | 1704960896789 |
시간 정보 포함 | 길이 김 |
7.2 로그에 포함해야 할 정보
{
// 필수
errorId: string,
message: string,
timestamp: string,
// 권장
operation: string, // API 작업명
userId?: string, // 요청한 사용자
stack?: string, // 스택 트레이스
// 상황별
requestBody?: object, // 요청 데이터
searchParams?: object, // 쿼리 파라미터
headers?: object, // 관련 헤더
}
7.3 보안 고려사항
✅ 해도 되는 것:
- errorId를 클라이언트에 노출
- 일반적인 에러 메시지 표시
❌ 하면 안 되는 것:
- 스택 트레이스를 클라이언트에 노출
- 상세 에러 메시지 노출 (SQL 에러 등)
- 내부 구조 힌트 노출
8. 베스트 프랙티스
8.1 체크리스트
□ 모든 API Route에 에러 핸들러 적용
□ 500 에러에 errorId 포함
□ 서버 로그에 충분한 컨텍스트 기록
□ 클라이언트에 errorId 표시
□ 로그 검색 방법 문서화
□ 팀원에게 errorId 활용법 공유
8.2 에러 핸들러 일관성
// 모든 API에 동일한 패턴 적용
export async function POST(request: NextRequest) {
try {
// 비즈니스 로직
} catch (error) {
return handleApiError(error, {
operation: 'operationName',
// 항상 동일한 컨텍스트 구조
});
}
}
8.3 에러 모니터링 연동
// Sentry 등 에러 모니터링 서비스 연동
import * as Sentry from '@sentry/nextjs';
export function handleApiError(error: unknown, context: ErrorContext) {
const errorId = crypto.randomUUID().slice(0, 8);
// Sentry에 errorId 태그 추가
Sentry.captureException(error, {
tags: { errorId },
extra: context,
});
// ... 나머지 로직
}
9. 참고 자료
10. 다음 단계
errorId 패턴으로 에러 추적을 개선했다면, Rate Limiting과 함께 API 보안을 강화해보세요.
시리즈 목차:
- Next.js API에 Rate Limiting 구현하기 (메모리 기반)
- API 에러 추적 개선하기: errorId 패턴으로 디버깅 시간 단축 ← 현재 글
11. FAQ (자주 묻는 질문)
Q: errorId가 충돌할 가능성은?
A: UUID의 앞 8자리(32비트)를 사용하므로 약 40억 개의 조합이 가능합니다. 일반적인 서비스에서는 충돌 가능성이 극히 낮습니다. 걱정된다면 타임스탬프를 추가하거나 더 긴 ID를 사용하세요.
Q: errorId를 DB에 저장해야 하나요?
A: 선택 사항입니다. 로그만으로 충분한 경우가 많지만, 에러 통계나 장기 추적이 필요하면 별도 테이블에 저장할 수 있습니다.
Q: 400 에러에도 errorId를 붙여야 하나요?
A: 권장하지 않습니다. 400 에러는 클라이언트 입력 오류이므로 명확한 메시지로 충분합니다. 500 에러(서버 오류)에 주로 사용하세요.
Q: 이미 Sentry를 사용 중인데 필요한가요?
A: Sentry와 함께 사용하면 더 좋습니다. Sentry의 이벤트 ID는 길고 복잡하지만, 짧은 errorId는 사용자가 전달하기 쉽습니다. 둘을 연결해서 사용하세요.