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

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건이고, 모두 낮은 빈도"라는 정량적인 상태 파악이 가능하다. 모르는 것보다 아는 게 낫다.