안녕하세요! 오늘은 개발 환경에서는 Sentry를 비활성화하고 프로덕션 환경에서만 활성화하는 방법과, 해당 작업을 수행하며 겪은 오류 및 해결 과정을 공유하고자 합니다.
Sentry란?
먼저, Sentry는 애플리케이션의 오류를 실시간으로 모니터링하고 추적하는 도구입니다. 애플리케이션에서 발생하는 예외를 캡처하여 개발자에게 알림을 보내고, 오류의 원인을 분석할 수 있는 상세 정보를 제공합니다. Next.js를 포함한 다양한 프레임워크와 통합하여 사용할 수 있으며, 오류 추적뿐만 아니라 성능 모니터링 기능도 제공합니다.
왜 개발 환경에서 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 모듈 시스템에 대해 설명하겠습니다:
- CommonJS (CJS): Node.js에서 전통적으로 사용되는 모듈 시스템으로,
require()
와module.exports
를 사용합니다. 동기적으로 작동하며 top-level await를 지원하지 않습니다. - ECMAScript Modules (ESM): 최신 JavaScript 표준의 모듈 시스템으로,
import
와export
구문을 사용합니다. 비동기적으로 작동하며 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;
이 방식의 작동 원리는 다음과 같습니다:
- 환경 변수
SENTRY
가 'true'가 아닌 경우, 웹팩의 alias 기능을 사용하여@sentry/nextjs
모듈의 import를 우리가 만든 가짜 구현(sentryDev.ts
)으로 리다이렉트합니다. - 프로덕션 환경(
SENTRY=true
)에서는 실제 Sentry 구성이 적용됩니다. createRequire
를 사용하여 ESM 환경에서도require.resolve
를 사용할 수 있게 합니다.
이 접근 방식의 장점은 코드 변경 없이 환경 변수만으로 Sentry의 활성화/비활성화를 제어할 수 있다는 점입니다. 코드에서는 항상 동일한 방식으로 Sentry를 사용할 수 있으며, 런타임에 환경에 따라 적절한 구현이 가능합니다.
성능 개선 결과
이 구현을 적용한 후 개발 환경에서의 성능 개선을 측정했습니다:
측정 항목 | 개선 전 | 개선 후 | 개선율 |
개발 서버 시작 시간 | 5 초 | 1.618 초 | 67.64% |
빌드 시간 | 40 초 | 20 초 | 50% |
개발 서버의 시작 시간과 빌드 시간이 크게 개선되어 개발 경험이 향상되었습니다.
결론
Next.js와 Sentry의 통합은 강력하지만, 개발 워크플로우에 맞게 조정하는 과정에서 예상치 못한 문제들을 만날 수 있습니다. 위의 접근법을 통해 개발 환경에서는 Sentry를 비활성화하고 프로덕션 환경에서만 활성화하는 효율적인 방법을 구현할 수 있었습니다.
이 방법은 개발 속도를 향상시키고, Sentry 할당량을 절약하며, 더 깨끗한 오류 로그를 유지하는 데 도움이 됩니다. 또한, 이 과정에서 JavaScript 모듈 시스템, 웹팩 구성, Next.js의 동작 방식에 대한 더 깊은 이해를 얻을 수 있었습니다.
참고 문헌:
'💻 개발 > 투데잇 - AI 자동 식단 일기' 카테고리의 다른 글
useFunnel 훅 트러블 슈팅: 리렌더링 및 Hydration 문제 해결 (0) | 2025.04.30 |
---|---|
Next.js에서 단계적 사용자 경험을 위한 useFunnel 훅 구현하기 (0) | 2025.04.29 |
투데잇: 우리 프로젝트의 코드 컨벤션 가이드 (0) | 2025.04.02 |