Next.js 프로덕션 모니터링 완전 가이드: Sentry 통합, 로그 관리, 알림 설정 실전

금요일 밤 9시 17분, 휴대폰이 울렸습니다.
WeChat 그룹은 이미 난리가 났습니다. “결제 페이지가 안 열려요”, “방금 주문이 실패했어요”, “흰 화면이에요, 흰 화면”. 로컬에서 한 번 돌려보면 완벽하게 동작합니다. 서버 로그를 확인해도 “Internal Server Error” 한 줄 말고는 아무것도 없습니다. 사용자는 결제 버튼을 누른 뒤 페이지가 멈췄다고 말하지만, 도저히 재현되지 않습니다.
그 주말에는 Vercel 배포 로그를 뒤지고, 테스트 버전을 다시 배포하고, 결국 서드파티 결제 SDK가 프로덕션 환경에서 간헐적으로 타임아웃된다는 사실을 찾아냈습니다. 전체 조사 과정은 어둠 속에서 떨어진 바늘을 찾는 느낌이었습니다.
Next.js 애플리케이션은 개발 환경에서는 매끄럽게 돌아가지만, 배포한 순간 판도라의 상자를 연 것처럼 바뀔 수 있습니다. 서버 사이드 렌더링이 가끔 500을 내고, 엣지 함수가 이유 없이 오류를 내며, API 응답 시간이 치솟아도 병목이 어디인지 모릅니다.
문제의 뿌리는 단순합니다. 완전한 프로덕션 모니터링 체계가 없기 때문입니다. 이 글에서는 Next.js 모니터링 시스템을 손으로 따라 만들 수 있게 설명합니다. Sentry 오류 추적부터 구조화 로그 관리까지, 성능 모니터링부터 알림 설정까지 다룹니다. 이론을 쌓아 올리는 글이 아니라 바로 복사해 붙여 넣을 수 있는 설정 코드 중심입니다. 다 읽고 나면 다음 온라인 장애에서는 사용자보다 먼저 문제를 알게 됩니다.
Next.js에 왜 별도의 모니터링 구성이 필요한가
Next.js의 “세 머리” 특성
기존 프론트엔드 애플리케이션은 브라우저에서만 실행되며, 오류는 DevTools 콘솔에서 바로 볼 수 있습니다. Next.js는 다릅니다. 같은 애플리케이션이 완전히 다른 세 곳에서 동시에 실행됩니다.
- 클라이언트(Browser): 사용자 브라우저의 React 컴포넌트
- 서버(Node.js): SSR 렌더링, API Routes, Server Actions
- 엣지 네트워크(Edge Runtime): 미들웨어, 엣지 함수
결제 기능 하나만 봐도 클라이언트 폼 검증 -> 미들웨어 인증 -> Server Action 호출 -> API Route의 데이터베이스 조회 -> 클라이언트에 결과 표시로 이어질 수 있습니다. 어느 한 단계에서 오류가 나도 기존 브라우저 모니터링만으로는 전체 그림을 볼 수 없습니다.
지난해 꽤 이상한 bug를 겪었습니다. 사용자가 “페이지 로딩이 매우 느리다가 500이 뜬다”고 피드백했습니다. 브라우저 Network 패널에는 요청이 실제로 느리다는 것만 보였고, 구체적으로 어디가 느린지는 알 수 없었습니다. 나중에 Sentry의 분산 추적을 붙이고 나서야 서버 렌더링 중 호출한 서드파티 API의 응답 시간이 평소 200ms에서 8초로 치솟은 것을 확인했습니다. 클라이언트 모니터링만으로는 이런 문제를 절대 발견할 수 없습니다.
SSR의 “블랙박스 효과”
서버 사이드 렌더링에서 오류가 나면 사용자가 보는 것은 대개 휑한 500 페이지뿐입니다. stack trace도 없고, 컨텍스트도 없고, 아무것도 없습니다.
더 까다로운 것은 Hydration 오류입니다. 이런 경고를 본 적이 있을 수 있습니다.
Warning: Expected server HTML to contain a matching <div> in <div>
이런 오류는 개발 환경에서는 크게 드러나지 않을 수 있지만, 프로덕션에서는 전체 페이지 상호작용을 망가뜨릴 수 있습니다. 모니터링 시스템이 없으면 “페이지가 눌리지 않는다”는 사용자 피드백에 기대어 수동적으로 발견할 수밖에 없습니다.
Vercel 데이터에 따르면 SSR 관련 오류는 Next.js 프로덕션 문제의 약 35%를 차지합니다. 이것도 오류만 계산한 것이고 성능 문제는 포함하지 않습니다. 예를 들어 어떤 컴포넌트의 SSR 시간이 갑자기 길어져 사용자는 페이지가 “느리게 열린다”고 느끼지만, 어디가 병목인지 전혀 알 수 없는 상황입니다.
완전한 모니터링의 네 가지 기둥
믿을 만한 Next.js 모니터링 구성은 다음을 포괄해야 합니다.
오류 추적
예외를 잡는 것에서 끝나지 않습니다. 누가 오류를 일으켰는지(사용자 정보), 어떤 환경이었는지(기기, 브라우저, 네트워크), 어떤 작업을 했는지(브레드크럼 추적), 관련 요청 파라미터가 무엇인지까지 알아야 합니다.
성능 모니터링
LCP(최대 콘텐츠 렌더링)가 2.5초를 넘었는지, API 인터페이스 응답 시간이 느려졌는지, 어떤 데이터베이스 쿼리가 전체 요청을 끌어내렸는지 확인해야 합니다.
로그 관리
구조화 로그를 사용해 시간, 사용자, 요청 ID 기준으로 빠르게 검색할 수 있어야 합니다. 개발 환경에서는 읽기 좋게 출력해 디버깅하고, 프로덕션에서는 로그 플랫폼에 연결해 통합 분석합니다.
알림 설정
오류율이 임계값을 넘으면 즉시 팀에 알리고, 새로운 유형의 오류가 처음 나타나면 Slack으로 보내며, 성능 회귀가 발생하면 자동으로 알림을 트리거해야 합니다.
이 네 가지가 있으면 온라인 문제가 생겼을 때 더 이상 “눈감고 더듬는” 상태가 아닙니다. 이제 하나씩 해결해 보겠습니다.
Sentry 통합 실전 - 설치부터 심화 설정까지
5분 빠른 도입
Sentry의 Next.js 지원은 이미 꽤 성숙합니다. 공식 자동 설정 마법사도 제공합니다. 실제로 5분이면 됩니다.
# SDK 설치
npm install @sentry/nextjs
# 설정 마법사 실행
npx @sentry/wizard@latest -i nextjs
마법사는 몇 가지 질문(Sentry 프로젝트 DSN, Source Maps 업로드 여부 등)을 묻고, 세 개의 설정 파일을 자동으로 만듭니다.
sentry.client.config.ts- 브라우저 환경sentry.server.config.ts- Node.js 서버sentry.edge.config.ts- Edge Runtime
또한 next.config.js를 수정해 Sentry의 webpack 플러그인을 추가합니다. 마법사를 한 번 돌리면 기본 모니터링은 연결됩니다.
하지만 이것은 시작일 뿐입니다. 프로덕션 환경에는 더 세밀한 설정이 필요합니다.
App Router의 오류 캡처 포인트
App Router를 사용한다면 특히 주의해야 할 지점이 몇 가지 있습니다.
전역 오류 처리
app/global-error.tsx를 만듭니다. App Router의 마지막 방어선입니다.
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Sentry로 전송
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h2>문제가 발생했습니다</h2>
<p>이 오류는 기록되었으며 최대한 빨리 수정하겠습니다</p>
<button onClick={() => reset()}>다시 시도</button>
</div>
</body>
</html>
);
}
Server Actions 오류 캡처
Server Actions는 App Router의 핵심 기능이지만 오류 처리는 자주 빠집니다.
'use server';
import * as Sentry from '@sentry/nextjs';
export async function createOrder(formData: FormData) {
return await Sentry.withServerActionInstrumentation(
'createOrder', // action 이름, Sentry에 표시됩니다
{
recordResponse: true, // 응답 데이터 기록
},
async () => {
// 비즈니스 로직
const productId = formData.get('productId');
const order = await db.order.create({
data: { productId, userId: getCurrentUserId() },
});
return order;
}
);
}
이렇게 감싸면 Server Action 안의 모든 오류가 자동으로 보고되고 실행 시간도 추적할 수 있습니다.
프로덕션 환경 최적화 설정
Sentry를 붙인 뒤 첫 달 청구서를 보고 놀랄 수 있습니다. 기본 설정이 모든 이벤트를 보고하기 때문입니다. 샘플링 비율을 조정해야 합니다.
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 성능 추적 샘플링 비율
// 개발 환경 100%, 프로덕션 환경 10%(트래픽에 맞춰 조정)
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Session Replay 샘플링
replaysSessionSampleRate: 0.1, // 정상 세션의 10%를 녹화
replaysOnErrorSampleRate: 1.0, // 오류 세션은 100% 녹화
// 환경 식별자
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || 'development',
// 특정 오류 무시
ignoreErrors: [
// 브라우저 확장 프로그램이 주입한 오류
'ResizeObserver loop limit exceeded',
// 서드파티 스크립트 오류
/chrome-extension/,
/^Non-Error promise rejection/,
],
});
tracesSampleRate 설정 가이드:
- 일 UV < 1만: 0.2 - 0.5
- 일 UV 1-10만: 0.1 - 0.2
- 일 UV > 10만: 0.05 - 0.1
우리 프로젝트는 일 UV가 약 3만이라 0.15로 설정했습니다. 매달 Sentry 할당량의 약 60%를 사용해 충분한 샘플을 확보하면서도 초과 비용을 피할 수 있었습니다.
Source Maps: 디버깅 가능하지만 코드는 노출하지 않기
프로덕션 환경의 JavaScript 코드는 보통 압축되고 난독화되어 오류 스택이 이렇게 보입니다.
at r.render (app.js:1:23456)
전혀 읽을 수 없습니다. Source Maps는 난독화된 코드를 원본 코드로 매핑해 줍니다. 하지만 Source Maps를 그대로 공개하면 소스 코드가 노출됩니다.
Sentry의 방식은 Source Maps를 Sentry 서버에 업로드하는 것입니다. 사용자 브라우저는 가져갈 수 없고, Sentry 내부에서만 스택 복원에 사용합니다.
CI/CD에서 설정합니다. GitHub Actions를 예로 들면 다음과 같습니다.
# .github/workflows/deploy.yml
- name: Upload Source Maps to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: your-org
SENTRY_PROJECT: your-project
run: npm run build
next.config.js의 Sentry 플러그인이 업로드를 자동으로 처리합니다. SENTRY_AUTH_TOKEN은 GitHub Secrets에 넣고 코드 저장소에는 커밋하지 마세요.
고급 기능: Session Replay와 분산 추적
Session Replay는 제가 가장 좋아하는 기능입니다. 사용자 작업을 다시 재생해 마치 녹화 영상을 보듯 현장을 복원합니다.
한 번은 사용자가 “결제 버튼이 안 눌린다”고 피드백한 적이 있습니다. Session Replay를 보니 iPad 가로 화면에서 가상 키보드가 결제 버튼을 가리고 있었습니다. 이런 문제는 오류 로그만으로는 절대 발견할 수 없습니다.
켜는 방법은 간단합니다. 클라이언트 설정에 추가하면 됩니다.
import * as Sentry from '@sentry/nextjs';
import { Replay } from '@sentry/nextjs';
Sentry.init({
integrations: [
new Replay({
maskAllText: false, // 모든 텍스트를 숨길지 여부
blockAllMedia: true, // 모든 미디어를 차단할지 여부
maskAllInputs: true, // 폼 입력 숨김(민감 정보 노출 방지)
}),
],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
분산 추적은 요청 하나의 전체 생명주기를 따라갑니다. 사용자가 버튼을 클릭하고, 프론트엔드가 요청을 보내고, API Route가 데이터베이스를 조회하고, 프론트엔드가 결과를 렌더링하는 각 단계의 시간을 모두 볼 수 있습니다.
설정도 복잡하지 않습니다. 프론트엔드와 백엔드가 같은 Sentry.init 설정을 사용하게 하면 Sentry SDK가 요청 헤더에 sentry-trace를 자동으로 전달합니다.
커스텀 컨텍스트: 오류를 더 의미 있게 만들기
기본적으로 Sentry는 “오류가 하나 발생했다”는 것만 압니다. 커스텀 컨텍스트를 추가하면 오류 보고서의 가치가 훨씬 커집니다.
import * as Sentry from '@sentry/nextjs';
// 사용자 정보 설정
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
// 비밀번호 같은 민감 정보는 넣지 마세요!
});
// 비즈니스 컨텍스트 추가
Sentry.setContext('purchase', {
orderId: '12345',
amount: 99.99,
paymentMethod: 'credit_card',
});
// 태그 추가(필터링에 편리)
Sentry.setTag('feature', 'checkout');
Sentry.setTag('ab_test', 'variant_b');
이렇게 하면 오류가 발생했을 때 어떤 사용자에게서, 어떤 비즈니스 상황에서 트리거됐는지 바로 알 수 있습니다.
실제 사례도 있습니다. 결제 오류 발생 빈도가 예상보다 높았던 적이 있는데, Sentry 컨텍스트를 보니 모두 ab_test: variant_b 사용자였습니다. A/B 테스트의 새 결제 흐름에 bug가 있음을 찾아 바로 이 변형을 꺼서 더 큰 손실을 피했습니다.
로그 관리 - 로그가 일하게 만들기
console.log만으로는 부족합니다
초기에는 저도 여기저기 console.log를 넣는 것을 좋아했습니다. 디버깅할 때는 꽤 시원하지만 배포하고 나면 막막해집니다.
- 필터링할 수 없음: 로그 10만 줄에서 특정 사용자의 요청 기록을 찾는다고요? 행운을 빕니다.
- 집계할 수 없음: 지난 1시간 동안 1초보다 느린 데이터베이스 쿼리가 몇 개인지 알고 싶어도 통계낼 수 없습니다.
- 알림할 수 없음: 로그에 “Payment failed”가 찍혀도 아무도 모릅니다.
구조화 로그가 이 문제를 해결합니다. 단순한 문자열을 찍는 것이 아니라 JSON 객체를 출력하고, 각 로그에 timestamp, 로그 레벨, 요청 ID, 사용자 ID 같은 메타데이터를 붙입니다. 이후에는 어떤 필드로도 검색하고 집계할 수 있습니다.
Pino vs Winston: 무엇을 고를까
Node.js 생태계의 대표 로그 라이브러리 두 개는 각각 장단점이 있습니다.
| 특성 | Pino | Winston |
|---|---|---|
| 성능 | 매우 빠르고 비동기 로그 오버헤드가 거의 없음 | 조금 느리지만 여전히 충분히 사용 가능 |
| 사용성 | 설정이 간결하고 바로 쓰기 좋음 | 기능이 풍부하고 플러그인 생태계가 좋음 |
| 확장성 | Transport로 확장 | 여러 Transport를 내장 |
| 커뮤니티 | Next.js 공식 추천 | 오래된 라이브러리이며 문서가 가장 풍부 |
제 제안:
- 고동시성 시나리오(QPS > 1000): Pino를 선택하세요. 성능 이점이 뚜렷합니다
- 복잡한 로그 처리(여러 형식, 여러 대상)가 필요할 때: Winston을 선택하세요
- 잘 모르겠다면: Pino를 선택하세요. Next.js 공식 문서에서도 사용합니다
Pino 실전 설정
먼저 설치합니다.
npm install pino
npm install pino-pretty --save-dev # 개발 환경 출력 보기 좋게 만들기
전역 logger를 만듭니다.
// lib/logger.ts
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// 로그 레벨 형식화
formatters: {
level: (label) => ({ level: label.toUpperCase() }),
},
// 개발 환경에서는 pino-pretty로 보기 좋게 출력
transport: process.env.NODE_ENV === 'development'
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
},
}
: undefined,
});
export { logger };
개발 환경 출력은 색이 입혀지고 읽기 쉬운 형식입니다. 프로덕션 환경에서는 JSON으로 출력되어 로그 플랫폼이 파싱하기 좋습니다.
API Route에서 사용하기
핵심은 요청마다 correlationId(연결 ID)를 부여해 이 요청과 관련된 모든 로그를 하나로 묶는 것입니다.
// app/api/products/route.ts
import { logger } from '@/lib/logger';
import { randomUUID } from 'crypto';
export async function GET(request: Request) {
// 요청 고유 ID 생성
const correlationId = request.headers.get('x-correlation-id') || randomUUID();
// 하위 logger 생성, correlationId를 자동으로 포함
const log = logger.child({ correlationId });
try {
log.info({ url: request.url }, '상품 요청 처리 중');
const products = await db.product.findMany();
log.info({ count: products.length }, '상품 조회 성공');
return Response.json(products);
} catch (error) {
log.error({ error: error.message, stack: error.stack }, '상품 조회 실패');
throw error;
}
}
이렇게 하면 같은 correlationId를 가진 모든 로그를 서로 연결할 수 있습니다. 문제를 조사할 때 이 ID만 검색하면 전체 요청 흐름이 한눈에 들어옵니다.
로그 레벨 모범 사례
로그가 너무 적으면 쓸모없고, 너무 많으면 핵심 정보가 묻힙니다. 제가 쓰는 기준은 이렇습니다.
ERROR - 즉시 처리해야 하는 문제
- 데이터베이스 연결 실패
- 결제 인터페이스 호출 실패
- 핵심 비즈니스 로직 예외
log.error({ error, userId, orderId }, '결제 처리 실패');
WARN - 비정상이지만 복구 가능한 상황
- API 호출 재시도 후 성공
- 폴백 로직 트리거
- 할당량 한도에 근접
log.warn({ retryCount: 3 }, '외부 API 재시도 성공');
INFO - 핵심 비즈니스 지점
- 사용자 로그인/로그아웃
- 주문 생성/완료
- 핵심 설정 변경
log.info({ userId, ip }, '사용자 로그인');
DEBUG - 자세한 디버깅 정보
- 함수 파라미터와 반환값
- 중간 상태
- 성능 타이밍
log.debug({ params }, '외부 API 호출 중');
프로덕션 환경 기본값은 INFO 레벨이고, 문제가 생겼을 때 임시로 DEBUG로 올려 조사합니다.
로그 집계와 분석
로컬 개발은 pino-pretty만으로 충분합니다. 프로덕션 환경에서는 로그 플랫폼에 연결해야 합니다. 대표 선택지는 다음과 같습니다.
Vercel Logs
Vercel에 배포한다면 내장 로그 시스템이 있어 설정이 필요 없습니다. 다만 7일만 보관하고 검색 기능은 제한적입니다.
Datadog
엔터프라이즈급 솔루션입니다. APM, 로그, 모니터링을 한 번에 제공합니다. 설정은 다음과 같습니다.
import { datadogLogs } from '@datadog/browser-logs';
datadogLogs.init({
clientToken: process.env.NEXT_PUBLIC_DATADOG_CLIENT_TOKEN,
site: 'datadoghq.com',
forwardErrorsToLogs: true,
sampleRate: 100,
});
Logtail/BetterStack
가격 대비 효율이 좋고 로그 분석에 집중합니다. 실시간 검색, 알림 규칙, 커스텀 패널을 지원합니다.
개인 프로젝트에서는 Logtail을 씁니다. 설정이 간단하고 매달 1G 로그가 무료입니다. 팀 프로젝트에서는 Datadog을 씁니다. 비싸지만 기능이 강력합니다.
핵심 로그 필드 설계
좋은 로그에는 이런 필드가 들어가야 합니다.
{
"timestamp": "2025-12-20T15:00:06.123Z", // timestamp
"level": "INFO", // 로그 레벨
"correlationId": "abc-123-def", // 요청 연결 ID
"userId": "user_456", // 사용자 ID
"action": "create_order", // 비즈니스 작업
"duration": 234, // 실행 시간(ms)
"status": "success", // 상태
"metadata": { // 추가 메타데이터
"orderId": "order_789",
"amount": 99.99
}
}
이런 로그는 “누가 언제 무엇을 했고, 결과가 어땠는가”에 답할 수 있습니다.
성능 모니터링 - 데이터로 움직이는 최적화
Core Web Vitals: Google이 신경 쓰는 지표
Google은 Core Web Vitals를 검색 순위 요소로 사용합니다. 우리도 봐야 합니다.
- LCP (Largest Contentful Paint): 최대 콘텐츠 렌더링 시간, 이상값 < 2.5s
- FID (First Input Delay) / INP (Interaction to Next Paint): 상호작용 응답 시간, < 100ms / < 200ms
- CLS (Cumulative Layout Shift): 누적 레이아웃 이동, < 0.1
Next.js에는 Web Vitals 보고가 내장되어 있습니다. app/layout.tsx에 몇 줄만 추가하면 됩니다.
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitalsReporter() {
useReportWebVitals((metric) => {
// Sentry로 전송
if (window.Sentry) {
window.Sentry.captureMessage(`Web Vital: ${metric.name}`, {
level: 'info',
tags: {
web_vital: metric.name,
},
contexts: {
web_vitals: {
value: metric.value,
rating: metric.rating,
},
},
});
}
// 또는 자체 분석 플랫폼으로 전송
fetch('/api/analytics/web-vitals', {
method: 'POST',
body: JSON.stringify(metric),
});
});
return null;
}
그리고 루트 레이아웃에서 불러옵니다.
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<WebVitalsReporter />
{children}
</body>
</html>
);
}
API 성능 추적
프론트엔드 성능은 절반일 뿐입니다. 백엔드 API가 느려도 사용자는 기다려야 합니다. Sentry의 Performance Monitoring은 각 API 요청을 추적할 수 있습니다.
// app/api/products/[id]/route.ts
import * as Sentry from '@sentry/nextjs';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// transaction 생성
return await Sentry.startSpan(
{
op: 'api.request',
name: 'GET /api/products/[id]',
},
async () => {
// 데이터베이스 쿼리 추적
const product = await Sentry.startSpan(
{
op: 'db.query',
name: 'Fetch product from database',
},
async () => {
return await db.product.findUnique({
where: { id: params.id },
include: { reviews: true },
});
}
);
if (!product) {
return Response.json({ error: '찾을 수 없음' }, { status: 404 });
}
// 외부 API 호출 추적
const pricing = await Sentry.startSpan(
{
op: 'http.client',
name: 'Fetch pricing from external API',
},
async () => {
const res = await fetch(`https://pricing-api.com/product/${params.id}`);
return res.json();
}
);
return Response.json({ ...product, pricing });
}
);
}
Sentry 패널에서는 다음을 볼 수 있습니다.
- 전체 요청 시간 450ms
- 데이터베이스 쿼리 120ms
- 외부 API 호출 300ms
- 기타 로직 30ms
한눈에 병목이 외부 API임을 알 수 있습니다.
느린 쿼리 알림
데이터베이스는 성능 병목의 단골입니다. Prisma 미들웨어에 모니터링을 추가할 수 있습니다.
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
import { logger } from './logger';
const prisma = new PrismaClient();
// 느린 쿼리 모니터링
prisma.$use(async (params, next) => {
const before = Date.now();
const result = await next(params);
const after = Date.now();
const duration = after - before;
// 1초를 넘는 쿼리는 WARN으로 기록
if (duration > 1000) {
logger.warn({
model: params.model,
action: params.action,
duration,
args: params.args,
}, '느린 데이터베이스 쿼리 감지');
// 동시에 Sentry로 전송
Sentry.captureMessage('느린 데이터베이스 쿼리', {
level: 'warning',
tags: { model: params.model, action: params.action },
extra: { duration, args: params.args },
});
}
return result;
});
export { prisma };
이렇게 해 두면 어떤 느린 쿼리도 눈을 피해 가지 못합니다.
실제 사용자 모니터링 vs 합성 모니터링
실제 사용자 모니터링(RUM)
Sentry, Datadog 등으로 실제 사용자의 성능 데이터를 수집합니다. 장점은 서로 다른 네트워크와 기기 같은 실제 상황을 반영한다는 점이고, 단점은 문제가 발생한 뒤에야 알 수 있는 수동적인 방식이라는 점입니다.
합성 모니터링(Synthetic Monitoring)
Checkly, Pingdom 등으로 전 세계 여러 위치에서 일정 주기로 웹사이트 방문을 시뮬레이션합니다. 장점은 문제를 능동적으로 발견할 수 있다는 점이고, 단점은 모든 사용자 상황을 포괄할 수 없다는 점입니다.
둘을 함께 쓰는 것이 가장 좋습니다:
- RUM은 사용자 경험 지표를 봅니다
- 합성 모니터링은 가용성과 핵심 흐름(로그인, 결제)을 담당합니다
저는 Checkly로 5분마다 5개 지리적 위치에서 홈페이지와 로그인 인터페이스를 방문하게 합니다. 하나라도 타임아웃되거나 실패하면 즉시 알림을 보냅니다.
알림 설정 - 문제를 가장 먼저 발견하기
Slack 통합: 팀이 가장 먼저 알게 하기
Sentry의 Slack 통합은 간단합니다. Settings -> Integrations -> Slack에서 권한을 부여한 뒤 어느 채널로 보낼지 선택하면 됩니다.
하지만 기본 설정은 모든 오류를 보내므로 금세 노이즈가 됩니다. 알림 규칙을 설정해야 합니다.
Sentry 프로젝트 설정에서:
- Alerts -> Create Alert Rule
- 트리거 조건을 선택합니다:
- 오류율 알림: “10분 안에 오류 수 > 50”
- 새 오류 알림: “처음 나타난 오류는 즉시 알림”
- 성능 회귀: “API P95 응답 시간 > 1s”
- Action을 선택합니다: Send a notification via Slack
Slack 메시지 형식:
🚨 Production Error Spike
Project: my-nextjs-app
Environment: production
Error: TypeError: Cannot read property 'id' of undefined
Events: 127 events in 10 minutes
View in Sentry: https://sentry.io/...
링크를 누르면 Sentry로 바로 이동해 상세 정보와 스택을 볼 수 있습니다.
알림 등급 나누기: 피로 피하기
모든 문제가 똑같이 긴급한 것은 아닙니다. 제 등급 전략은 이렇습니다.
P0 - 심각 (즉시 처리)
- 서비스 완전 불가
- 결제 기능 실패
- 데이터베이스 연결 끊김
트리거 방식: 전화(PagerDuty) + Slack @channel
P1 - 중요 (1시간 안에 대응)
- 핵심 기능 이상
- 오류율 급증(10분 안에 > 100회)
- API 응답 시간 P95 > 3s
트리거 방식: Slack을 개발 채널로 전송
P2 - 일반 (업무 시간에 처리)
- 작은 범위의 오류(< 10회/시간)
- 비핵심 기능 이상
- 서드파티 스크립트 오류
트리거 방식: 메일 일일 요약
설정 예시(Sentry Alert Rule):
// P0 알림: 결제 실패
{
conditions: [
{ type: 'event.tag', key: 'feature', value: 'payment' },
{ type: 'event.level', value: 'error' }
],
frequency: 'every event', // 매번 알림
actions: [
{ type: 'slack', channel: '#critical-alerts', mention: '@channel' },
{ type: 'pagerduty', service: 'payments' }
]
}
// P1 알림: 오류율 급증
{
conditions: [
{ type: 'event.count', value: 100, interval: '10m' }
],
frequency: 'once per issue', // 문제마다 한 번만 알림
actions: [
{ type: 'slack', channel: '#alerts-dev' }
]
}
알림 노이즈 줄이기
처음 모니터링을 붙이면 알림에 묻힐 수 있습니다. 노이즈를 줄이는 몇 가지 방법이 있습니다.
1. 알려진 문제 무시하기
개발 환경의 핫 리로드 오류, 서드파티 스크립트 예외 같은 노이즈는 필터링할 수 있습니다.
// sentry.client.config.ts
Sentry.init({
ignoreErrors: [
// 브라우저 확장 프로그램 오류
/chrome-extension/,
/moz-extension/,
// 서드파티 스크립트
/google-analytics/,
// 개발 환경 핫 리로드
/HMR/,
],
denyUrls: [
// 특정 도메인의 스크립트 오류 무시
/extensions\//i,
/^chrome:\/\//i,
],
});
2. 반복 알림 합치기
같은 오류는 10분 안에 한 번만 알림을 보내 채널이 도배되지 않게 합니다. Sentry의 “Issue Grouping”은 비슷한 오류를 자동으로 합칩니다.
3. 조용한 기간 설정하기
배포 중에는 짧은 오류 피크가 생길 수 있습니다. “Mute for 10 minutes”를 설정할 수 있습니다.
4. 지문(Fingerprint) 사용하기
커스텀 오류 그룹 규칙으로 같은 근본 원인의 오류를 합칩니다.
Sentry.captureException(error, {
fingerprint: ['database-connection-error', databaseName],
});
이렇게 하면 서로 다른 데이터베이스 연결 오류가 분리되어 위치를 찾기 쉽습니다.
실전 사례 - 전체 모니터링 구성 적용
이커머스 웹사이트 모니터링 아키텍처
지난해 한 이커머스 웹사이트의 모니터링 개편을 도왔습니다. 전체 구성을 공유합니다.
배경:
- 일평균 UV 8만
- 피크 QPS 3000+
- 주요 문제: 결제 간헐 실패, 홈페이지 로딩 느림
모니터링 아키텍처:
┌─────────────┐
│ Next.js │
│ 프론트/SSR │
└──────┬──────┘
│
├─ Sentry (오류 + 성능)
├─ Pino (구조화 로그) → Datadog
├─ Web Vitals → Sentry
└─ Checkly (합성 모니터링)
핵심 설정:
- 사용자 행동 추적
// lib/tracking.ts
import * as Sentry from '@sentry/nextjs';
export function trackCheckoutStep(step: string, data: any) {
Sentry.addBreadcrumb({
category: 'checkout',
message: `Checkout step: ${step}`,
data,
level: 'info',
});
}
// 쇼핑 흐름에서 호출
trackCheckoutStep('add_to_cart', { productId, price });
trackCheckoutStep('proceed_to_payment', { cartTotal });
trackCheckoutStep('payment_submitted', { method: 'credit_card' });
이렇게 하면 결제가 실패했을 때 사용자의 전체 쇼핑 경로를 볼 수 있습니다.
- 결제 모니터링
// app/api/payment/route.ts
export async function POST(request: Request) {
const log = logger.child({ action: 'payment' });
try {
const result = await processPayment(data);
log.info({ orderId, amount, method }, '결제 성공');
return Response.json({ success: true, orderId });
} catch (error) {
log.error({ error, orderId, userId }, '결제 실패');
// P0 알림
Sentry.captureException(error, {
tags: { feature: 'payment', severity: 'critical' },
level: 'fatal',
});
return Response.json({ error: '결제 실패' }, { status: 500 });
}
}
결제가 실패하면 언제든 팀에 즉시 알려집니다.
- 성능 기준선 설정
Sentry Performance Monitoring으로 기준선을 세웁니다.
- 홈페이지 LCP < 2s
- 상품 상세 페이지 LCP < 2.5s
- API /api/products P95 < 500ms
기준선을 넘으면 자동으로 알림을 보냅니다.
효과:
- 장애 발견 시간이 평균 40분에서 3분으로 줄었습니다
- 결제 실패율이 0.8%에서 0.2%로 낮아졌습니다
- 홈페이지 LCP는 최적화 후 3.2s에서 1.8s로 내려갔습니다
모니터링 Checklist
마지막으로 체크리스트를 드립니다. 그대로 프로젝트를 점검해 보세요.
**오류 모니터링**
- [ ] Sentry가 설정되어 있고 테스트를 통과함
- [ ] Source Maps 업로드 성공
- [ ] global-error.tsx 생성 완료(App Router)
- [ ] Server Actions 오류 처리 래핑 완료
- [ ] 무시 규칙 설정 완료(노이즈 필터링)
**로그 관리**
- [ ] 로그 라이브러리 통합 완료(Pino/Winston)
- [ ] 프로덕션 환경 JSON 형식 출력
- [ ] 로그에 correlationId 포함
- [ ] 로그 레벨 올바르게 설정(프로덕션 INFO)
- [ ] 로그 집계 플랫폼 연결 완료
**성능 모니터링**
- [ ] Web Vitals 보고 활성화
- [ ] Core Web Vitals 기준 충족(LCP<2.5s, INP<200ms, CLS<0.1)
- [ ] 핵심 API에 성능 추적 추가
- [ ] 느린 쿼리 모니터링 설정 완료
- [ ] 합성 모니터링 설정 완료(선택)
**알림 설정**
- [ ] Slack/메일 알림 테스트 완료
- [ ] 알림 규칙을 우선순위별로 분류
- [ ] 알림 노이즈 감소 규칙 설정 완료
- [ ] 팀원이 모두 알림 프로세스를 알고 있음
- [ ] P0 사건에 명확한 대응자가 있음
**지속 개선**
- [ ] 매주 모니터링 데이터 회고
- [ ] 오류 추세 분석(어떤 오류가 늘고 있는지)
- [ ] 성능 회귀 감지(어떤 페이지가 느려졌는지)
- [ ] 알림 규칙 정기 최적화
결론
수동적인 진화에서 능동적인 발견으로 넘어갈 때, 모니터링은 프로덕션 환경에 대한 통제력을 줍니다.
우리가 구축한 모니터링 체계를 다시 정리하면 다음과 같습니다.
- Sentry는 오류 추적과 성능 모니터링을 맡고, 사용자 작업도 재생할 수 있습니다
- Pino는 구조화 로그를 제공해 correlationId로 전체 요청 흐름을 연결합니다
- Web Vitals는 사용자 경험 지표를 보며 SEO 순위에도 직접 영향을 줍니다
- Slack 알림은 팀이 문제를 가장 먼저 알게 하고, 등급별 처리로 피로를 줄입니다
더 중요한 것은 사고방식의 변화입니다. 모니터링은 “있으면 더 좋은 것”이 아니라 프로덕션 환경의 에어백입니다. 사고가 난 뒤에 에어백을 설치하지 않듯, 온라인 문제가 난 뒤에야 모니터링을 떠올려서는 안 됩니다.
오늘 바로 시작하세요. 프로젝트에 아직 아무 모니터링도 없다면 이렇게 진행하면 됩니다.
- 이번 주말 2시간을 들여 Sentry를 붙이고 기본 오류 추적을 설정합니다
- 다음 주에는 구조화 로그와 correlationId를 추가합니다
- 그다음 주에는 Slack 알림과 성능 기준선을 설정합니다
처음부터 완벽하게 만들려고 하지 마세요. 가장 기본적인 오류 모니터링을 먼저 동작하게 하고, 이후 단계적으로 보강하면 됩니다. 온라인 장애가 날 때마다 “모니터링이 이 문제를 더 빨리 발견할 수 있었을까?”라고 되묻고 계속 개선하세요. 모니터링은 가장 믿을 만한 동료가 됩니다.
마지막으로 이 글이 도움이 되었다면 팀에 공유해 함께 개선해 보세요. 결국 모니터링은 팀의 일이지 한 사람의 싸움이 아닙니다.
여러분의 Next.js 애플리케이션이 흔들림 없이 안정적으로 운영되기를 바랍니다. 하지만 현실은 종종 기대와 다르니, 그래서 모니터링은 정말 중요합니다.
Next.js 프로덕션 모니터링 전체 설정 절차
Sentry 통합부터 로그 관리, 성능 모니터링, 알림 설정까지 이어지는 전체 단계
⏱️ Estimated time: 3 hr
- 1
Step 1: Sentry 오류 추적 통합
설치:
```bash
npm install @sentry/nextjs
```
초기화:
```bash
npx @sentry/wizard@latest -i nextjs
```
클라이언트 설정:
```ts
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
})
```
서버 설정:
```ts
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
})
```
핵심:
• 클라이언트와 서버 설정을 분리합니다
• tracesSampleRate로 샘플링 비율을 제어합니다
• 환경 변수를 설정합니다 - 2
Step 2: 구조화 로그 설정
correlationId로 요청을 연결합니다:
```ts
// middleware.ts
import { v4 as uuidv4 } from 'uuid'
export function middleware(request: NextRequest) {
const correlationId = request.headers.get('x-correlation-id') || uuidv4()
const response = NextResponse.next()
response.headers.set('x-correlation-id', correlationId)
return response
}
```
로그에서 사용합니다:
```ts
import { headers } from 'next/headers'
export async function handler() {
const headersList = headers()
const correlationId = headersList.get('x-correlation-id')
console.log({
correlationId,
message: '사용자 작업',
timestamp: new Date().toISOString(),
})
}
```
핵심:
• 요청마다 고유 ID를 생성합니다
• 모든 로그에 correlationId를 붙입니다
• 전체 요청 흐름을 추적하기 쉬워집니다 - 3
Step 3: 성능 모니터링 설정
Sentry APM:
```ts
Sentry.init({
tracesSampleRate: 1.0, // 100% 샘플링
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
})
```
커스텀 성능 모니터링:
```ts
const transaction = Sentry.startTransaction({
op: 'http.server',
name: 'API Route',
})
try {
// 비즈니스 로직
await processRequest()
} finally {
transaction.finish()
}
```
핵심:
• tracesSampleRate로 샘플링 비율을 제어합니다
• API 응답 시간을 모니터링합니다
• 성능 병목을 식별합니다 - 4
Step 4: 알림 설정
Sentry 알림:
• Sentry Dashboard에서 알림 규칙을 설정합니다
• 오류 임계값을 설정합니다
• 알림 채널(Slack, 메일 등)을 설정합니다
Slack 통합:
```ts
// Sentry Dashboard에서 설정
// Webhook URL: https://hooks.slack.com/services/...
```
메일 알림:
• Sentry Dashboard에서 설정합니다
• 수신자를 설정합니다
• 알림 조건을 설정합니다
핵심:
• 적절한 알림 임계값을 설정합니다
• 알림 피로를 피합니다
• 알림에 제때 대응합니다
FAQ
Next.js에는 왜 별도의 모니터링 구성이 필요한가요?
같은 애플리케이션이 세 곳에서 동시에 실행됩니다:
• 클라이언트(Browser): 사용자 브라우저의 React 컴포넌트
• 서버(Node.js): SSR 렌더링, API Routes, Server Actions
• 엣지 네트워크(Edge Runtime): 미들웨어, 엣지 함수
기존 프론트엔드 모니터링은 클라이언트 오류만 볼 수 있고, 서버와 엣지 오류는 보지 못합니다.
SSR 블랙박스 효과:
• 서버 사이드 렌더링에서 오류가 나면 사용자는 500 페이지만 봅니다
• stack trace도, 컨텍스트도 없습니다
• 문제를 찾으려면 Sentry 분산 추적이 필요합니다
실제 사례:
• 사용자가 "페이지 로딩이 매우 느리다가 500이 뜬다"고 피드백했습니다
• 브라우저 Network 패널에는 요청이 느리다는 것만 보이고 어디가 느린지는 알 수 없었습니다
• Sentry를 붙인 뒤에야 서버가 호출한 서드파티 API의 응답 시간이 평소 200ms에서 8초로 치솟은 것을 확인했습니다
해결책은 클라이언트, 서버, 엣지 네트워크를 모두 포괄하는 완전한 모니터링 체계입니다.
Sentry는 어떻게 통합하나요?
```bash
npm install @sentry/nextjs
```
초기화:
```bash
npx @sentry/wizard@latest -i nextjs
```
클라이언트 설정:
```ts
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
})
```
서버 설정:
```ts
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
})
```
핵심:
• 클라이언트와 서버 설정을 분리합니다
• tracesSampleRate로 샘플링 비율을 제어합니다
• 환경 변수(NEXT_PUBLIC_SENTRY_DSN, SENTRY_DSN)를 설정합니다
구조화 로그는 어떻게 설정하나요?
middleware에서 생성합니다:
```ts
import { v4 as uuidv4 } from 'uuid'
export function middleware(request: NextRequest) {
const correlationId = request.headers.get('x-correlation-id') || uuidv4()
const response = NextResponse.next()
response.headers.set('x-correlation-id', correlationId)
return response
}
```
로그에서 사용합니다:
```ts
import { headers } from 'next/headers'
export async function handler() {
const headersList = headers()
const correlationId = headersList.get('x-correlation-id')
console.log({
correlationId,
message: '사용자 작업',
timestamp: new Date().toISOString(),
})
}
```
장점:
• 요청마다 고유 ID를 생성합니다
• 모든 로그에 correlationId를 붙입니다
• 전체 요청 흐름을 추적하기 쉽습니다
• 문제를 빠르게 찾을 수 있습니다
핵심은 middleware에서 생성하고 로그에서 사용해 서로 연결하기 쉽게 만드는 것입니다.
성능 모니터링은 어떻게 설정하나요?
```ts
Sentry.init({
tracesSampleRate: 1.0, // 100% 샘플링(프로덕션에서는 0.1 권장)
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
})
```
커스텀 성능 모니터링:
```ts
const transaction = Sentry.startTransaction({
op: 'http.server',
name: 'API Route',
})
try {
// 비즈니스 로직
await processRequest()
} finally {
transaction.finish()
}
```
모니터링 지표:
• API 응답 시간
• 데이터베이스 쿼리 시간
• 서드파티 API 호출 시간
• 페이지 로딩 시간
핵심:
• tracesSampleRate로 샘플링 비율을 제어합니다(프로덕션에서는 0.1 권장)
• 핵심 경로를 모니터링합니다
• 성능 병목을 식별합니다
알림은 어떻게 설정하나요?
• Sentry Dashboard에서 알림 규칙을 설정합니다
• 오류 임계값을 설정합니다(예: 5분 안에 오류 수 > 10)
• 알림 채널(Slack, 메일 등)을 설정합니다
Slack 통합:
• Sentry Dashboard에서 Webhook URL을 설정합니다
• 알림 조건을 설정합니다
• 알림을 테스트합니다
메일 알림:
• Sentry Dashboard에서 설정합니다
• 수신자를 설정합니다
• 알림 조건을 설정합니다
권장 알림 규칙:
• 오류율이 임계값을 넘을 때
• 응답 시간이 임계값을 넘을 때
• 특정 오류 유형이 발생할 때
• 새로운 오류가 처음 나타날 때
핵심:
• 합리적인 알림 임계값을 설정합니다
• 알림 피로를 피합니다
• 알림에 제때 대응합니다
먼저 가장 기본적인 오류 모니터링부터 동작하게 만들고, 이후 단계적으로 보강하는 편이 좋습니다.
모니터링의 모범 사례는 무엇인가요?
1. 이번 주말 2시간을 써서 Sentry를 붙이고 기본 오류 추적을 설정합니다
2. 다음 주에는 구조화 로그와 correlationId를 추가합니다
3. 그다음 주에는 Slack 알림과 성능 기준선을 설정합니다
처음부터 완벽하게 만들려고 하지 말고, 가장 기본적인 오류 모니터링을 먼저 동작하게 만든 뒤 단계적으로 보강합니다.
지속적으로 개선합니다:
• 온라인 장애가 날 때마다 "모니터링이 이 문제를 더 빨리 발견할 수 있었을까?"라고 되묻습니다
• 실제 상황에 맞춰 알림 임계값을 조정합니다
• 모니터링 설정을 정기적으로 검토합니다
핵심 지표:
• 오류율
• 응답 시간
• 사용자 영향 범위
• 복구 시간
권장 사항:
• 모니터링은 팀의 일이지 한 사람의 싸움이 아닙니다
• 모니터링 데이터를 정기적으로 공유합니다
• 모니터링 체계를 계속 개선합니다
기억할 점은 모니터링이 일회성 작업이 아니라 지속적인 과정이라는 것입니다.
6분 읽기 · 게시일: 2025년 12월 20일 · 수정일: 2026년 10월 21일
Next.js 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Vercel을 떠나 Next.js를 Docker로 셀프 호스팅하는 방법
Next.js를 Docker로 셀프 호스팅하는 과정을 설명합니다. standalone 설정, 다단계 Dockerfile, Nginx 리버스 프록시, 스트리밍 렌더링 문제 해결과 비용 비교까지 다룹니다.
51편 중 42편
다음
Next.js 다크 모드 구현: next-themes 완벽 가이드
Next.js에서 next-themes로 깜빡임 없는 다크 모드를 구현하는 방법을 설명합니다. 설정 원리, 전체 코드, 시스템 테마 연동과 자주 발생하는 오류 해결법까지 다룹니다.
51편 중 44편



댓글
GitHub로 로그인하여 댓글을 남기세요