최근 회사 프로젝트의 GCP 클라우드 로그 중에 서버 컴포넌트 관련 에러가 꽤 잡혀 나왔다. 전부터 해당 로그가 계속 쌓이고 있긴 했는데 문제는 소스맵을 활성화하지 않았기 때문에 정확한 에러 발생 위치를 잡을 수가 없다는 것. ㄸㄹㄹ...
아무래도 무작정 해당 에러의 발생 위치를 찾는 건 모래사장에서 바늘 찾기인지라 일단 의심 되는 Date 객체 관련 부분만 수정해보기로 했는데, 이게 근본적인 해결책은 절대 아니었다. 소스맵을 활성화하든지, 아니면 다른 방법을 찾아야 했다.
하지만 소스맵을 아예 공개하는 건 보안 문제나 여러 가지 문제가 있을 수 있어서 처음부터 선택지에서 제외했다.
그렇다면 남는 건 Sentry 같은 걸 도입하는 방법 뿐.
이전 회사에서도 Sentry를 썼었고, 다른 팀원들도 Sentry 경험이 다들 있었어서 일단 Sentry를 도입하기로 그렇게 결정도 빠르게 땅땅!
그런데 아직 설정할 시간이 없어서 못하고 있다....
그래서 시간이 나면 도입할 때 참고를 해보려고 Next.js 14 App Router기반 프로젝트에 Sentry를 도입하는 방법을 미리 GPT의 도움을 받아서 알아본 내용을 정리해보려고 한다.
Sentry 공식문서를 보면, 아주 친절하게 쉬운 도입 방법을 제시하고 있다. 바로 마법사를 설치하는 방법이다.
아래 명령어는 Sentry 마법사를 설치하는 명령어이다.
npx @sentry/wizard@latest -i nextjs
Sentry 마법사가 설치되면, 프로젝트에 자동으로 아래와 같은 변화가 적용된다고 한다.
그러니까 간단히 말해서 Sentry 마법사가 기본적으로 필요한 설정들을 알아서 추가해준다는 것이다.
그렇다면 이걸로 끝일까?
끝이면 좋겠지만, 그렇게 간단하면 얼마나 좋게요.
Sentry에만 소스맵을 업로드할 수 있도록 환경 변수를 설정하는 등 추가적인 설정은 수동으로 해주어야 한다.
그 중에서 먼저, Sentry 토큰값과 업로드할 소스맵 지정 관련 환경변수 설정부터 해보자. 환경변수는 아래와 같이 설정할 수 있다.
# 런타임
NEXT_PUBLIC_SENTRY_DSN=<your_dsn>
SENTRY_DSN=<your_server_dsn>
SENTRY_ENVIRONMENT=production
# CI(소스맵 업로드)
SENTRY_AUTH_TOKEN=<ci_auth_token>
SENTRY_ORG=<org_slug>
SENTRY_PROJECT=<project_slug>
next.config.ts — 소스맵 비공개 업로드 + 선택적 터널이제 추가로 수동 설정을 해주기 전에 Sentry SDK도 설치해주자.
npm install @sentry/nextjs --save
설치가 완료되었으면, 앞서 Sentry 마법사가 기본 설정을 적용한 next config부터 손볼 차례이다.
next config에는 이미 withSentryConfig가 적용되어 있을 것이다. withSentryConfig 내부에는 위에서 미리 작성해둔 환경변수를 사용하여 org, project 등에 값을 넣어주면 된다.
아래 코드는 Sentry 공식문서의 예시 코드이다.
// next.config.ts
import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";
const nextConfig: NextConfig = {
// …기존 설정
};
export default withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
// Only print logs for uploading source maps in CI
// Set to `true` to suppress logs
silent: !process.env.CI,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
});
위 예시 코드에서 silent는 빌드 시 Sentry SDK가 출력하는 빌드 로그를 모두 숨기고 싶을 때 true로 설정하면, CI 로그 노이즈가 줄어든다. 런타임 동작에는 영향이 없고, 기본값은 false이다.
disableLogger는 true로 설정하면, 번들 사이즈를 줄이기 위해서 Sentry SDK 내부 로거 코드를 자동으로 트리셰이킹한다.
이외에도 다른 옵션들이 많은데, 다른 기술 블로그 등 실제 도입 사례를 보면 공식문서에서 소개하는 이 정도의 옵션만 바꾸는 정도를 추천하는 것 같다. 아무래도 다른 옵션 바꾸면 문제가 생길 수 있는 것 같다^ㅁ^
여기에 추가로 Sentry 공식문서에서는 Next.js 14 App Router에 분산 트레이싱을 적용하는 방법도 알려준다.
아래 예시 코드처럼 간단하게 루트 레이아웃의 generateMetadata() 함수 내부의 옵션에 Sentry 함수 하나를 적용하면 된다.
import * as Sentry from "@sentry/nextjs";
import type { Metadata } from "next";
export function generateMetadata(): Metadata {
return {
// ... your existing metadata
other: {
...Sentry.getTraceData(),
},
};
}
이제 기본적인 설정이 끝났으니, Sentry를 사용하기 위해 초기화를 해주어야 한다.
클라이언트용과 서버용, 그리고 엣지용 초기화 config 파일이 각각 존재하기 때문에 각각 초기화 코드를 작성해야 한다.
모든 초기화 예시코드는 Sentry 공식문서에 나와있는 것을 그대로 가져왔고, dsn 부분만 앞서 환경변수 파일에 작성한 환경변수를 적용해두었다.
추가로 필요한 옵션들은 공식문서와 다른 예제들을 참고하여 작성하면 된다.
// instrumentation-client.(js|ts)
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
// This export will instrument router navigations, and is only relevant if you enable tracing.
// `captureRouterTransitionStart` is available from SDK version 9.12.0 onwards
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
클라이언트용 파일명은 instrumentation-client.(js|ts)인데, Sentry 이전 버전에서는 파일명이 sentry.client.config.(js|ts)였는지, 만약 파일명이 이렇게 되어 있다면 Next.js 전체 버전에서 적용 가능한 instrumentation-client.(js|ts)로 바꿔야 한다.
클라이언트 초기화 코드에는 주석으로도 작성되어 있듯이, 클라이언트 라우팅 계측을 위해 instrumentation-client.(js|ts)에 export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; 작성이 필요하다.
// sentry.server.config.(js|ts)
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
// that it will also get attached to your source maps
});
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
// that it will also get attached to your source maps
});
앞서 초기화 코드를 모두 작성했으면, 이제 Sentry SDK의 서버 측 초기화를 등록해주어야 한다.
Sentry 공식문서에서는 instrument.(js|ts) 파일에서 아래와 같이 간단하게 등록 코드를 작성하면 된다고 안내되어 있다.
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
여기까지 왔으면 Sentry를 사용하기 위한 설정이 거의 다 끝났다고 볼 수 있다.
그럼 Sentry에 이벤트를 보내기 위한 코드를 또 작성해야겠쥬?
여러 이벤트들이 있겠지만, GPT의 추천을 받아 몇 가지 예시를 가져와 보았다.
라우트 핸들러는 자동 계측으로 충분한 경우가 많지만, 더 세밀한 제어가 필요할 수 있다. 그럴 때에는 아래와 같이 적용할 수 있다고 한다.
// app/api/notes/route.ts
import { NextRequest } from "next/server";
import { wrapRouteHandlerWithSentry } from "@sentry/nextjs";
async function post(req: NextRequest) {
// …업무 로직(throw 시 자동 보고)
return new Response("ok");
}
export const POST = wrapRouteHandlerWithSentry(post, {
method: "POST",
parameterizedRoute: "/api/notes",
});
서버 액션은 현재 우리 서비스에서는 로그인 쪽에서 쓰는 게 전부이긴 하지만, 혹시 추후 필요할까 싶어서 일단 기록해둔다.
"use server";
import * as Sentry from "@sentry/nextjs";
export async function createOrder(form: FormData) {
return Sentry.withServerActionInstrumentation("createOrder", {}, async () => {
// …업무 로직
return { ok: true };
});
}
에러 바운더리에서 잡은 에러들은 Sentry에 수동으로 이벤트 전송이 가능하다.
에러 바운더리에서 잡힌 에러는 대부분 클라우드 로그에도 잘 로깅되고 있긴 해서 필요할까 싶긴 하지만, 인생 어떻게 될지 모르니까...^_T
// app/error.tsx
"use client";
import * as Sentry from "@sentry/nextjs";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
Sentry.captureException(error);
return <button onClick={reset}>다시 시도</button>;
}
5번까지 정리한 내용들만 해도 Sentry를 도입하고 사용하기 위한 기본적인 것들은 대충 포함되어 있지만, 추가로 알아두거나 적용하면 좋을 것 같은 사항들이 있어서 가져와보았다.
특히나 운영 환경에서는 신경 쓸 것들이 더 많다 보니 알아둬서 나쁠 것은 없을 것 같다.
운영 환경에서는 헬스체크, 확장 프로그램, 터널 경로 등 불필요한 이벤트를 막고 PII를 마스킹해야 한다. 이를 적용하기 위해서는 아래 예시코드처럼 작성할 수 있다.
// sentry.client.config.ts / sentry.server.config.ts
Sentry.init({
// ...
beforeSend(event) {
const url = event.request?.url || "";
if (url.includes("/healthz") || url.includes("/monitoring")) return null; // 불필요 이벤트 제거
// PII 마스킹 예시
if (event.user?.email) event.user.email = "[redacted]";
return event;
},
denyUrls: [
/extensions\//i,
/chrome\-extension:/i,
/safari-web-extension:/i,
],
});
throw new Error("Sentry test")를 추가instrumentation-client export를 확인/monitoring 등) 예외 처리.tracesSampleRate 0.05~0.1) 후 필요에 따라 조정.wrapRouteHandlerWithSentry).이렇게 열심히 정리는 해두었는데 이건 또 언제 적용할 수 있을까? 하는 걱정이 먼저 드는 나, 정상인가요? 정말이지 할 게 너무 많아 슬픈 중생...
여튼 조금 업무가 한가해지면 후다닥 도입하고 끝내버려야지. 근데 한가해질 때가 올까...?