Sentry로 프로덕션 에러 추적하기: 에러 캡처, 성능 모니터링, Source Maps, 알림, 릴리스 추적

이 글의 핵심

프로덕션 에러는 스택 트레이스가 압축된 코드 기준으로 찍히고 어느 배포에서 시작됐는지도 알기 어려워 원인 파악이 늦어집니다. Sentry가 에러에 문맥을 붙이는 방식과 Datadog·CloudWatch와의 차이를 보고, Source Maps와 Release Tracking으로 원본 코드와 배포 버전을 연결한 뒤 개인정보 처리 주의점까지 짚습니다.

이 글의 핵심

Next.js에 Sentry를 붙여 프로덕션 에러를 추적하는 방법을 다룹니다. SDK 설정, 자동·수동 에러 캡처와 컨텍스트, 성능 추적, Source Maps 업로드, 알림, 릴리스 연결까지 예제로 보고, 설정해 놓고도 “스택 트레이스가 압축된 코드로 나온다”, “알림이 너무 많다” 같은 흔한 문제의 원인을 함께 설명합니다. 코드는 현재 SDK(v8 이상) 기준입니다.

Sentry를 도입하게 되는 상황

사용자가 겪은 에러를 개발자가 모르고 있어요

서버 에러는 로그라도 남지만, 브라우저에서 난 JavaScript 에러는 사용자가 신고하지 않으면 아무 기록도 남지 않습니다. Sentry SDK는 전역 에러 핸들러에 연결되어 처리되지 않은 예외를 자동으로 수집하고, 새로운 종류의 에러가 생기면 알림을 보냅니다.

에러 메시지만으로는 재현이 안 돼요

TypeError: Cannot read properties of undefined (reading 'name') 한 줄로는 어느 화면에서 어떤 데이터로 발생했는지 알 수 없습니다. Sentry는 에러와 함께 브라우저·OS, 직전의 사용자 행동, 네트워크 요청, 사용자 ID 같은 문맥을 묶어 보냅니다.

어느 배포부터 느려졌는지 모르겠어요

성능 추적을 켜면 페이지 로드와 API 요청 시간이 트랜잭션 단위로 기록되고, 릴리스 정보와 연결하면 “이 배포 이후 결제 API가 느려졌다”를 바로 확인할 수 있습니다.


Sentry란?

에러 추적 플랫폼의 역사 (2008~)

Sentry는 2008년 David Cramer가 Django 애플리케이션의 에러를 모으기 위해 만든 오픈소스 도구(django-sentry)로 시작했습니다. 당시 프로덕션 에러는 서버에 SSH로 접속해 로그 파일을 grep하는 방식으로 찾았고, 사용자가 겪은 에러를 개발자가 모르는 경우가 많았습니다.

Sentry는 이 문제를 “에러가 발생하면 즉시 알리고, 재현에 필요한 정보를 자동으로 수집한다”는 방식으로 해결했습니다. 이후 회사로 설립되어 여러 언어 SDK, Source Maps를 통한 압축 코드 추적, 성능 모니터링, Session Replay까지 기능을 넓혔습니다. 서버는 지금도 소스가 공개되어 있어 셀프 호스팅할 수 있지만, 구성 요소가 많아(Kafka, ClickHouse, Redis 등) 운영 부담이 커서 대부분은 SaaS를 씁니다.

Sentry의 철학: “에러는 문맥 없이는 해결할 수 없다”

로그 파일에 TypeError: Cannot read property 'name' of undefined만 있으면 재현이 어렵습니다. Sentry는 에러와 함께 문맥을 수집합니다:

  • 사용자: 어떤 유저가 겪었나 (ID, 세션)
  • 환경: 브라우저·OS·디바이스
  • 경로: 에러 직전 사용자 행동(Breadcrumbs) — 클릭, 페이지 이동, 콘솔 로그, fetch 요청
  • 상태: 개발자가 직접 붙인 컨텍스트(주문 ID, 기능 플래그 등)
  • 스택: Source Maps로 복원한 원본 코드 위치

또 하나 중요한 기능은 그룹핑입니다. 같은 버그로 에러가 1만 번 발생해도 Sentry는 스택 트레이스를 기준으로 하나의 “이슈”로 묶고 발생 횟수와 영향받은 사용자 수를 보여 줍니다. 로그 검색과 가장 크게 다른 점이 이것입니다. 개발자는 1만 줄의 로그가 아니라 “사용자 350명에게 영향을 준 이슈 1개”를 보게 되고, 그 이슈를 해결 처리했는데 다음 배포에서 다시 발생하면 회귀(regression)로 다시 알림을 받습니다.

Sentry vs Datadog vs CloudWatch

측면SentryDatadogAWS CloudWatch
강점에러 추적·이슈 그룹핑APM·인프라·로그 통합AWS 네이티브
무료개발자 플랜(월 5K 에러 수준)체험판기본 지표 무료
Source Maps✅ 빌드 플러그인으로 자동 업로드✅ CLI로 업로드❌
Session Replay✅✅ (RUM)❌
가격팀 플랜 월 $26~호스트·기능별 과금종량제
사용 시나리오프론트엔드·백엔드 에러인프라 전체 관측AWS 중심 서비스

선택 기준:

  • 애플리케이션 에러 추적이 핵심 → Sentry (이슈 그룹핑·Source Maps·릴리스 회귀 추적)
  • 인프라·메트릭·로그를 한 도구로 → Datadog (서버·DB·네트워크를 한눈에)
  • AWS 서비스 위주, 추가 도구 최소화 → CloudWatch (Lambda·EC2·RDS 통합)

세 도구는 경쟁 관계라기보다 겹치는 영역이 일부 있는 관계라, 인프라는 Datadog이나 Prometheus로, 애플리케이션 에러는 Sentry로 나눠 쓰는 조합도 흔합니다. 가격은 이벤트 수에 비례하므로 도구를 고를 때 “월 에러 이벤트가 몇 건인가”를 먼저 추정해 보는 것이 좋습니다.


설치 및 설정

Next.js

npx @sentry/wizard@latest -i nextjs

위저드는 SDK 설치, 설정 파일 생성, next.config에 withSentryConfig 적용, Source Maps 업로드용 인증 토큰(.env.sentry-build-plugin) 생성까지 한 번에 해 줍니다. 수동 설정보다 빠지는 부분이 적으므로 처음에는 위저드를 권합니다. 생성되는 파일은 SDK와 Next.js 버전에 따라 다른데, 최근 버전은 브라우저용 instrumentation-client.ts와 서버·엣지용 sentry.server.config.ts·sentry.edge.config.ts를 만들고 instrumentation.ts에서 런타임별로 불러옵니다. Next.js는 코드가 브라우저, Node 서버, 엣지 런타임 세 곳에서 돌기 때문에 각각에 SDK를 초기화해야 서버 컴포넌트와 API 라우트의 에러까지 잡힙니다.

수동 설정

// instrumentation-client.ts (구버전 SDK는 sentry.client.config.ts)
import * as Sentry from '@sentry/nextjs';
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
  debug: false,
  replaysOnErrorSampleRate: 1.0,
  replaysSessionSampleRate: 0.1,
  integrations: [
    Sentry.replayIntegration({
      maskAllText: true,
      blockAllMedia: true,
    }),
  ],
});

dsn은 이벤트를 보낼 프로젝트 주소입니다. 브라우저 번들에 들어가야 하므로 공개되어도 되는 값이며, 누군가 DSN으로 가짜 이벤트를 보내는 것을 막으려면 프로젝트 설정의 허용 도메인(Allowed Domains)을 지정합니다. v7까지는 new Sentry.Replay()처럼 클래스로 통합을 만들었지만 v8부터는 Sentry.replayIntegration() 같은 함수 형태로 바뀌었습니다. 예전 블로그 코드를 그대로 붙이면 Sentry.Replay is not a constructor 에러가 나는 이유입니다.

샘플링 값은 비용과 직결됩니다. tracesSampleRate: 1.0은 모든 요청의 성능 데이터를 보내라는 뜻이라 개발·스테이징에서는 괜찮지만, 트래픽이 많은 프로덕션에서는 무료 한도가 금방 바닥나고 과금이 급증합니다. 프로덕션은 0.05~0.2 수준에서 시작하거나, tracesSampler 함수로 결제처럼 중요한 경로만 높게 잡는 방식을 씁니다. Replay는 평소 세션의 10%만 녹화하고(replaysSessionSampleRate), 에러가 난 세션은 모두 녹화(replaysOnErrorSampleRate: 1.0)하는 설정이 일반적입니다. maskAllText와 blockAllMedia는 화면의 텍스트와 이미지를 가려서 녹화하는 옵션으로, 개인정보가 화면에 나오는 서비스라면 기본값 그대로 켜 두는 것이 안전합니다.


에러 추적

자동 에러 캡처

// 자동으로 캡처됨
throw new Error('Something went wrong');
// Promise rejection
Promise.reject('Failed');

SDK는 window.onerror와 unhandledrejection 이벤트에 연결되어 잡히지 않은 예외와 처리되지 않은 Promise 거부를 자동으로 보냅니다. 두 번째 줄처럼 Error 객체가 아닌 문자열로 reject하면 스택 트레이스가 없어서 Sentry에는 Non-Error promise rejection captured with value: Failed로만 기록됩니다. 어디서 발생했는지 알 수 없으므로, reject와 throw에는 항상 new Error(...)를 쓰는 습관이 중요합니다.

자동 캡처가 동작하지 않는 대표적인 경우도 알아 두어야 합니다. try/catch로 잡아서 console.error만 찍고 넘어간 에러, React 에러 경계(Error Boundary)가 잡아서 대체 화면을 보여 준 에러는 “처리된” 에러라 자동으로 전송되지 않습니다. Next.js App Router의 error.tsx에서 Sentry.captureException(error)를 호출하고, 루트의 global-error.tsx도 만들어 두어야 렌더링 에러가 누락되지 않습니다.

수동 에러 캡처

import * as Sentry from '@sentry/nextjs';
try {
  riskyOperation();
} catch (error) {
  Sentry.captureException(error);
}
// 메시지만 전송
Sentry.captureMessage('Something important happened', 'info');

captureException은 에러를 보내고 실행을 계속하므로, 사용자에게 대체 동작을 보여 주면서도 개발팀은 문제를 알 수 있습니다. captureMessage는 예외는 아니지만 알아야 하는 상황(결제 모듈이 대체 경로로 동작함 등)을 기록할 때 씁니다. 다만 메시지 캡처를 로그처럼 남발하면 이벤트 한도가 빨리 차고 진짜 에러가 묻히므로, 일반 로그는 로그 시스템에 두고 Sentry에는 대응이 필요한 사건만 보내는 것이 원칙입니다.

Context 추가

Sentry.setUser({
  id: user.id,
  email: user.email,
  username: user.name,
});
Sentry.setTag('page', 'checkout');
Sentry.setContext('order', {
  orderId: '12345',
  amount: 99.99,
});
Sentry.captureException(error);

태그(setTag)와 컨텍스트(setContext)는 용도가 다릅니다. 태그는 검색과 필터링에 쓰이는 짧은 키-값이라 “checkout 페이지에서만 나는 에러”를 모아 볼 수 있고, 값의 종류가 적어야 합니다(주문 ID처럼 매번 다른 값은 태그로 부적합). 컨텍스트는 이벤트 상세 화면에 표시되는 구조화된 데이터로, 검색은 안 되지만 디버깅에 필요한 정보를 담기 좋습니다.

setUser에 이메일과 이름을 넣으면 개인정보가 외부 서비스로 전송된다는 점을 꼭 고려해야 합니다. 사용자 영향 범위를 세는 데는 내부 사용자 ID만으로 충분한 경우가 많고, 개인정보 처리 방침에 Sentry를 수탁자로 명시해야 할 수도 있습니다. SDK의 sendDefaultPii 기본값은 false로 IP 주소 같은 정보를 자동으로 보내지 않지만, 개발자가 직접 넣은 값은 그대로 전송됩니다. 요청 본문이나 에러 메시지에 토큰·주민번호 같은 값이 섞여 들어갈 수 있으므로 beforeSend에서 민감한 필드를 지우고, Sentry 프로젝트 설정의 Data Scrubber도 함께 켜 두는 것이 좋습니다.


성능 모니터링

Span 추적

import * as Sentry from '@sentry/nextjs';
await Sentry.startSpan({ name: 'Checkout Process', op: 'checkout' }, async () => {
  await Sentry.startSpan({ name: 'validate', op: 'validate' }, () => validateOrder());
  await Sentry.startSpan({ name: 'payment', op: 'payment' }, () => processPayment());
});
// 콜백에서 예외가 나면 span 상태가 자동으로 error가 되고, 예외는 그대로 다시 던져짐

v7의 startTransaction() / startChild() / finish() API는 v8에서 제거되고 startSpan 하나로 통합되었습니다. 콜백 방식은 span의 시작과 끝을 함수 실행 범위에 묶어 주므로 예외가 나도 span이 닫히지 않은 채 남는 일이 없고, 안쪽 startSpan은 자동으로 바깥 span의 자식이 됩니다. 이렇게 기록된 span은 폭포수(waterfall) 형태로 표시되어 결제 과정 중 어느 단계가 시간을 잡아먹는지 보여 줍니다. 여기서 에러는 span 상태에만 반영되므로, 에러 이슈로도 남기려면 호출하는 쪽에서 잡아 captureException을 호출하거나 전역 핸들러에 맡겨야 합니다.

자동 성능 추적

// Next.js는 자동으로 추적됨
// - 페이지 로드
// - API 요청
// - 데이터베이스 쿼리

Next.js SDK는 페이지 로드와 클라이언트 네비게이션, 서버 요청을 자동으로 트랜잭션으로 기록하고, 브라우저의 fetch/XHR과 Node의 HTTP 요청을 자식 span으로 붙입니다. 데이터베이스 쿼리는 사용하는 드라이버에 맞는 통합(Prisma, pg 등)이 활성화되어 있어야 잡힙니다. 브라우저 트랜잭션과 서버 트랜잭션을 하나의 추적으로 이으려면 요청 헤더에 sentry-trace와 baggage가 실려야 하는데, 다른 도메인의 API로 보내는 요청은 기본적으로 헤더를 붙이지 않으므로 tracePropagationTargets에 API 도메인을 추가해야 합니다. 이 헤더가 CORS 허용 목록에 없으면 API 요청이 preflight에서 막히므로 서버의 Access-Control-Allow-Headers도 함께 확인해야 합니다.


Source Maps

Next.js

// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
  {
    // Next.js config
  },
  {
    silent: true,
    org: 'your-org',
    project: 'your-project',
  }
);

빌드 시 업로드

npm run build

프로덕션 JavaScript는 압축되어 있어서 Source Maps 없이 보면 스택 트레이스가 at t (main-3f2a.js:1:48213)처럼 나옵니다. withSentryConfig는 빌드가 끝날 때 생성된 소스 맵을 Sentry에 업로드하고, 각 파일에 Debug ID를 심어 이벤트와 소스 맵을 정확히 짝짓습니다. Sentry는 이 소스 맵으로 원래 파일명, 줄 번호, 코드 주변 줄을 복원해 보여 줍니다.

설정했는데도 스택 트레이스가 압축된 코드로 나온다면 대개 다음 중 하나입니다. 가장 흔한 원인은 인증 토큰이 CI에 없는 것입니다. SENTRY_AUTH_TOKEN 환경 변수가 없으면 업로드 단계가 경고만 남기고 건너뛰어 빌드는 성공하므로, 빌드 로그를 확인하지 않으면 모릅니다. silent: true는 이 경고까지 숨기므로 처음 설정할 때는 false로 두고 로그를 확인하는 편이 좋습니다. 두 번째는 배포한 파일과 업로드한 소스 맵이 다른 빌드에서 나온 경우입니다. 빌드를 두 번 하거나(업로드용 빌드와 배포용 빌드가 따로), 배포 후 CDN이 예전 파일을 캐시하고 있으면 짝이 맞지 않습니다. 소스 맵 파일 자체는 원본 코드를 담고 있으므로 공개 서버에 올리지 않도록 업로드 후 삭제하는 옵션(sourcemaps.deleteSourcemapsAfterUpload)을 켜 두는 것이 일반적입니다.


알림 설정

Slack 통합

  1. Sentry → Settings → Integrations → Slack
  2. 채널 선택
  3. Alert Rules 설정

Alert Rules

# 조건
- 에러 발생 시
- 특정 에러 타입
- 특정 환경 (production)
- 특정 사용자
# 액션
- Slack 알림
- Email 발송
- Webhook 호출

Sentry의 알림은 크게 이슈 알림(새 이슈가 생김, 해결한 이슈가 재발함, 한 이슈가 1시간에 N번 이상 발생)과 지표 알림(전체 에러율이나 응답 시간이 임계값을 넘음)으로 나뉩니다. 처음 도입할 때 흔히 저지르는 실수는 “모든 에러 발생 시 알림”으로 설정하는 것입니다. 이렇게 설정하면 브라우저 확장 프로그램이 일으키는 에러, 네트워크가 끊긴 사용자의 Failed to fetch, 광고 차단기 때문에 로드되지 않은 스크립트 에러가 쉴 새 없이 Slack 채널을 채우고, 얼마 지나지 않아 팀원들이 채널 알림을 꺼 버리는 것이 흔히 보는 결말입니다. 알림이 무시되기 시작하면 모니터링은 없는 것과 같습니다.

효과가 있었던 방법은 세 가지입니다. 첫째, 알림은 production 환경에서 새 이슈와 회귀에만 걸고, 빈도 기반 알림은 영향받은 사용자 수를 조건으로 씁니다. 둘째, SDK의 ignoreErrors와 denyUrls로 우리 코드가 아닌 에러(확장 프로그램 URL, 서드파티 스크립트)를 처음부터 걸러 냅니다. 셋째, 이슈마다 담당자(Ownership Rules)를 코드 경로 기준으로 지정해 알림이 모두에게 가지 않고 해당 팀에만 가도록 합니다.


Release Tracking

배포 추적

// instrumentation-client.ts
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  release: process.env.NEXT_PUBLIC_SENTRY_RELEASE,
  environment: process.env.NODE_ENV,
});

CI/CD

# .github/workflows/deploy.yml
- name: Create Sentry release
  uses: getsentry/action-release@v1
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
    SENTRY_ORG: your-org
    SENTRY_PROJECT: your-project
  with:
    environment: production

릴리스는 “이 에러가 어느 버전에서 처음 생겼는가”를 알려 주는 기준입니다. SDK의 release 값과 CI에서 만든 릴리스 이름이 정확히 같아야 연결되므로, 보통 Git 커밋 SHA를 두 곳에 똑같이 씁니다. withSentryConfig를 쓰면 빌드 시 커밋 SHA를 릴리스로 자동 주입하므로 release를 직접 지정하지 않아도 되는 경우가 많습니다. 릴리스에 커밋 정보가 연결되면 Sentry가 “이 이슈를 만들었을 가능성이 높은 커밋”(Suspect Commits)을 표시해 주고, 이슈를 “다음 릴리스에서 해결됨”으로 표시해 두면 그 뒤 버전에서 다시 발생할 때 회귀로 알려 줍니다. 액션은 현재 더 새로운 메이저 버전이 나와 있으니 사용할 때 최신 버전을 확인하세요.

environment: process.env.NODE_ENV는 실수하기 쉬운 설정입니다. Next.js는 next build로 만든 결과물에서 NODE_ENV가 항상 production이라, 스테이징 서버에 배포한 빌드도 production 환경으로 기록됩니다. 그러면 스테이징에서 테스트하며 낸 에러가 production 알림을 울리고 에러율 통계도 섞입니다. NEXT_PUBLIC_APP_ENV처럼 배포 환경을 나타내는 별도 변수를 두고 그 값을 environment에 넣어야 합니다.


도입 직후 확인할 세 가지

Sentry를 붙인 뒤 효과를 보려면 세 가지를 먼저 확인하는 것이 좋습니다. Source Maps가 실제로 업로드되는지 빌드 로그로 확인할 것(업로드가 빠지면 스택 트레이스가 번들된 코드 위치로만 찍힙니다), 샘플링과 필터로 비용과 소음을 통제할 것, environment와 release 값을 정확히 넣어 “어느 환경의 어느 버전”에서 난 에러인지 구분할 것입니다. 개인정보는 SDK가 알아서 막아 주지 않으므로 직접 넣는 컨텍스트 값과 beforeSend 필터를 함께 점검해야 합니다.


같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. 무료로 사용할 수 있나요?

A. 네. 1인 개발자용 무료 플랜이 있고 월 에러 이벤트 한도(약 5천 건 수준)가 있습니다. 성능 추적과 Replay에도 별도 한도가 있으므로 샘플링을 낮게 잡아야 무료 범위 안에서 쓸 수 있습니다. 정확한 한도는 요금 페이지를 확인하세요.

Q. 모든 프레임워크를 지원하나요?

A. JavaScript(브라우저·Node), Python, Go, Java, .NET, PHP, Ruby, 모바일(iOS·Android·React Native·Flutter), 게임 엔진(Unity·Unreal)까지 공식 SDK가 있습니다. 프레임워크별 SDK(@sentry/nextjs, sentry-sdk[django] 등)를 쓰면 라우팅과 요청 정보가 자동으로 붙습니다.

Q. 개인정보가 걱정돼요

A. SDK는 기본적으로 IP 같은 개인정보를 보내지 않고 서버 측 Data Scrubber가 비밀번호·토큰처럼 보이는 필드를 가립니다. 하지만 개발자가 setUser나 컨텍스트로 직접 넣은 값, 에러 메시지에 섞인 값은 전송되므로 beforeSend에서 필터링하고, Replay는 텍스트 마스킹을 켜 두는 것이 안전합니다.

Q. Sentry SDK를 붙이면 앱 성능에 영향이 있나요?

A. SDK는 이벤트를 비동기로 전송해 앱 성능에 주는 영향이 작지만, 성능 추적과 Replay는 샘플링 비율에 따라 오버헤드와 비용이 늘어나므로 비율을 조절해 쓰는 것이 좋습니다.