에러가 터져도 몰랐다. 사용자가 CS팀에 "화면이 안 떠요"라고 문의하면, CS팀이 슬랙에 올리고, 개발팀이 재현하려고 이것저것 시도해보고, 결국 콘솔에서 에러를 발견하는 흐름이었다. 사용자가 문의하지 않으면? 그냥 조용히 묻혔다.
이 흐름을 바꾸기로 했다. 에러가 발생하면 개발팀이 사용자보다 먼저 알아야 한다.
Sentry 도입
에러 모니터링 서비스는 여러 가지가 있는데 Sentry를 골랐다. 이유는 단순하다. React, Next.js 공식 SDK가 잘 되어 있고, 소스맵 연동이 편하고, 무료 플랜으로 시작할 수 있다.
Next.js에 Sentry를 붙이는 건 @sentry/nextjs 하나면 된다:
npx @sentry/wizard@latest -i nextjs위저드가 sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts, next.config.js 설정을 자동으로 만들어준다.
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1, // 성능 트레이싱 10%만 샘플링
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1.0, // 에러 발생 시 세션 리플레이 100% 수집
});tracesSampleRate를 1.0으로 하면 모든 요청의 성능 데이터를 수집하는데, 프로덕션에서는 비용과 성능에 영향을 준다. 0.1~0.2 정도가 적당하다.
소스맵 연동
프로덕션 빌드는 코드가 minify되어 있어서 에러 스택트레이스가 이렇게 보인다:
TypeError: Cannot read properties of undefined (reading 'map')
at a.render (main-abc123.js:1:23456)
어디서 터진 건지 전혀 모른다. 소스맵을 Sentry에 업로드하면 원본 코드 위치로 매핑해준다:
TypeError: Cannot read properties of undefined (reading 'map')
at PostList (src/components/PostList.tsx:24:18)
@sentry/nextjs의 webpack 플러그인이 빌드 시 자동으로 소스맵을 업로드한다. next.config.js에서 withSentryConfig로 감싸면 끝:
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(nextConfig, {
sourcemaps: {
deleteSourcemapsAfterUpload: true, // 업로드 후 소스맵 삭제 (보안)
},
});소스맵을 프로덕션 서버에 남겨두면 누구나 원본 코드를 볼 수 있다. deleteSourcemapsAfterUpload: true로 Sentry에만 업로드하고 서버에서는 삭제하자.
노이즈 필터링
Sentry를 켜자마자 에러가 쏟아졌다. 하루에 수백 건. 대부분은 의미 없는 노이즈였다.
- 크롬 확장 프로그램이 주입한 스크립트 에러
- 봇 크롤러의 JavaScript 실행 실패
- 네트워크 순단으로 인한 fetch 실패
- 사용자가 탭을 닫으면서 발생하는 AbortError
이것들을 걸러내지 않으면 진짜 중요한 에러가 묻힌다.
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
ignoreErrors: [
'AbortError',
'ResizeObserver loop',
'Non-Error promise rejection',
/^Loading chunk \d+ failed/,
/^Network request failed/,
],
beforeSend(event) {
// 크롬 확장 프로그램에서 발생한 에러 필터링
const frames = event.exception?.values?.[0]?.stacktrace?.frames;
if (frames?.some((frame) => frame.filename?.includes('extension://'))) {
return null;
}
return event;
},
});ignoreErrors로 패턴 매칭하고, beforeSend에서 더 세밀한 필터링을 했다. 이걸로 노이즈의 80% 정도가 사라졌다.
의미 있는 컨텍스트 추가
에러 메시지만으로는 원인을 파악하기 어려울 때가 많다. "Cannot read properties of null"이 뜨면, 뭐가 null인데? 어떤 상황에서?
사용자 컨텍스트와 브레드크럼을 추가했다:
// 로그인 후 사용자 정보 설정
Sentry.setUser({
id: user.id,
segment: user.plan, // free, pro, enterprise
});
// 중요한 액션에 브레드크럼 추가
function handlePayment(amount: number) {
Sentry.addBreadcrumb({
category: 'payment',
message: `결제 시도: ${amount}원`,
level: 'info',
});
// ...결제 로직
}에러가 발생하면 Sentry 대시보드에서 "이 사용자가 어떤 경로로 이 페이지에 왔고, 어떤 버튼을 눌렀고, 어떤 API를 호출했는지" 타임라인으로 볼 수 있다.
슬랙 알림 파이프라인
Sentry에 에러가 쌓이는 것만으로는 부족하다. 팀이 Sentry 대시보드를 매일 확인할 리가 없으니까.
Sentry의 Alert Rules를 설정해서 슬랙으로 알림이 오게 했다:
- 새로운 이슈 발생: 처음 보는 에러가 생기면 즉시 알림
- 이슈 회귀: 해결했던 에러가 다시 발생하면 알림
- 빈도 임계값: 같은 에러가 1시간에 50건 이상이면 알림 (대규모 장애 감지)
모든 에러를 다 슬랙에 보내면 채널이 묘지가 된다. "새로운 이슈"와 "회귀"만 보내는 게 핵심이다. 이미 알고 있는 에러는 Sentry에서 확인하면 된다.
에러 바운더리와 연동
React Error Boundary에서 Sentry에 추가 정보를 보내도록 했다:
import * as Sentry from '@sentry/nextjs';
class ErrorBoundary extends React.Component<Props, State> {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
Sentry.withScope((scope) => {
scope.setTag('boundary', this.props.name);
scope.setExtra('componentStack', errorInfo.componentStack);
Sentry.captureException(error);
});
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
어떤 Error Boundary에서 잡혔는지 태그로 남기니까, "결제 섹션에서 에러가 집중되고 있다" 같은 패턴을 빠르게 파악할 수 있었다.
도입 후
에러 발생부터 인지까지의 시간이 "CS 문의가 올 때까지"에서 "수 분 이내"로 줄었다. 배포 직후 에러가 급증하면 바로 알 수 있으니까 빠른 롤백 판단도 가능해졌다.
가장 큰 변화는 팀의 태도다. 예전에는 "에러가 있을 수도 있다" 정도의 막연한 불안이었다면, 이제는 "현재 미해결 이슈가 3건이고, 모두 낮은 빈도"라는 정량적인 상태 파악이 가능하다. 모르는 것보다 아는 게 낫다.
