프로덕션에서 런타임 에러가 나면, 사용자가 스크린샷을 보내기 전에 스택과 환경을 먼저 잡는 쪽이 복구가 빠릅니다. Sentry(센트리)는 프론트·서버 예외를 이슈로 묶고, 릴리스·소스맵·알림까지 한 흐름으로 붙일 수 있는 에러 모니터링 도구입니다.
이 튜토리얼은 Next.js(App Router) 기준입니다. CLI 설치 → 설정 파일 → 테스트 이벤트 → 사용자 컨텍스트 → 소스맵·릴리스 → 슬랙 알림 순서입니다. 로그 필드 설계는 구조화 로깅 체크리스트와 같이 쓰면 원인 추적이 한결 수월합니다.
로컬에서는 console.error만으로도 충분합니다. 문제는 배포 후입니다. 브라우저·OS·버전·세션이 제각각이라 재현이 어렵고, 서버 로그에 스택이 잘려 있으면 원인 후보만 늘어납니다.
센트리에 보내는 값은 대략 이렇게 나뉩니다.
| 구분 |
콘솔·서버 로그 |
센트리 |
| 스택 트레이스 | 환경마다 형식 다름 | 이슈 단위로 묶임 |
| 사용자 컨텍스트 | 직접 붙여야 함 | user·tags·extra 표준 |
| 릴리스 대응 | 버전 문자열 수작업 | release + 소스맵 연결 |
| 알림 | 별도 파이프 필요 | 이슈 규칙·슬랙·메일 |
| 비용 감각 | 로그 저장량 | 이벤트·성능 트랜잭션 한도 |
단위 테스트·E2E로 막는 버그와, 프로덕션에서만 터지는 예외는 겹치지 않습니다. Vitest와 Playwright로 회귀를 줄이고, 센트리로 남은 런타임 구멍을 메우는 조합이 실무에서 많이 쓰입니다.
1단계 | Next.js 프로젝트에 센트리 붙이기
공식 권장은 위저드입니다. DSN·프로젝트 연결·설정 파일 생성을 한 번에 처리합니다. 계정이 없으면 sentry.io에서 조직을 만든 뒤 진행합니다.
Node 18 이상, Next.js 13+ App Router를 전제로 합니다. 패키지 매니저는 npm·pnpm·yarn 모두 가능합니다.
센트리 Next.js 위저드 실행
# 프로젝트 루트에서
npx @sentry/wizard@latest -i nextjs
# 위저드가 묻는 항목 예시
# - Login / org / project 선택
# - Error Monitoring: Yes
# - Session Replay: 팀 정책에 맞게 (PII 주의)
# - Tracing(성능): 초기엔 sample rate 낮게
# - CI 업로드(소스맵): 가능하면 Yes
위저드가 끝나면 대략 아래 파일이 생깁니다. 버전에 따라 경로 이름이 조금 다를 수 있으니 생성된 목록을 기준으로 확인하세요.
sentry.client.config.ts — 브라우저 SDK
sentry.server.config.ts — 서버(Node) SDK
sentry.edge.config.ts — 미들웨어·Edge 런타임
instrumentation.ts — 서버 부트 시 훅
next.config 에 withSentryConfig 래핑
환경 변수는 로컬 .env.local에 두고, 배포 환경(버셀 등)에는 대시보드 값으로 넣습니다. DSN은 공개되어도 이벤트 수신용이지만, 소스맵 업로드 토큰은 비밀로 취급합니다.
환경 변수 예시 (.env.local)
# 공개 DSN (클라이언트에도 노출 가능)
NEXT_PUBLIC_SENTRY_DSN=https://xxxx@o0.ingest.sentry.io/0
# 소스맵 업로드용 (서버/CI 전용, 커밋 금지)
SENTRY_AUTH_TOKEN=sntrys_...
SENTRY_ORG=your-org-slug
SENTRY_PROJECT=your-project-slug
# 릴리스 이름 (선택, 빌드 파이프라인에서 주입 권장)
SENTRY_RELEASE=web@1.4.2+abc1234
센트리 위저드 실행 후 DSN·릴리스 환경 변수를 먼저 고정한다샘플링을 1.0으로 두면 무료 한도·노이즈가 금방 찹니다. 초기에는 에러는 전부, 성능 트레이스는 낮은 비율로 두는 편이 안전합니다. 프로덕션 배포 전 Next.js 프로덕션 체크리스트의 환경 분리 항목과 맞춰 두세요.
sentry.client.config.ts 최소 예
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
// 개발 중 전송을 끄려면 true
enabled: process.env.NODE_ENV === 'production',
tracesSampleRate: 0.1,
// 세션 리플레이는 동의·마스킹 정책 확정 후
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0.2,
beforeSend(event) {
// 이메일·토큰 등 민감 필드 제거 훅
if (event.request?.headers) {
delete event.request.headers['authorization']
}
return event
},
})
sentry.server.config.ts 최소 예
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
enabled: process.env.NODE_ENV === 'production',
tracesSampleRate: 0.1,
// 서버에서 잡힌 예외에 공통 태그
initialScope: {
tags: { runtime: 'nodejs' },
},
})
주의: enabled: false를 로컬에만 켜 두면, "설정은 됐는데 이벤트가 안 온다"로 착각하기 쉽습니다. 검증 단계에서는 잠시 enabled: true로 두고 테스트 이벤트를 보낸 뒤, 다시 프로덕션 조건으로 되돌리세요.
대시보드에 이슈가 보이기 전까지는 "연동 완료"로 보지 않는 편이 좋습니다. 클라이언트 버튼 한 번, 서버 라우트 한 번, 두 경로 모두 확인합니다.
클라이언트 테스트 버튼 (App Router)
'use client'
import * as Sentry from '@sentry/nextjs'
export function SentryTestButton() {
return (
<button
type="button"
onClick={() => {
Sentry.captureException(new Error('sentry client test error'))
// 또는 throw new Error('unhandled client test')
}}
>
센트리 클라이언트 테스트
</button>
)
}
서버 라우트 테스트 (app/api/sentry-test/route.ts)
import * as Sentry from '@sentry/nextjs'
import { NextResponse } from 'next/server'
export async function GET() {
try {
throw new Error('sentry server test error')
} catch (err) {
Sentry.captureException(err)
await Sentry.flush(2000)
return NextResponse.json({ ok: false, captured: true }, { status: 500 })
}
}
이벤트가 안 보이면 순서대로 점검합니다.
- DSN이 현재 프로젝트와 일치하는가
enabled / NODE_ENV 조건이 전송을 막고 있지 않은가
- AdBlock·브라우저 확장이 ingest 도메인을 막지 않는가
- 서버라면
flush 없이 프로세스가 바로 종료되지 않았는가
- 조직의 이벤트 쿼터·스팸 필터에 걸리지 않았는가
검증용 라우트와 버튼은 프로덕션 배포 전에 제거하거나, 관리자 전용·feature flag 뒤로 숨기세요.
테스트 이벤트 수신 후 이슈 상세에서 스택·환경·릴리스를 확인한다같은 예외라도 "누가, 어느 테넌트에서, 어떤 화면 직후"인지 알면 우선순위가 갈립니다. 로그인 직후 user를 세팅하고, 결제·업로드 같은 중요 흐름에는 태그를 붙입니다. 비밀번호·토큰·주민번호는 절대 올리지 마세요.
로그인 후 setUser / setTag 예
import * as Sentry from '@sentry/nextjs'
export function identifySentryUser(user: {
id: string
email?: string
plan?: string
}) {
Sentry.setUser({
id: user.id,
// 이메일은 개인정보 정책에 따라 해시만 권장
email: user.email,
})
if (user.plan) {
Sentry.setTag('plan', user.plan)
}
Sentry.setContext('app', {
locale: 'ko-KR',
surface: 'web',
})
}
export function clearSentryUser() {
Sentry.setUser(null)
}
브레드크럼으로 직전 행동 남기기
import * as Sentry from '@sentry/nextjs'
export async function onCheckoutClick(cartId: string) {
Sentry.addBreadcrumb({
category: 'checkout',
message: 'checkout button clicked',
level: 'info',
data: { cartId },
})
try {
// await startCheckout(cartId)
} catch (err) {
Sentry.captureException(err, {
tags: { flow: 'checkout' },
extra: { cartId },
})
throw err
}
}
구조화 로그의 request_id·trace_id를 센트리 태그에도 동일하게 넣으면, 로그 검색과 이슈 화면을 오가며 볼 수 있습니다. 필드 이름 통일은 프로덕션 구조화 로깅 쪽 규칙을 그대로 따르면 됩니다.
5단계 | 소스맵·릴리스로 읽히는 스택 만들기
minify된 한 줄 스택은 사실상 무용입니다. 빌드 시 소스맵을 업로드하고, 런타임 이벤트에 같은 release 문자열을 넣어야 원본 파일·라인으로 풀립니다. 업로드 토큰은 CI 시크릿에만 둡니다.
withSentryConfig 래핑 스케치 (next.config)
const { withSentryConfig } = require('@sentry/nextjs')
/** @type {import('next').NextConfig} */
const nextConfig = {
// 기존 설정...
}
module.exports = withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.CI,
widenClientFileUpload: true,
// 소스맵을 클라이언트에 공개하지 않으려면 hideSourceMaps 등 옵션 확인
})
GitHub Actions에서 릴리스 이름 주입 예
# .github/workflows/deploy.yml 일부
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: your-org
SENTRY_PROJECT: your-project
SENTRY_RELEASE: web@${{ github.sha }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
env:
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ env.SENTRY_ORG }}
SENTRY_PROJECT: ${{ env.SENTRY_PROJECT }}
SENTRY_RELEASE: ${{ env.SENTRY_RELEASE }}
CI 워크플로우 구조는 GitHub Actions CI/CD 가이드와 같은 패턴으로 확장하면 됩니다. Docker 이미지로 빌드할 때는 멀티스테이지에서 업로드 토큰이 최종 이미지 레이어에 남지 않게 멀티스테이지 빌드 경계를 지키세요.
릴리스 화면에서 커밋 연결·배포 시각을 남기면, "이 배포 직후 이슈가 폭증했는지"를 바로 볼 수 있습니다.
빌드 산출물과 같은 release 문자열로 소스맵을 올려야 스택이 원본으로 풀린다모든 이슈를 슬랙에 때리면 곧 무시됩니다. 심각도·빈도·신규 여부를 나눠 알림을 줄이는 쪽이 오래 갑니다.
| 신호 |
추천 대응 |
알림 |
| 신규 이슈 (first seen) | 담당 로테이션이 1차 분류 | 즉시 (업무 채널) |
| 동일 이슈 급증 | 배포 롤백·킬스위치 검토 | 즉시 (온콜) |
| 브라우저 확장 노이즈 | inbound filter·ignore | 끄기 |
| 예상된 4xx 비즈니스 오류 | capture 제외 또는 레벨 info | 끄기 |
| 주간 다발·저영향 | 백로그 티켓으로 묶기 | 일일/주간 다이제스트 |
이슈 상태를 Resolved / Ignored / Regressed로 운영하면, 재발 시 다시 알림이 뜹니다. "고쳤다"고 닫은 뒤 같은 스택이 돌아오면 배포 회귀 신호로 쓰면 됩니다.
- 로컬에서만 테스트하고 프로덕션 DSN을 안 넣음 — 환경별 DSN 또는 동일 DSN + environment 태그를 확인합니다.
- 소스맵은 올렸는데 release 문자열이 다름 — 빌드 env와 런타임 env의 릴리스 이름을 한 변수로 맞춥니다.
- 서버리스에서 flush 누락 — 핸들러 종료 전
await Sentry.flush()를 넣습니다.
- PII가 extra에 그대로 실림 —
beforeSend에서 마스킹하고, 폼 본문 전체를 올리지 않습니다.
- 성능 트랜잭션 샘플 100% — 트래픽 있는 서비스는 비용·노이즈가 급증합니다. 0.05~0.2부터 올립니다.
보안: 소스맵 업로드 토큰, 내부 API 키, 세션 쿠키는 이벤트에 넣지 마세요. 클라이언트 번들에 시크릿이 섞이면 센트리보다 먼저 공급망·탈취 위험이 커집니다.
배포 전 체크리스트
참고 자료
자주 묻는 질문
Sentry 무료 플랜으로 시작해도 되나?
개인·사이드 프로젝트는 무료 티어 이벤트로 충분한 경우가 많습니다. 트래픽이 늘면 이벤트·리플레이·성능 트랜잭션 한도를 먼저 보고, 샘플링과 필터로 줄인 뒤 유료를 검토하면 됩니다. 정확한 한도는 가입 시점 요금 페이지를 확인하세요.
Next.js App Router에서 서버 컴포넌트 에러도 잡히나?
서버 SDK와 instrumentation이 올바르게 로드되면 서버 예외도 전송됩니다. 다만 스트리밍·캐시 경계에서 삼켜진 오류는 프레임워크 동작에 따라 누락될 수 있어, 중요 경로는 try/catch 후 captureException을 명시하는 편이 안전합니다.
소스맵을 올리면 소스 코드가 외부에 공개되나?
업로드된 소스맵은 센트리 프로젝트 권한 있는 구성원 중심으로 쓰입니다. 다만 빌드 산출물을 웹에 그대로 노출하거나, 토큰이 유출되면 위험이 커집니다. 공개 디렉터리에 .map을 두지 말고, auth token은 CI 시크릿만 사용하세요.
console.error만 보내도 되나?
captureException·captureMessage API를 쓰는 편이 이슈 그룹핑에 유리합니다. console 연동 옵션을 켜면 노이즈가 늘 수 있어, 초기에는 예외 중심 + 중요 경로 수동 capture를 권장합니다.
세션 리플레이는 꼭 켜야 하나?
재현이 어려운 UI 버그에 도움이 되지만, 화면·입력 마스킹과 개인정보 동의가 필요합니다. 에러 순간의 리플레이만 낮은 비율로 켜고, 결제·인증 화면은 차단 목록에 넣는 운영이 흔합니다.
Datadog·New Relic 대신 Sentry만 쓰면 되나?
역할이 겹치면서도 다릅니다. 센트리는 이슈·릴리스 단위 디버깅에 강하고, 인프라 메트릭·APM 전체는 다른 스택과 병행하는 팀이 많습니다. 이미 로그·메트릭 도구가 있다면 센트리 이벤트의 trace id를 맞춰 교차 조회하는 구성이 실용적입니다.