프로덕션 에러 추적 세팅
프롤로그 - "저는 잘 되는데요?"
배포하고 한숨 돌렸다. 로컬에서 테스트도 다 했고, 스테이징에서도 문제없었다. 그런데 디스코드에 메시지가 왔다.
"회원가입 버튼 누르면 아무것도 안 돼요."
황급히 접속해보니 내 브라우저에서는 잘 된다. 개발자 도구 열어도 에러가 없다. 사용자한테 스크린샷을 부탁했는데, 그냥 흰 화면만 보인다. "다시 해보시겠어요?"라고 말하는 순간, 나는 깨달았다. 나는 내가 만든 앱에서 무슨 일이 벌어지는지 전혀 모르고 있었다는 걸.
프로덕션은 로컬과 다르다. 사용자의 브라우저는 내 맥북이 아니다. Safari에서는 되는데 삼성 인터넷에서는 안 된다. 한국에서는 되는데 베트남에서는 API 타임아웃이 난다. 사용자 A는 문제없는데 사용자 B는 매번 크래시한다.
console.log는 내 터미널에만 찍힌다. 사용자의 에러는 조용히 삼켜진다. 마치 우주에서 혼자 비명을 지르는 것처럼, 아무도 듣지 못한다.
결국 이해했다. 프로덕션에서는 내가 보지 못하는 것을 봐야 한다. 사용자가 신고하기 전에 에러를 알아야 한다. 그래서 Sentry를 도입했다.
Aha Moment: 실시간으로 에러가 날아온다
Sentry를 세팅하고 배포한 첫날, 슬랙 채널이 울렸다.
[Sentry] New Issue: Cannot read property 'map' of undefined
in ProfilePage.tsx:45
User: user-abc-123
Browser: Safari 15.2
5 occurrences in the last hour
아, 이게 그 버그였구나. 프로필 페이지에서 팔로워 리스트를 렌더링할 때, API 응답이 null인 케이스를 처리하지 않았다. 로컬에서는 내 계정에 팔로워가 있어서 문제가 없었지만, 신규 사용자는 빈 배열도 아닌 null을 받았던 거다.
5분 만에 고쳤다. 사용자가 신고하기 전에.
그 순간 와닿았다. 에러 추적은 단순히 로그를 보는 게 아니라, 사용자 경험을 먼저 지키는 것이었다. 마치 소방서가 화재 신고를 기다리는 게 아니라 연기 감지기로 먼저 아는 것처럼.
Sentry 세팅부터 실전까지 뜯어보기
1. Next.js에 Sentry 붙이기
가장 간단한 방법은 공식 위저드를 쓰는 거다.
npx @sentry/wizard@latest -i nextjs
근데 나는 뭐가 설치되는지 알고 싶었다. 그래서 수동으로 했다.
// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 프로덕션에서만 100% 샘플링, 개발에서는 끄기
tracesSampleRate: process.env.NODE_ENV === "production" ? 1.0 : 0,
// 배포 버전 추적 - 이게 중요하다
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
// 환경 구분
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || "development",
// 민감한 데이터 필터링
beforeSend(event, hint) {
// 로컬 개발 환경에서는 보내지 않기
if (window.location.hostname === "localhost") {
return null;
}
// 비밀번호 같은 필드 제거
if (event.request?.data) {
delete event.request.data.password;
delete event.request.data.token;
}
return event;
},
// 무시할 에러들
ignoreErrors: [
"ResizeObserver loop limit exceeded", // 브라우저 버그
"Non-Error promise rejection captured", // 라이브러리 경고
],
});
서버 사이드도 따로 설정해야 한다.
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
// 서버 에러는 더 많은 컨텍스트 필요
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
beforeSend(event) {
// 환경 변수 노출 방지
if (event.contexts?.runtime?.environment) {
delete event.contexts.runtime.environment;
}
return event;
},
});
2. Source Maps: 압축된 코드를 읽을 수 있게
프로덕션 빌드는 압축된다. 에러 스택이 t.map is not a function at r (chunk-abc.js:1:2345) 이런 식으로 나온다. 이걸 읽으려면 소스맵을 업로드해야 한다.
// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig(
{
// 기존 Next.js 설정
},
{
// Sentry 웹팩 플러그인 설정
silent: true, // 빌드 로그 조용히
org: "your-org",
project: "your-project",
// 소스맵 업로드
widenClientFileUpload: true,
// 트리 쉐이킹으로 프로덕션 번들 사이즈 줄이기
hideSourceMaps: true,
// 디버깅 정보 자동 삭제
disableLogger: true,
}
);
이제 에러가 나면 원본 TypeScript 파일과 라인 번호가 정확히 나온다.
3. Error Fingerprinting: 같은 에러는 묶어서
처음에는 같은 에러가 사용자마다 다른 이슈로 생성됐다. Sentry가 URL이나 사용자 ID로 구분해버렸기 때문이다. 이걸 해결하려면 fingerprinting이 필요했다.
Sentry.init({
beforeSend(event, hint) {
// 에러 타입과 발생 위치만으로 그룹화
if (event.exception?.values?.[0]) {
const error = event.exception.values[0];
event.fingerprint = [
error.type || "Error",
error.value || "Unknown",
error.stacktrace?.frames?.[0]?.filename || "unknown",
];
}
return event;
},
});
이제 100명이 같은 버그를 겪어도 1개의 이슈로 묶인다. 노이즈가 줄었다.
4. Custom Context: 에러 재현에 필요한 정보
단순히 에러 메시지만으로는 재현이 어렵다. 사용자의 상태를 함께 보내야 한다.
import * as Sentry from "@sentry/nextjs";
// 사용자 정보 설정 (로그인 시)
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
});
// 추가 컨텍스트
Sentry.setContext("subscription", {
plan: user.plan,
status: user.subscriptionStatus,
expiresAt: user.subscriptionExpiresAt,
});
// 특정 액션에 breadcrumb 남기기
Sentry.addBreadcrumb({
category: "payment",
message: "User clicked checkout button",
level: "info",
data: {
amount: 99,
currency: "USD",
},
});
// 수동으로 에러 캡처
try {
await processPayment();
} catch (error) {
Sentry.captureException(error, {
tags: {
payment_method: "stripe",
flow: "checkout",
},
extra: {
cartItems: cart.items.length,
totalAmount: cart.total,
},
});
}
이제 에러가 나면 "무료 플랜 사용자가 결제 버튼 눌렀을 때"처럼 컨텍스트가 함께 보인다.
5. Alerts: 슬랙으로 바로 받기
Sentry 대시보드를 계속 볼 수는 없다. 중요한 에러는 슬랙으로 알림을 받도록 설정했다.
Alert Rule 예시:
- 새로운 타입의 에러가 발생했을 때
- 같은 에러가 1시간에 10번 이상 발생했을 때
- 에러율이 5%를 넘었을 때
- 특정 태그 (
payment,auth)를 가진 에러일 때
# .sentry/alerts.yaml (선언적으로 관리)
- name: "Critical Payment Errors"
conditions:
- type: "event.type"
match: "error"
- type: "event.tag"
key: "transaction"
match: "contains"
value: "/api/payment"
actions:
- type: "slack"
workspace: "your-workspace"
channel: "#alerts-critical"
새벽 3시에 알림이 울린 적도 있다. 결제 API가 다운됐었는데, 사용자가 신고하기 전에 알아서 바로 대응했다.
6. Release Tracking: 어느 배포부터 문제였는지
Git SHA를 릴리즈 버전으로 쓰면, 배포와 에러를 연결할 수 있다.
# 배포할 때 릴리즈 생성
sentry-cli releases new $VERCEL_GIT_COMMIT_SHA
sentry-cli releases set-commits $VERCEL_GIT_COMMIT_SHA --auto
sentry-cli releases finalize $VERCEL_GIT_COMMIT_SHA
Vercel에서는 환경 변수로 자동으로 들어간다.
// sentry.client.config.ts
Sentry.init({
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
});
이제 Sentry에서 "이 에러는 커밋 abc1234 이후에만 발생했다"고 알려준다. 어떤 PR이 문제인지 바로 알 수 있다.
7. Performance Monitoring: 느린 페이지 찾기
Sentry는 에러뿐 아니라 성능도 추적한다.
Sentry.init({
tracesSampleRate: 0.1, // 10%만 샘플링 (비용 절약)
integrations: [
new Sentry.BrowserTracing({
// 특정 URL만 추적
tracePropagationTargets: [
"localhost",
/^https:\/\/yourapp\.com\/api/,
],
}),
],
});
API 호출 시간, 페이지 로드 시간, 느린 컴포넌트를 볼 수 있다. "프로필 페이지가 평균 5초 걸린다"는 걸 알고, 데이터 fetching을 최적화했다.
Sentry vs 다른 도구들 - 어떤 걸 써야 할까?
처음에는 선택지가 많아서 헷갈렸다.
Sentry: 에러 추적 전문. 오픈소스라 셀프 호스팅 가능. 무료 플랜이 넉넉하다 (월 5,000 이벤트). 스타트업 친화적. 내가 선택한 이유는 단순했다 - 빠르게 시작할 수 있고, 나중에 커스터마이징도 가능해서.
LogRocket: 세션 리플레이가 강력하다. 사용자가 클릭하고 타이핑한 걸 영상처럼 볼 수 있다. 근데 비싸다. 스타트업 초기에는 오버킬이었다.
Datadog: 인프라 모니터링까지 다 한다. 로그, 메트릭, APM, 에러 추적을 한곳에서. 근데 엔터프라이즈 가격이다. 우리는 아직 그 단계가 아니었다.
Rollbar: Sentry의 경쟁자. 비슷한데 UI가 조금 덜 직관적이었다. 커뮤니티도 Sentry가 더 크다.
결국 Sentry + Vercel Analytics + PostHog(사용자 분석)으로 정착했다. 각자 전문 분야가 달라서 겹치지 않았다.
비용 관리 - 스타트업이 부담 없이 쓰려면
Sentry 무료 플랜은 월 5,000 이벤트다. 처음에는 넉넉했는데, 트래픽이 늘자 3일 만에 한도가 찼다.
해결책:
- 샘플링: 모든 에러를 다 보낼 필요 없다. 10%만 샘플링해도 패턴은 보인다.
- 필터링:
ResizeObserver같은 브라우저 노이즈는 무시. - Performance 끄기: 에러 추적만 쓰고, 성능 모니터링은 나중에.
- 환경 분리: 스테이징 에러는 Sentry로 안 보내기.
Sentry.init({
// 에러는 100% 캡처하되, 퍼포먼스는 샘플링
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.05 : 0,
beforeSend(event) {
// rate limit 직접 구현
if (Math.random() > 0.3) { // 30%만 보내기
return null;
}
return event;
},
});
이렇게 했더니 무료 플랜으로 버틸 수 있었다.
결국 이거였다
프로덕션 에러 추적을 세팅하고 나서 달라진 것들:
- 사용자가 신고하기 전에 안다: 트위터에 불평이 올라오기 전에 이미 고쳤다.
- 재현 불가능한 버그를 잡는다: "저는 안 되는데요"가 더 이상 미스터리가 아니다. 브라우저, OS, 사용자 상태를 다 볼 수 있다.
- 배포가 무섭지 않다: 예전에는 금요일에 배포 안 했다. 지금은 뭔가 터져도 바로 알 수 있으니까 배포한다.
- 데이터 기반 우선순위: "이 버그가 100명한테 영향을 줬다"는 걸 알면 급한 게 뭔지 명확하다.
마치 자동차에 블랙박스를 다는 것과 같다. 사고가 나면 정확히 무슨 일이 있었는지 알 수 있다.
프로덕션에서 장님으로 사는 건 끔찍하다. 이제는 눈을 뜨고 운전한다. Sentry 덕분에.