본문 바로가기
💻 개발/투데잇 - AI 자동 식단 일기

투데잇: 개발 환경에서 Sentry 비활성화하기 - 문제와 해결 과정

by llddang 2025. 4. 2.

안녕하세요! 오늘은 개발 환경에서는 Sentry를 비활성화하고 프로덕션 환경에서만 활성화하는 방법과, 해당 작업을 수행하며 겪은 오류 및 해결 과정을 공유하고자 합니다.

 

Sentry란?

먼저, Sentry는 애플리케이션의 오류를 실시간으로 모니터링하고 추적하는 도구입니다. 애플리케이션에서 발생하는 예외를 캡처하여 개발자에게 알림을 보내고, 오류의 원인을 분석할 수 있는 상세 정보를 제공합니다. Next.js를 포함한 다양한 프레임워크와 통합하여 사용할 수 있으며, 오류 추적뿐만 아니라 성능 모니터링 기능도 제공합니다.

sentry 대시보드 예제

 

 

 

왜 개발 환경에서 Sentry를 비활성화하려고 했나요?

개발 환경에서 Sentry를 비활성화하려는 데에는 몇 가지 중요한 이유가 있습니다:

 

1. 빌드 시간 단축

Sentry는 소스맵을 생성하고 업로드하는 과정을 포함하므로, 개발 환경에서 이를 비활성화하면 빌드 시간을 단축할 수 있습니다. 소스맵 생성은 특히 대규모 프로젝트에서 상당한 시간이 소요될 수 있습니다.

2. 개발 환경 성능 향상

Sentry 통합은 추가적인 런타임 오버헤드를 발생시킬 수 있으므로, 개발 중에는 이를 제거하여 더 빠른 피드백 루프를 구축할 수 있습니다. 이는 hot-reload 성능 향상으로 이어져 개발 효율성을 높입니다.

이러한 이유로 환경별 분기 처리를 시도하게 되었습니다.

 

 

문제 해결 과정

과정 1: withSentryConfig 조건부 적용

첫 번째 접근법은 Next.js 설정에서 withSentryConfig를 조건부로 적용하는 것이었습니다.

import { withSentryConfig } from '@sentry/nextjs';

/** @type {import('next').NextConfig} */
let nextConfig = {};

if (process.env.SENTRY === 'true') {
  nextConfig = withSentryConfig(nextConfig, {
    // Sentry 설정...
  });
}

export default nextConfig;

 

이 코드는 SENTRY 환경 변수가 'true'로 설정된 경우에만 Sentry 설정을 적용합니다. 처음에는 NODE_ENV 환경 변수를 사용하려고 했지만, Next.js에서는 mode를 통해 production, development를 직접 설정할 수 없었습니다. 따라서 별도의 SENTRY 환경변수를 만들어 제어하기로 했습니다.

 

withSentryConfig에 조건을 적용함으로써 개발 환경에서는 Sentry 관련 빌드 프로세스를 완전히 건너뛰게 되어, 빌드 시간 단축과 함께 런타임에서의 Sentry 코드 실행도 방지할 수 있을 것으로 기대했습니다.

 

하지만 다음과 같은 오류가 발생했습니다:

Critical dependency: require function is used in a way in which dependencies cannot be statically extracted

 

이 오류는 코드 내에서 Sentry를 사용하고 있지만, Next.js 구성에서 설정되어 있지 않아서 발생하는 것이었습니다.
웹팩이 정적 분석을 통해 의존성을 추출할 수 없을 때 발생하는 오류로, 동적 import나 조건부 require가 사용되는 경우에 나타날 수 있습니다.

 

과정 2: Adapter 패턴 사용

이 문제를 해결하기 위해 Adapter 패턴을 사용하기로 결정했습니다.

Adapter 패턴은 인터페이스가 호환되지 않는 클래스들이 함께 작동할 수 있도록 하는 구조적 디자인 패턴입니다.

let sentry = {
  captureException(error: Error) {}
};

if (process.env.SENTRY === 'true') {
  sentry = await import('@sentry/nextjs');
}

export default sentry;

 

이 접근 방식은 개발 환경에서는 빈 구현(아무 동작도 하지 않는 captureException 함수)을 제공하고, 프로덕션 환경에서만 실제 Sentry를 동적으로 가져오는 방식입니다. 이렇게 하면 코드 어디서든 sentry.captureException(error)와 같은 방식으로 일관되게 사용할 수 있습니다.

 

하지만 이번에는 TypeScript 오류가 발생했습니다:

최상위 'await' 식은 'module' 옵션이 'es2022', 'esnext', 'system', 'node16', 'node18', 'nodenext' 또는 'preserve'로 설정되고 'target' 옵션이 'es2017' 이상으로 설정된 경우에만 허용됩니다.ts(1378)

 

이 오류는 최상위 레벨에서 await을 사용하기 위해서는 TypeScript의 설정이 필요하다는 것을 의미합니다.
Top-level await는 ES2022에서 도입된 기능으로, 모듈의 최상위 레벨에서 비동기 작업을 기다릴 수 있게 해줍니다.

 

이 문제를 해결하기 위해 tsconfig.json에 다음과 같은 설정을 추가했습니다:

{
  "compilerOptions": {
    "target": "ESNext",
    // 다른 설정들...
  }
}

module 옵션은 이미 esnext로 설정되어 있었기 때문에, target 옵션만 추가했습니다.

 

그러나 또 다른 오류가 발생했습니다:

The generated code contains 'async/await' because this module is using "topLevelAwait".
However, your target environment does not appear to support 'async/await'.

이 오류는 sentryDev.ts 파일에서 top-level await 사용과 관련된 문제입니다. 일반적인 TypeScript 파일에서 top-level await를 사용하려면, 특정 컴파일러 설정이 필요합니다.

 

 

CommonJS vs ESM

여기서 잠시 두 가지 주요 JavaScript 모듈 시스템에 대해 설명하겠습니다:

  1. CommonJS (CJS): Node.js에서 전통적으로 사용되는 모듈 시스템으로, require()module.exports를 사용합니다. 동기적으로 작동하며 top-level await를 지원하지 않습니다.
  2. ECMAScript Modules (ESM): 최신 JavaScript 표준의 모듈 시스템으로, importexport 구문을 사용합니다. 비동기적으로 작동하며 top-level await를 지원합니다.

Next.js는 이 두 시스템을 모두 지원하지만, 서버 사이드 렌더링 환경은 기본적으로 CommonJS로 설정되어 있습니다. 따라서 TypeScript 설정에서 module: "esnext"target: "ESNext"를 설정했다 하더라도, Next.js의 서버 사이드 빌드 프로세스는 이 파일을 CommonJS 형식으로 처리하게 됩니다.


해당 내용은 package.json"type": "module"을 추가하여 프로젝트 전체를 ESM 모드로 전환함으로써 이 문제를 해결할 수도 있었을 것입니다. 하지만 이 접근법은 commonJS를 사용하는 라이브러리들과 호환성 문제가 발생할 수 있기 때문에, 서버 사이드에서 사용되는 sentryDev.ts 파일에서 top-level await를 사용하는 대신, 웹팩 alias와 같은 다른 접근법으로 문제를 해결하기로 결정했습니다.

 

과정 3: 웹팩 alias 사용하기

마지막으로, 웹팩의 alias 기능을 사용하여 환경에 따라 다른 구현을 가리키도록 했습니다. 웹팩 alias는 import 경로를 다른 경로로 매핑하는 기능으로, 동일한 import 구문이 환경에 따라 다른 모듈을 가리키게 할 수 있습니다.

먼저, 개발 환경에서 사용할 mock Sentry 모듈을 별도의 파일에 정의했습니다:

// src/sentryDev.ts
/* eslint-disable @typescript-eslint/no-unused-vars */
export const captureException = (error: Error) => {};

 

이 파일은 Sentry의 captureException 함수를 모방하지만 실제로는 아무 동작도 하지 않는 빈 함수를 제공합니다.

 

그리고 Next.js 설정에서 웹팩 alias를 사용하여 개발 환경에서 @sentry/nextjs를 이 mock 파일로 리다이렉트했습니다:

/** @type {import('next').NextConfig} */
let nextConfig = {
  webpack: (config) => {
    if (process.env.SENTRY !== 'true') {
      config.resolve.alias['@sentry/nextjs'] = require.resolve('./src/sentryDev.ts');
    }
    return config;
  }
};

 

이 접근법은 개발 환경에서 @sentry/nextjs 가져오기를 가짜 구현으로 리다이렉트하는 방식입니다. 웹팩의 resolve.alias 옵션은 특정 모듈 경로를 다른 경로로 매핑할 수 있게 해줍니다.

 

하지만 이번에는 Node.js ESM 환경에서는 require를 직접 사용할 수 없는 새로운 문제가 발생했습니다:

ReferenceError: require is not defined

 

ESM에서는 require 함수를 직접 사용할 수 없기 때문에 발생하는 오류입니다.

 

마지막 과정: createRequire 사용

이 문제를 해결하기 위해 Node.js의 createRequire 함수를 사용했습니다:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

/** @type {import('next').NextConfig} */
let nextConfig = {
  webpack: (config) => {
    if (process.env.SENTRY !== 'true') {
      config.resolve.alias['@sentry/nextjs'] = require.resolve('./src/sentryDev.ts');
    }
    return config;
  }
};

 

createRequire는 ESM 환경에서도 CommonJS 스타일의 require 함수를 생성할 수 있게 해주는 Node.js API입니다. import.meta.url은 현재 모듈의 URL을 제공하여 createRequire가 적절한 기준점을 가질 수 있게 합니다.

 

최종 구현

최종적으로 채택한 방식은 adaptor, 웹팩 alias, createRequire를 함께 사용하는 것이었습니다:

// src/sentryDev.ts
/* eslint-disable @typescript-eslint/no-unused-vars */
export const captureException = (error: Error) => {};
// 필요한 다른 Sentry 함수들도 여기에 빈 구현으로 추가할 수 있습니다


// next.config.js
import { withSentryConfig } from '@sentry/nextjs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

/** @type {import('next').NextConfig} */
let nextConfig = {
  webpack: (config) => {
    if (process.env.SENTRY !== 'true') {
      config.resolve.alias['@sentry/nextjs'] = require.resolve('./src/sentryDev.ts');
    }
    return config;
  }
};

// Sentry 설정은 항상 적용하지만, 개발 환경에서는 가짜 구현을 사용합니다
if (process.env.SENTRY === 'true') {
  nextConfig = withSentryConfig(nextConfig, {
    // Sentry 설정...
    silent: true, // 로그 출력 제어
    org: "your-org",
    project: "your-project",
    // 그 외 필요한 설정들...
  });
}

export default nextConfig;

이 방식의 작동 원리는 다음과 같습니다:

  1. 환경 변수 SENTRY가 'true'가 아닌 경우, 웹팩의 alias 기능을 사용하여 @sentry/nextjs 모듈의 import를 우리가 만든 가짜 구현(sentryDev.ts)으로 리다이렉트합니다.
  2. 프로덕션 환경(SENTRY=true)에서는 실제 Sentry 구성이 적용됩니다.
  3. createRequire를 사용하여 ESM 환경에서도 require.resolve를 사용할 수 있게 합니다.

이 접근 방식의 장점은 코드 변경 없이 환경 변수만으로 Sentry의 활성화/비활성화를 제어할 수 있다는 점입니다. 코드에서는 항상 동일한 방식으로 Sentry를 사용할 수 있으며, 런타임에 환경에 따라 적절한 구현이 가능합니다.

 

성능 개선 결과

이 구현을 적용한 후 개발 환경에서의 성능 개선을 측정했습니다:

측정 항목 개선 전 개선 후 개선율
개발 서버 시작 시간 5 초 1.618 초 67.64%
빌드 시간 40 초 20 초 50%

 

개발 서버의 시작 시간과 빌드 시간이 크게 개선되어 개발 경험이 향상되었습니다.

 

 

결론

Next.js와 Sentry의 통합은 강력하지만, 개발 워크플로우에 맞게 조정하는 과정에서 예상치 못한 문제들을 만날 수 있습니다. 위의 접근법을 통해 개발 환경에서는 Sentry를 비활성화하고 프로덕션 환경에서만 활성화하는 효율적인 방법을 구현할 수 있었습니다.

이 방법은 개발 속도를 향상시키고, Sentry 할당량을 절약하며, 더 깨끗한 오류 로그를 유지하는 데 도움이 됩니다. 또한, 이 과정에서 JavaScript 모듈 시스템, 웹팩 구성, Next.js의 동작 방식에 대한 더 깊은 이해를 얻을 수 있었습니다.

 

 

 

참고 문헌: