Sentry 실전 세팅: 유저가 에러를 알려주기 전에 내가 먼저 알고 싶다
유저가 카카오톡으로 '안 돼요'라고 보내기 전에 에러를 먼저 감지하고 싶었다. Next.js 프로젝트에 Sentry를 연동하면서 배운 실전 설정과 알림 구성.
유저가 카카오톡으로 '안 돼요'라고 보내기 전에 에러를 먼저 감지하고 싶었다. Next.js 프로젝트에 Sentry를 연동하면서 배운 실전 설정과 알림 구성.
새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

왜 넷플릭스는 멀쩡한 서버를 랜덤하게 꺼버릴까요? 시스템의 약점을 찾기 위해 고의로 장애를 주입하는 카오스 엔지니어링의 철학과 실천 방법(GameDay)을 소개합니다.

카카오톡 메시지가 왔다. 오후 11시였다.
"저 근데 아까부터 계속 안 되는 것 같은데요?"
무엇이? 어디서? 어떻게? 아무것도 모른다. 로그를 열었다. 서버는 돌고 있다. 에러 메시지가 보이지 않는다. 재현을 시도했지만 내 환경에서는 아무 문제가 없다. 결국 유저에게 다시 물어봤다.
"어떤 화면에서요? 버튼 누를 때요, 아니면 페이지 들어갈 때요?"
"그냥 전반적으로요"
그냥. 전반적으로.
비슷한 경험이 몇 번 쌓이고 나서 결심했다. 유저가 알기 전에 내가 먼저 알아야 한다. 감으로 디버깅하는 시대는 끝내야 했다. Sentry를 제대로 세팅하기로 했다.
처음엔 Sentry를 "에러 로거 좀 더 예쁜 거" 정도로 생각했다. 아니었다.
Sentry는 애플리케이션 CCTV다. 매장에 CCTV를 달아놓으면, 도난 사고가 발생했을 때 녹화 영상을 돌려볼 수 있다. 누가, 언제, 어떤 경로로 들어왔는지 전부 추적된다. Sentry도 똑같다. 에러가 발생하면 그 순간까지의 유저 행동 경로, 브라우저 정보, 어떤 함수에서 터졌는지, 심지어 에러 직전에 발생한 네트워크 요청까지 기록해둔다.
비행기엔 블랙박스가 있다. 사고가 나면 블랙박스를 꺼내 마지막 비행 기록을 분석한다. Sentry의 Breadcrumbs 기능이 이 블랙박스에 해당한다. 에러가 터지기 직전까지 유저가 뭘 클릭했고, 어떤 API를 호출했고, 콘솔에 뭐가 찍혔는지가 순서대로 저장된다.
핵심 기능을 정리하면 이렇다.
| 기능 | 설명 |
|---|---|
| Error Capture | 예외를 자동으로 수집하고 스택 트레이스 제공 |
| Breadcrumbs | 에러 직전 유저 행동 경로 기록 |
| Source Maps | 압축된 프로덕션 코드를 원본 코드로 매핑 |
| Performance Monitoring | 느린 트랜잭션, N+1 쿼리 감지 |
| Alerting | Slack/Discord/이메일로 실시간 알림 |
| Replays | 에러 발생 시점 유저 세션 영상 재생 |
Sentry에는 Next.js 전용 SDK가 있다. @sentry/nextjs 패키지다. 공식 마법사(npx @sentry/wizard)를 쓰면 파일을 자동으로 만들어주는데, 자동 생성된 파일을 그냥 두면 나중에 뭐가 뭔지 모르게 된다. 직접 하나씩 이해하면서 세팅하는 게 낫다고 판단했다.
npm install @sentry/nextjs
설치 후 세 개의 설정 파일이 필요하다.
sentry.client.config.ts — 브라우저에서 실행되는 클라이언트 사이드 설정이다.
// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 프로덕션에서만 에러 리포팅 활성화
enabled: process.env.NODE_ENV === "production",
// 샘플링 비율: 1.0 = 100% (트래픽 많으면 낮춰야 한다)
tracesSampleRate: 0.1,
// 세션 리플레이: 에러 발생 시 100%, 평소엔 10%
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
integrations: [
Sentry.replayIntegration({
// 개인정보 마스킹: input과 텍스트 자동 마스킹
maskAllText: true,
blockAllMedia: false,
}),
],
// 노이즈 제거: 이 에러들은 무시한다
ignoreErrors: [
"ResizeObserver loop limit exceeded",
"Non-Error promise rejection captured",
/^Network request failed/,
/^ChunkLoadError/,
],
beforeSend(event) {
// 로컬호스트에서 발생한 에러는 무시
if (event.request?.url?.includes("localhost")) {
return null;
}
return event;
},
});
sentry.server.config.ts — Node.js 서버에서 실행되는 설정이다.
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
enabled: process.env.NODE_ENV === "production",
tracesSampleRate: 0.1,
// 서버 에러는 더 많이 캡처한다
beforeSend(event, hint) {
const error = hint.originalException;
// 404는 에러가 아니다. Sentry로 보내지 않는다
if (error instanceof Error && error.message.includes("NEXT_NOT_FOUND")) {
return null;
}
// 민감한 정보 제거
if (event.request?.cookies) {
event.request.cookies = "[Filtered]";
}
return event;
},
});
sentry.edge.config.ts — Next.js Edge Runtime (미들웨어) 설정이다.
// sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
enabled: process.env.NODE_ENV === "production",
tracesSampleRate: 0.05, // Edge는 요청량이 많아서 낮게 설정
});
설정 파일만 만든다고 끝이 아니다. next.config.ts에 withSentryConfig를 감싸야 소스맵 업로드와 빌드 타임 연동이 된다.
// next.config.ts
import { withSentryConfig } from "@sentry/nextjs";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// 기존 설정
};
export default withSentryConfig(nextConfig, {
// Sentry 조직과 프로젝트 정보
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
// 소스맵: 프로덕션 번들에서 제거하되 Sentry에만 업로드
sourcemaps: {
deleteSourcemapsAfterUpload: true,
},
// 빌드 로그 최소화
silent: !process.env.CI,
// 자동 계측 비활성화 (필요한 것만 쓴다)
autoInstrumentServerFunctions: true,
autoInstrumentMiddleware: true,
autoInstrumentAppDirectory: true,
});
deleteSourcemapsAfterUpload: true가 중요하다. 소스맵을 Sentry에 업로드한 뒤 프로덕션 번들에서 삭제한다. 소스맵이 클라이언트에 노출되면 원본 코드가 보이기 때문에 반드시 삭제해야 한다.
자동 에러 캡처만으론 부족한 경우가 있다. 예외가 발생하지 않지만 비정상적인 상태를 Sentry에 기록하고 싶을 때, 혹은 특정 에러에 컨텍스트를 추가하고 싶을 때다.
// lib/sentry.ts - 공통 유틸리티
import * as Sentry from "@sentry/nextjs";
// 유저 컨텍스트 설정
export function setSentryUser(user: { id: string; email?: string }) {
Sentry.setUser({
id: user.id,
// 이메일은 해시 처리하거나 아예 빼는 게 낫다 (GDPR)
email: user.email ? "[filtered]" : undefined,
});
}
// 구조화된 에러 캡처
export function captureError(
error: unknown,
context?: Record<string, unknown>
) {
Sentry.withScope((scope) => {
if (context) {
scope.setContext("additional", context);
}
Sentry.captureException(error);
});
}
// 에러가 아니지만 주목할 이벤트 기록
export function captureWarning(message: string, data?: Record<string, unknown>) {
Sentry.captureMessage(message, {
level: "warning",
extra: data,
});
}
React 컴포넌트에서는 Error Boundary를 직접 구성했다.
// components/error-boundary.tsx
"use client";
import * as Sentry from "@sentry/nextjs";
import { Component, ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
eventId?: string;
}
export class SentryErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
const eventId = Sentry.captureException(error, {
extra: { componentStack: errorInfo.componentStack },
});
this.setState({ eventId });
}
render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div className="error-container">
<p>문제가 발생했습니다. 팀에 자동으로 보고되었습니다.</p>
{this.state.eventId && (
<p className="text-sm text-muted-foreground">
이벤트 ID: {this.state.eventId}
</p>
)}
</div>
)
);
}
return this.props.children;
}
}
captureException 반환값이 eventId다. 이 ID를 유저에게 보여주면, 나중에 유저가 "아까 그 에러요"라고 할 때 Sentry에서 정확히 그 이벤트를 찾을 수 있다. 고객 지원 플로우에 유용하다.
Sentry 알림을 처음 켜면 슬랙이 폭발한다. 별것 아닌 에러부터 실제 장애까지 전부 알림이 오기 때문에, 결국 알림을 꺼버리게 된다. 이렇게 되면 CCTV를 달아놓고 모니터를 꺼놓은 것과 같다.
Sentry 대시보드의 Alerts 설정에서 알림 규칙을 세밀하게 조정했다.
내가 실제로 쓰는 규칙은 세 가지다.
규칙 1: 새로운 에러 발생 (즉시 알림)처음 보는 에러 유형이 발생하면 바로 알림을 받는다. 기존에 알려진 에러가 또 발생하는 건 알림이 오지 않는다. 새로운 에러만 감지한다.
A new issue is createdIssue is older than 0 minutes (신규만)#errors-critical이미 알고 있는 에러라도 갑자기 발생 빈도가 급증하면 뭔가 잘못된 것이다.
The issue occurs more than 100 times in 1 hour#errors-critical + 이메일당장 급하진 않지만 주기적으로 상태를 확인하고 싶다.
#errors-dailySlack 연동은 Sentry 대시보드 Settings → Integrations → Slack에서 설정한다. 워크스페이스를 연결하고 채널을 지정하면 된다. Discord 연동도 동일한 방식으로 가능하다.
소스맵 없이 Sentry를 보면 이런 스택 트레이스가 나온다.
Error: Cannot read properties of undefined (reading 'map')
at t.default (main-abc123.js:1:28491)
at e (chunk-xyz789.js:1:14832)
숫자만 있고 파일명도, 함수명도, 줄 번호도 없다. 아무 의미가 없다. withSentryConfig에서 소스맵 업로드를 활성화해야 비로소 아래처럼 보인다.
Error: Cannot read properties of undefined (reading 'map')
at PostList (src/components/blog/PostList.tsx:42:18)
at BlogPage (src/app/[locale]/blog/page.tsx:28:12)
이 차이가 디버깅 시간을 몇 시간에서 몇 분으로 줄여준다.
나중에 "아, 이 필드에 개인정보가 들어갔네"를 발견하면 이미 늦다. 처음부터 규칙을 정해두는 게 낫다.
Sentry에는 Data Scrubbing 기능이 있다. 프로젝트 설정에서 password, token, email, credit_card 같은 키워드를 지정하면 해당 필드를 자동으로 마스킹한다. beforeSend 훅에서 직접 처리하는 것과 병행해서 쓴다.
tracesSampleRate: 1.0으로 설정하면 모든 트랜잭션을 Sentry로 보낸다. 트래픽이 적을 때는 괜찮다. 그런데 유저가 늘어나면 Sentry 요금이 같이 올라간다. 처음엔 0.1 (10%) 정도로 시작하고, 필요하면 올리는 방식이 낫다.
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV, // "production" | "development"
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, // 배포 커밋
});
environment 태그를 설정하면 Sentry 대시보드에서 프로덕션 에러만 필터링해서 볼 수 있다. release를 커밋 SHA로 설정하면 어느 배포 버전에서 에러가 처음 발생했는지 추적된다. 회귀 에러를 잡을 때 결정적이다.
지금은 유저가 카카오톡을 보내기 전에 내가 먼저 알림을 받는다. Slack 채널에 에러 이벤트가 뜨면 이미 재현 경로, 스택 트레이스, Breadcrumbs, 영향받은 유저 수까지 다 나와 있다. "그냥 전반적으로요"를 들어도 이제 무엇을 봐야 하는지 안다.
소스맵 없이는 반쪽짜리다. withSentryConfig의 deleteSourcemapsAfterUpload는 필수다.
beforeSend로 노이즈를 먼저 잘라낸다. 404, localhost 에러, 네트워크 실패는 처음부터 필터링한다.
알림 규칙을 세 단계로 나눈다. 신규 에러, 급증 에러, 일일 요약. 전부 같은 채널로 오면 결국 알림을 끄게 된다.
PII 필터링은 첫날부터. password, token, email은 beforeSend와 Data Scrubbing을 함께 써서 두 번 막는다.
captureException의 반환 eventId를 유저에게 보여준다. 고객 문의가 왔을 때 ID 하나로 해당 에러를 즉시 찾는다.
Sentry는 레이더다. 레이더 없이 비행하면 산이 어디 있는지 감으로 피해야 한다. 레이더가 있으면 미리 우회할 수 있다. 유저가 "안 돼요"를 보내기 전에, 내가 먼저 알고 이미 고쳐두는 것. 그게 목표였고, 이제는 된다.