Next.js + Hono 모노레포 환경에 Sentry 도입
도입: 왜 Sentry를 도입하기로 결정했나?
Deep Dive! 블로그의 기능이 점차 확장됨에 따라, 예측하지 못한 에러를 체계적으로 추적하고 관리할 필요성을 느끼게 되었다. 특히 프론트엔드에서 에러가 예상치 못하게 나타나며, 인지하기 어렵다는 문제가 있었다. 단순 console.log만으로는 이러한 '침묵의 오류'들을 모두 파악하기에 한계가 있다고 느꼈다.
중앙화된 에러 및 성능 모니터링 시스템의 필요성을 느끼던 중, Sentry가 개인 프로젝트에 부담 없이 도입할 수 있는 넉넉한 프리티어를 제공한다는 점을 알게 되었다. 이를 계기로 프로젝트의 안정성을 한 단계 높이기 위해 Sentry 도입을 결정했다.
기본 연동: Sentry 마법사와 수동 설정
Sentry 연동은 백엔드(Hono)와 프론트엔드(Next.js)에 각각 진행했다.
백엔드 (Hono) 연동
백엔드는 @sentry/node SDK를 설치하고, 애플리케이션의 진입점인 index.ts에서 Sentry를 초기화했다. 이전에 진행했던 리팩토링 덕분에 중앙화된 글로벌 에러 핸들러(app.onError)가 이미 존재했고, 이곳에 단 한 줄의 코드만 추가하여 모든 백엔드 에러를 Sentry로 전송할 수 있었다.
// apps/backend/src/index.ts import * as Sentry from '@sentry/node'; // ... Sentry.init() ... app.onError((err, c) => { // Sentry로 에러 전송 Sentry.captureException(err); console.error(`[GLOBAL ERROR HANDLER] Path: ${c.req.path}`, err); // ... 기존 에러 처리 로직 ... });


프론트엔드 (Next.js) 연동
프론트엔드는 Sentry가 제공하는 @sentry/wizard CLI 도구를 사용하여 초기 설정을 진행했다. 마법사는 sentry.*.config.ts, next.config.js 등 필요한 파일들을 자동으로 생성하고 수정해주어 매우 편리했다.
하지만 자동 설정 과정에서 한 가지 문제가 발견되었다. 생성된 설정 파일 내부에 DSN 키가 문자열로 하드코딩되어 있었는데, 이는 보안 및 환경별 설정 분리에 적합하지 않았다. 따라서 이 부분을 process.env를 참조하도록 직접 수정하여, DSN 키를 환경 변수로 안전하게 관리하도록 변경했다.
// apps/frontend/sentry.server.config.ts Sentry.init({ // 하드코딩된 DSN 대신 환경 변수를 참조하도록 수정 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 1.0, });
CI/CD 연동: 릴리즈 추적과 소스맵 업로드
단순 에러 수집을 넘어, "어떤 배포 버전에서 발생한 에러인지"를 명확히 추적하기 위해 CI/CD 파이프라인과의 연동을 진행했다. 목표는 배포 시마다 Sentry에 릴리즈 정보를 생성하고, 디버깅에 필수적인 소스맵을 함께 업로드하는 것이다.
과정은 다음과 같았다.
- Sentry에서
SENTRY_AUTH_TOKEN을 발급받는다. - GitHub Actions Secrets에
SENTRY_AUTH_TOKEN과NEXT_PUBLIC_SENTRY_DSN을 등록한다. deploy.yml워크플로우에getsentry/action-release@v1액션을 추가하여, 빌드 시 릴리즈 생성 및 소스맵 업로드를 자동화했다.
이 설정을 통해 Sentry는 에러 리포트에 해당 코드가 배포된 릴리즈 버전을 태그하고, 소스맵을 이용해 압축된 JavaScript 코드의 에러 위치를 원본 TypeScript 코드의 정확한 위치로 보여주게 되었다.


마주한 문제들과 최적화 과정
기본 연동 후 두 가지 예상치 못한 문제가 발생했다.
문제 1: Docker 이미지 크기 급증
Sentry 도입 후, 프론트엔드 Docker 이미지의 크기가 약 35MB 증가했다. 처음에는 소스맵 파일이 이미지에 포함된 것으로 추측하고 .dockerignore에 .map 파일을 추가했지만, 이미지 크기는 변함이 없었다.
분석 결과, 내가 사용하던 Dockerfile은 이미 멀티-스테이지 빌드를 통해 소스맵을 제외하고 있었다. 크기 증가는 @sentry/nextjs SDK 자체와 그 의존성 패키지들의 순수한 용량 때문이었다. 이는 Sentry의 풍부한 기능을 사용하기 위해 감수해야 할 합리적인 트레이드오프라고 판단했다.
문제 2: CI/CD 배포 시간 2분 이상 증가
Sentry 연동 후 배포 시간이 5분대에서 7분 이상으로 크게 늘어나는 문제가 발생했다. 이는 소스맵 업로드 과정이 전체 배포 파이프라인을 차단(blocking)하기 때문이라고 추측했다.
GitHub Actions의 빌드 로그를 자세히 분석한 결과, 범인은 CI/CD 스텝이 아니라 next.config.js의 withSentryConfig 래퍼(wrapper)가 next build 과정에서 자동으로 실행하는 소스맵 업로드 기능임을 발견했다.

이 문제를 근본적으로 해결하기 위해, withSentryConfig라는 "마법"을 걷어내고 수동으로 설정을 제어하기로 결정했다.
next.config.js수정:withSentryConfig를 완전히 제거하고, 소스맵 생성을 위해 Next.js의 공식 옵션인productionBrowserSourceMaps: true만 남겼다.deploy.yml수정: 소스맵 업로드의 책임을 CI/CD 스텝으로 완전히 이전했다.sentry-cli를 직접 호출하고, 셸의 백그라운드 실행 기능(&)을 사용하여 업로드 작업이 다른 배포 작업을 차단하지 않도록 변경했다.
# .github/workflows/deploy.yml - name: "Upload Sourcemaps to Sentry (in Background)" run: | npm install -g @sentry/cli sentry-cli releases files $SENTRY_RELEASE upload-sourcemaps ./apps/frontend/.next --org $SENTRY_ORG --project $SENTRY_PROJECT &


결론: Sentry, 제대로 사용하기
Sentry 도입은 단순히 SDK를 설치하는 것에서 끝나지 않았다. CI/CD 파이프라인과 빌드 프로세스에 대한 깊은 이해를 바탕으로 최적화를 진행하는 과정이 필수적이었다.
@sentry/wizard와 withSentryConfig는 초기 설정에 매우 편리한 도구지만, 복잡한 커스텀 환경에서는 오히려 내부 동작을 파악하기 어렵게 만들 수 있다. 이번 경험을 통해, 때로는 자동화된 "마법"을 걷어내고 수동으로 제어권을 가져오는 것이 더 나은 성능과 안정성을 제공할 수 있음을 배우게 되었다.
최종적으로, 빌드 시간은 Sentry 도입 이전 수준으로 원복하면서 에러 추적, 릴리즈 관리, 소스맵 연동이라는 모든 목표를 달성할 수 있었다. 앞으로 Sentry를 통해 얻게 될 안정성이 블로그의 성장에 든든한 기반이 되어줄 것이라 기대한다.
