Sentry 커스텀 계측으로 FE 무결성을 증명한 로그인 풀림 트러블슈팅 (일 400건)

Sentry 커스텀 계측으로 FE 무결성을 증명한 로그인 풀림 트러블슈팅 (일 400건)

Sentry 커스텀 계측으로 FE 무결성을 증명한 로그인 풀림 트러블슈팅 (일 400건)

기간: 2026-03-21 ~ 2026-03-24 규모: 1,200건 이상 (하루 약 400건 지속 발생) 결론: Sentry 커스텀 계측으로 FE 무결성을 데이터로 증명 → 백엔드 코드 버그 확정 및 수정


1. 문제 상황

앱/웹에서 로그인이 간헐적으로 풀리는 현상이 다수 사용자에게 발생했다. 재현 조건이 불명확하고 FE / BE / 앱 어느 쪽이 원인인지 특정하기 어려운 상태였다.

초기 가능성으로 열어둔 원인:

  • FE 토큰 갱신 로직 버그
  • 동시 다중 401 처리의 race condition
  • 앱(WebView) 전용 이슈
  • BE refreshToken 관리 정책 문제

2. 접근 방식: 먼저 계측, 그다음 분석

원인이 불명확한 상태에서 코드를 건드리는 대신, Sentry 커스텀 계측을 먼저 설계해 실제 사용자 환경의 데이터를 수집했다.

2-1. captureMessage로 실패 이벤트 생성

강제 로그아웃이 발생하는 logoutAndRedirect 시점에 토큰의 발급/만료 시각, 환경, 실패 이유를 모두 기록했다.

Sentry.captureMessage('token_refresh_failed: 토큰 갱신 실패로 강제 로그아웃', { level: 'warning', extra: { reason, // 실패 분류 (C999, network_error 등) memberNo, isApp: isApp(), isOnline: navigator.onLine, accessExpiry: getTokenExp(accessToken), // JWT decode로 만료 시각 추출 refreshExpiry: getTokenExp(refreshToken), accessIssuedAt: getTokenIat(accessToken), // 발급 시각 → 이전 갱신 이력 추적 refreshIssuedAt: getTokenIat(refreshToken), at: maskToken(accessToken), // 뒤 10자 마스킹 rt: maskToken(refreshToken) } });

설계 의도: accessIssuedAt vs refreshIssuedAt가 다르면 이전에 갱신 성공 이력이 있다는 뜻. 이 필드 하나로 "처음 시도라서 실패했다" 가설을 데이터로 반박할 수 있었다.

2-2. addBreadcrumb로 전체 흐름 추적

토큰 갱신 과정의 모든 분기점에 브레드크럼을 심어, 실패 이벤트에 경위가 자동으로 첨부되도록 설계했다.

브레드크럼기록 내용
401 감지어떤 API가 401을 받았는지, isTokenRefreshing 상태
refresh 시작 - 이 요청이 트리거갱신을 시작시킨 최초 API URL
갱신 로직 진입accessToken 만료 여부 (atExpired)
refresh 실패서버 응답 코드, 이유, step

이 구조 덕분에 각 이벤트에서 "어떤 API가 먼저 401을 받았고 → refresh를 시도했고 → 서버가 어떻게 응답했는지"를 한 화면에서 볼 수 있었다.

2-3. Sentry Data Scrubbing 우회

Sentry는 필드명에 token, api 등이 포함되면 값을 [Filtered]로 자동 마스킹한다. 정작 필요한 필드가 전부 가려지는 문제가 있었다.

우회 전략 ①: 동일 값을 다른 이름으로 중복 저장

accessTokenExp: getTokenExp(accessToken), // [Filtered] — 안 보임 accessExpiry: getTokenExp(accessToken), // ✅ 보임

우회 전략 ②: 브레드크럼 message가 필터링되면 data.step으로 대체

Sentry.addBreadcrumb({ category: 'token', message: 'refresh 실패: server_error_401_C999', // category가 'token'이라 필터링될 수 있음 data: { step: 'refresh_실패_server_error_401_C999' // data는 필터링 패턴에 안 걸림 ✅ } });

3. FE 토큰 갱신 로직

계측 설계의 기반이 된 실제 axios interceptor 흐름:

401 수신 (C999 제외)
  └─ isTokenRefreshing 이미 true → errorAxiosRetry 폴링 (500ms × 최대 2회)
  └─ isTokenRefreshing false
       └─ memberAuthToken null → 브레드크럼 기록 (갱신 불가)
       └─ memberAuthToken 있음
            └─ refreshToken 만료? → 로그아웃 (서버 요청 없음, JWT decode로 판단)
            └─ refreshToken 유효 → refreshAuthToken() 진입
                  └─ accessToken 만료 또는 만료 10분 전
                       └─ YES → 카시나 refresh API 호출 (access + refresh 신규 발급)
                       └─ NO  → 기존 accessToken 유지
                  └─ 샵바이 accessToken 재발급 (항상)
            └─ isTokenRefreshing = false
            └─ errorAxiosRetry → 원래 요청 재시도 → 성공

핵심: 동시에 401이 N개 와도 isTokenRefreshing 플래그로 refresh는 단 1회만 실행된다. 이 동작이 브레드크럼에서 반복적으로 확인됐다.


4. 수집 데이터 분석

4-1. 10건 상세

이벤트발급 후 경과refreshToken JWT 잔여환경트리거
fc23dc3723시간89일iOS 앱샵바이 M0013
67300a4211일79일Android 앱샵바이 M0013
a275188f22일68일Android 앱샵바이 M0013
d76d548958일32일iOS 앱샵바이 M0013
507d2f3b15일75일iOS 앱샵바이 M0013
b72fd33818시간90일iOS 앱카시나 C010
7ed8f1eb87일3일웹 Chrome샵바이 M0013
ba254a2478일12일웹 Chrome샵바이 M0013
b80f691e52일86일웹 Samsung샵바이 M0013
8f64cba429시간89일iOS 앱샵바이 M0013

C999 발생 시점에 JWT 만료된 건: 0건 / 10건

4-2. 결정적 증거 — 이벤트 d76d5489

2026-01-22  로그인 → refreshToken 발급
2026-02-24  동일 refreshToken → refresh ✅ 성공 (accessToken 갱신됨)
2026-03-21  동일 refreshToken → refresh ❌ C999

Sentry 데이터:

  • accessIssuedAt: 2026-02-24 → Feb 24 갱신 성공 확인
  • refreshIssuedAt: 2026-01-22 → 동일 토큰 그대로

Feb 24에 성공했던 토큰이 Mar 21에 실패 → FE 코드가 원인이라면 Feb 24에도 실패했어야 한다. 서버가 그 사이에 무효화한 것이 데이터로 확정됐다.

4-3. FE 무결성 증거 3가지

#이벤트증명하는 것
1d76d5489Feb 24 성공 이력 → "첫 시도라서 실패" 가설 배제
2b72fd338샵바이 전부 200인데도 카시나 C010 트리거 → FE가 두 토큰 상태를 정확히 구분해 동작
3271ada53 (정상)JWT 만료 refreshToken → 서버 요청 없이 자체 로그아웃 → 동일 코드가 정상 케이스에서 의도대로 작동

5. 가설별 검증

가설검증 방법결과
FE race conditionisTokenRefreshing 브레드크럼 — 모든 이벤트에서 단일 refresh 확인❌ 제외
refreshToken 만료10건 전부 JWT exp 기준 유효, 백엔드에 "만료 아니냐" 반박 시 데이터 제시❌ 제외
장기 미접속 패턴경과 시간 18시간 ~ 87일, 패턴 없음❌ 제외
앱 전용 이슈웹 Chrome 2건, 웹 Samsung 1건 포함 확인❌ 제외
50% 갱신 정책45일 초과 4건 설명 가능, 18시간/29시간 케이스 설명 불가⚠️ 부분만 해당
FE deleteOauthToken 비의도 호출호출 시 localStorage도 삭제 → C999 이벤트 자체가 발생 불가❌ 제외
백엔드 코드 버그백엔드 직접 확인확정

6. 해결

백엔드 코드 버그 확인 및 수정 완료. (2026-03-24)

근본 원인: 서버가 특정 조건에서 유효한 refreshToken을 잘못 무효화하는 로직이 있었음.


7. 회고

이 접근의 핵심

재현이 안 되는 이슈는 코드를 보는 게 아니라 데이터를 만드는 것부터 시작해야 한다.

FE/BE 중 어디가 문제인지 모르는 상황에서 코드만 보고 추측하면 시간만 낭비한다. Sentry에 계측을 먼저 심고, 실제 사용자 환경에서 발생하는 데이터를 수집한 뒤 분석했다.

결과적으로:

  • 1,200건의 이벤트를 통해 FE 로직이 매 케이스마다 정상 플로우를 탔음을 증명
  • 백엔드가 "만료된 토큰이 아니냐"고 반박했을 때, JWT exp 값과 이벤트 4의 갱신 성공 이력으로 즉시 데이터 반박
  • 감정이나 추측 없이 데이터로만 대화해서 빠르게 원인을 좁혔다
JP
이중표Frontend Engineer

3년차 프론트엔드 개발자. Next.js, React, TypeScript 기반 웹 애플리케이션 개발 전문. 대규모 트래픽 환경에서 SSR·ISR 렌더링 전략 설계 경험.

이력서 보기