블로그 · 2026년 4월 6일 · 읽기 6분

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
d76d5489 ⭐58일32일iOS 앱샵바이 M0013
507d2f3b15일75일iOS 앱샵바이 M0013
b72fd338 ⭐18시간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의 갱신 성공 이력으로 즉시 데이터 반박
  • 감정이나 추측 없이 데이터로만 대화해서 빠르게 원인을 좁혔다