뒤로가기
프론트엔드 에러 모니터링 시스템 구축기

April 15, 2024

frontenddevops

에러가 터져도 몰랐다. 사용자가 CS팀에 "화면이 안 떠요"라고 문의하면, CS팀이 슬랙에 올리고, 개발팀이 재현하려고 이것저것 시도해보고, 결국 콘솔에서 에러를 발견하는 흐름이었다. 사용자가 문의하지 않으면? 그냥 조용히 묻혔다.

이 흐름을 바꾸기로 했다. 에러가 발생하면 개발팀이 사용자보다 먼저 알아야 한다.

Sentry 도입#

에러 모니터링 도구로는 Sentry를 골랐다. 이유는 단순하다. React, Next.js 공식 SDK가 잘 되어 있고, 소스맵 연동이 편하고, 무료 플랜으로 시작할 수 있다.

Next.js에 Sentry를 붙이는 건 @sentry/nextjs 하나면 된다:

bash
npx @sentry/wizard@latest -i nextjs

위저드가 sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts, next.config.js 설정을 자동으로 만들어준다.

typescript
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1, // 성능 트레이싱 10%만 샘플링
  replaysSessionSampleRate: 0,
  replaysOnErrorSampleRate: 1.0, // 에러 발생 시 세션 리플레이 100% 수집
});

tracesSampleRate를 1.0으로 하면 모든 요청의 성능 데이터를 수집하는데, 프로덕션에서는 비용과 성능에 영향을 준다. 0.1~0.2 정도가 적당하다.

소스맵 연동#

프로덕션 빌드는 코드가 minify되어 있어서 에러 스택트레이스가 이렇게 보인다:

text
TypeError: Cannot read properties of undefined (reading 'map')
    at a.render (main-abc123.js:1:23456)

어디서 터진 건지 전혀 모른다. 소스맵을 Sentry에 업로드하면 원본 코드 위치로 매핑해준다:

text
TypeError: Cannot read properties of undefined (reading 'map')
    at PostList (src/components/PostList.tsx:24:18)

@sentry/nextjs의 webpack 플러그인이 빌드 시 자동으로 소스맵을 업로드한다. next.config.js에서 withSentryConfig로 감싸면 끝:

javascript
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig(nextConfig, {
  sourcemaps: {
    deleteSourcemapsAfterUpload: true, // 업로드 후 소스맵 삭제 (보안)
  },
});
Warning

소스맵을 프로덕션 서버에 남겨두면 누구나 원본 코드를 볼 수 있다. deleteSourcemapsAfterUpload: true로 Sentry에만 업로드하고 서버에서는 삭제하자.

노이즈 필터링#

Sentry를 켜자마자 에러가 쏟아졌다. 하루에 수백 건. 대부분은 의미 없는 노이즈였다.

  • 크롬 확장 프로그램이 주입한 스크립트 에러
  • 봇 크롤러의 JavaScript 실행 실패
  • 네트워크 순단으로 인한 fetch 실패
  • 사용자가 탭을 닫으면서 발생하는 AbortError

이것들을 걸러내지 않으면 진짜 중요한 에러가 묻힌다.

typescript
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  ignoreErrors: [
    'AbortError',
    'ResizeObserver loop',
    'Non-Error promise rejection',
    /^Loading chunk \d+ failed/,
    /^Network request failed/,
  ],
  beforeSend(event) {
    // 크롬 확장 프로그램에서 발생한 에러 필터링
    const frames = event.exception?.values?.[0]?.stacktrace?.frames;
    if (frames?.some((frame) => frame.filename?.includes('extension://'))) {
      return null;
    }
    return event;
  },
});

ignoreErrors로 패턴 매칭하고, beforeSend에서 더 세밀한 필터링을 했다. 이걸로 노이즈의 80% 정도가 사라졌다.

의미 있는 컨텍스트 추가#

에러 메시지만으로는 원인을 파악하기 어려울 때가 많다. "Cannot read properties of null"이 뜨면, 뭐가 null인데? 어떤 상황에서?

사용자 컨텍스트와 브레드크럼을 추가했다:

typescript
// 로그인 후 사용자 정보 설정
Sentry.setUser({
  id: user.id,
  segment: user.plan, // free, pro, enterprise
});

// 중요한 액션에 브레드크럼 추가
function handlePayment(amount: number) {
  Sentry.addBreadcrumb({
    category: 'payment',
    message: `결제 시도: ${amount}`,
    level: 'info',
  });
  // ...결제 로직
}

에러가 발생하면 Sentry 대시보드에서 "이 사용자가 어떤 경로로 이 페이지에 왔고, 어떤 버튼을 눌렀고, 어떤 API를 호출했는지" 타임라인으로 볼 수 있다.

슬랙 알림 파이프라인#

Sentry에 에러가 쌓이는 것만으로는 부족하다. 팀이 Sentry 대시보드를 매일 확인할 리가 없으니까.

Sentry의 Alert Rules를 설정해서 슬랙으로 알림이 오게 했다:

  • 새로운 이슈 발생: 처음 보는 에러가 생기면 즉시 알림
  • 이슈 회귀: 해결했던 에러가 다시 발생하면 알림
  • 빈도 임계값: 같은 에러가 1시간에 50건 이상이면 알림 (대규모 장애 감지)

모든 에러를 다 슬랙에 보내면 채널이 묘지가 된다. "새로운 이슈"와 "회귀"만 보내는 게 핵심이다. 이미 알고 있는 에러는 Sentry에서 확인하면 된다.

에러 바운더리와 연동#

React Error Boundary에서 Sentry에 추가 정보를 보내도록 했다:

typescript
import * as Sentry from '@sentry/nextjs';

class ErrorBoundary extends React.Component<Props, State> {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    Sentry.withScope((scope) => {
      scope.setTag('boundary', this.props.name);
      scope.setExtra('componentStack', errorInfo.componentStack);
      Sentry.captureException(error);
    });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

어떤 Error Boundary에서 잡혔는지 태그로 남기니까, "결제 섹션에서 에러가 집중되고 있다" 같은 패턴을 빠르게 파악할 수 있었다.

도입 후#

에러 발생부터 인지까지의 시간이 "CS 문의가 올 때까지"에서 "수 분 이내"로 줄었다. 배포 직후 에러가 급증하면 바로 알 수 있으니까 빠른 롤백 판단도 가능해졌다.

팀이 에러를 대하는 태도가 바뀌었다. 예전에는 "에러가 있을 수도 있다" 정도의 막연한 불안이었다면, 이제는 "현재 미해결 이슈가 3건이고, 모두 낮은 빈도"라는 정량적인 상태 파악이 가능하다. 모르는 것보다 아는 게 낫다.