[feature] 서버 워커 장애 대응 프론트 방어 - #1827
Conversation
- fetchWithTimeout 래퍼 추가(AbortController 타임아웃) 및 fetch/secureFetch 호출부 연결 - 타임아웃/네트워크 실패를 NetworkError로 변환 - QueryClient 지수 백오프 재시도(4xx 제외) + QueryCache.onError로 Sentry 전파 - 메인 공개 쿼리 localStorage 영속화(PersistQueryClientProvider, maxAge 1h, buster, 화이트리스트)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @seongwon030's task in 3m 35s —— View job 🔍 harry 리뷰 (opened)서버 워커 무응답 장애에 대한 프론트 방어(타임아웃·재시도·Sentry 전파·캐시 영속화)를 중앙 경로 위주로 깔끔하게 잡았어요. 다만 이 레포의 중앙 상수 관리 컨벤션과 테스트 누락 관점에서 몇 가지만 짚었어요. 상세는 인라인 코멘트로 남겼어요.
짚은 점
기능 로직 자체는 방어 목적에 잘 맞고, 위 항목은 대부분 컨벤션 정리 수준이라 반영은 어렵지 않을 거예요. |
WalkthroughAPI 요청에 Changes네트워크 및 쿼리 안정성
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ UI 변경사항 없음
전체 88개 스토리 · 32개 컴포넌트 |
There was a problem hiding this comment.
Code Review
This pull request introduces query persistence using TanStack Query's persist client to cache specific public data in localStorage, and adds a custom fetchWithTimeout wrapper to handle request timeouts and network errors consistently. Additionally, it configures Sentry error reporting on the query cache and implements exponential backoff for retries. Feedback is provided regarding the fetchWithTimeout utility, which currently overwrites and ignores caller-provided abort signals, and the Sentry error handler, which may report duplicate exceptions on every query retry attempt.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
frontend/src/apis/auth/refreshAccessToken.ts (1)
2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
frontend/src내부 import를@/*alias로 통일해 주세요.네 파일 모두 새 유틸리티 import에 상대 경로를 사용하고 있습니다. 프로젝트 규칙에 맞게 alias 경로로 변경해 주세요.
frontend/src/apis/auth/refreshAccessToken.ts#L2-L2:@/apis/utils/fetchWithTimeout사용frontend/src/apis/calendarOAuth.ts#L16-L16:@/apis/utils/fetchWithTimeout사용frontend/src/apis/club.ts#L5-L5:@/apis/utils/fetchWithTimeout사용frontend/src/apis/promotion.ts#L10-L10:@/apis/utils/fetchWithTimeout사용As per coding guidelines,
frontend/src내부 import에는@/*path alias를 사용해야 합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/apis/auth/refreshAccessToken.ts` at line 2, Replace the relative fetchWithTimeout imports with the `@/`* alias in all affected sites: frontend/src/apis/auth/refreshAccessToken.ts:2, frontend/src/apis/calendarOAuth.ts:16, frontend/src/apis/club.ts:5, and frontend/src/apis/promotion.ts:10. Use the shared `@/apis/utils/fetchWithTimeout` path consistently.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/package.json`:
- Around line 35-37: Replace the async persister dependency in the frontend
package configuration with `@tanstack/query-sync-storage-persister`, and update
the corresponding persister imports/usages to use the sync package for
localStorage. Remove the async-storage package dependency while preserving the
existing React Query persistence setup.
In `@frontend/src/apis/utils/fetchWithTimeout.ts`:
- Around line 17-36: Update the fetchWithTimeout flow to preserve and combine
init.signal with the internal timeout signal instead of overwriting the caller’s
signal. Ensure external aborts propagate as external cancellation rather than
being converted to the timeout NetworkError, while timeout-triggered AbortErrors
retain the existing timeout message; also clean up any abort listener in the
existing finally block.
In `@frontend/src/App.tsx`:
- Around line 48-51: Update the persister initialization to use
createSyncStoragePersister instead of createAsyncStoragePersister, while
preserving the existing window.localStorage storage and MOADONG_QUERY_CACHE key
configuration.
- Line 3: In App.tsx, replace the async persister import
createAsyncStoragePersister with the synchronous createSyncStoragePersister
package export to match the localStorage-backed persistence flow, and update its
usage consistently.
---
Nitpick comments:
In `@frontend/src/apis/auth/refreshAccessToken.ts`:
- Line 2: Replace the relative fetchWithTimeout imports with the `@/`* alias in
all affected sites: frontend/src/apis/auth/refreshAccessToken.ts:2,
frontend/src/apis/calendarOAuth.ts:16, frontend/src/apis/club.ts:5, and
frontend/src/apis/promotion.ts:10. Use the shared `@/apis/utils/fetchWithTimeout`
path consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 14992a1c-67d5-4b43-a602-2d6a147eb6cf
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (13)
frontend/package.jsonfrontend/src/App.tsxfrontend/src/apis/application.tsfrontend/src/apis/auth.tsfrontend/src/apis/auth/refreshAccessToken.tsfrontend/src/apis/auth/secureFetch.tsfrontend/src/apis/banner.tsfrontend/src/apis/calendarOAuth.tsfrontend/src/apis/club.tsfrontend/src/apis/game.tsfrontend/src/apis/promotion.test.tsfrontend/src/apis/promotion.tsfrontend/src/apis/utils/fetchWithTimeout.ts
- fetchWithTimeout: 호출부 signal 병합, 외부 취소는 원본 AbortError 전파 - fetchWithTimeout 테스트 6케이스 추가 - 영속화 화이트리스트를 queryKeys에서 파생, localStorage 키 STORAGE_KEYS로 중앙화 - refreshAccessToken import를 @/ alias로 통일, 폴더 CLAUDE.md 갱신
- Vercel이 빌드마다 자동 주입하는 VERCEL_GIT_COMMIT_SHA를 __BUILD_ID__로 define - buster가 배포마다 자동 변경되어 크로스-배포 캐시 무효화 보장 - 비Vercel 환경은 기존 VITE_SENTRY_RELEASE 폴백 유지
suhyun113
left a comment
There was a problem hiding this comment.
서버에 장애가 있는 줄 몰랐는데 프론트 측에서도 서버 문제에 대비하여 캐시로 대응한거 좋습니다. 수고하셨어요!
| } else { | ||
| externalSignal.addEventListener('abort', onExternalAbort); | ||
| } | ||
| } |
There was a problem hiding this comment.
찾아보니 AbortSignal.any([controller.signal, externalSignal])를 사용하여 수동 관리 없이 코드를 더 간결하게 쓸 수도 있는 것 같네요.
다만 Chrome 116+/Safari 17.4+ 제한이 있네요. 타겟 브라우저 지원 범위 밖이었나요? 아니라면 써봐도 좋을 것 같네요.
There was a problem hiding this comment.
AbortSignal.any는 Safari 17.4+라 Safari 16~17.3에서 모든 API 호출이 런타임 TypeError로 터질 것 같네요. 유저 타겟 버전을 올리면 다시 고려해 보시져
배경
서버 워커가 죽어 응답 자체가 오지 않는(무응답/커넥션 거부/타임아웃) 장애 상황에 대한 프론트 방어가 없었습니다. 5xx 응답은 에러 클래스/바운더리로 일부 처리되나, 요청 타임아웃 부재로 무한 로딩 위험이 있었고,
NetworkError가 실제 fetch 계층에 미연결, 쿼리 에러가throwOnError: false라 Sentry로도 전파되지 않았습니다.변경 사항
1. 요청 타임아웃 + NetworkError 연결
fetchWithTimeout래퍼 신규(AbortController기반 기본 10s 타임아웃) + 전용 테스트 6케이스NetworkError('요청 시간 초과'), 네트워크 실패(TypeError) →NetworkError()변환init.signal병합 지원 — 외부 취소(언마운트 등)는 원본AbortError그대로 전파 (리뷰 반영)fetch/secureFetch호출부 전부 연결 (SSE 스트림·S3 presigned 업로드·테스트는 의도적 제외)2. 쿼리 재시도 + Sentry 전파
QueryCache.onError로 쿼리 에러를 Sentry 전파(4xx 노이즈 제외) — 훅을 건드리지 않는 중앙 경로. v5에서onError는 재시도 소진 후 1회만 호출됨(중복 전송 없음, query-core 소스 확인)3. React Query localStorage 영속화
PersistQueryClientProvider+createAsyncStoragePersister(localStorage) — sync persister는 v5.101+에서 deprecated라 async 사용maxAge1h(장애 시 stale 상한), 저장 키는STORAGE_KEYS.QUERY_CACHE로 중앙 관리 (리뷰 반영)shouldDehydrateQuery화이트리스트를queryKeys에서 파생 (리뷰 반영): 성공한 공개 쿼리(clubs/promotions/banner/game)만 저장, 개인·인증·어드민 데이터/검색 자동완성/실패 응답 제외4. buster = Vercel 커밋 SHA (
__BUILD_ID__)VERCEL_GIT_COMMIT_SHA를 vitedefine으로 주입해 buster로 사용VITE_SENTRY_RELEASE폴백리뷰 대응 (gemini / harry / coderabbit 총 7건)
createSyncStoragePersister는 5.101.2에서@deprecated명시 (async가 공식 대체)QueryCache.onError는 retryer가 재시도를 소진한 최종 실패 시 1회만 호출 (query-core/src/query.tscatch 블록)관련 인프라 수정 (PR 외, 완료)
Vercel 환경변수가 webpack 시절 이름(
SENTRY_DSN/SENTRY_RELEASE)으로 남아 있어 2025-11 Vite 전환 이후 프로덕션 Sentry가 미초기화 + 소스맵 업로드 skip 상태였음 (빌드 로그[sentry-vite-plugin] Missing ...경고로 확인).VITE_접두사로 이름 변경 + 재배포 완료 — 프로덕션 번들에 DSN 인라인 확인됨. 이 PR의 Sentry 전파가 동작하기 위한 전제조건.검증
tsc·eslint통과, 테스트 265개 전부 통과 (기존 259 + fetchWithTimeout 신규 6)VERCEL_GIT_COMMIT_SHA=testsha123으로 프로덕션 빌드 후 번들에 buster 값 주입 확인