Skip to content

[feature] 서버 워커 장애 대응 프론트 방어 - #1827

Merged
seongwon030 merged 4 commits into
develop-fefrom
frontend-worker-failure-handling
Jul 19, 2026
Merged

[feature] 서버 워커 장애 대응 프론트 방어 #1827
seongwon030 merged 4 commits into
develop-fefrom
frontend-worker-failure-handling

Conversation

@seongwon030

@seongwon030 seongwon030 commented Jul 14, 2026

Copy link
Copy Markdown
Member

배경

서버 워커가 죽어 응답 자체가 오지 않는(무응답/커넥션 거부/타임아웃) 장애 상황에 대한 프론트 방어가 없었습니다. 5xx 응답은 에러 클래스/바운더리로 일부 처리되나, 요청 타임아웃 부재로 무한 로딩 위험이 있었고, NetworkError가 실제 fetch 계층에 미연결, 쿼리 에러가 throwOnError: falseSentry로도 전파되지 않았습니다.

변경 사항

1. 요청 타임아웃 + NetworkError 연결

  • fetchWithTimeout 래퍼 신규(AbortController 기반 기본 10s 타임아웃) + 전용 테스트 6케이스
  • abort → NetworkError('요청 시간 초과'), 네트워크 실패(TypeError) → NetworkError() 변환
  • 호출부 init.signal 병합 지원 — 외부 취소(언마운트 등)는 원본 AbortError 그대로 전파 (리뷰 반영)
  • raw fetch / secureFetch 호출부 전부 연결 (SSE 스트림·S3 presigned 업로드·테스트는 의도적 제외)

2. 쿼리 재시도 + Sentry 전파

  • 지수 백오프 재시도(1s→2s, 최대 10s), 4xx는 재시도 제외
  • QueryCache.onError로 쿼리 에러를 Sentry 전파(4xx 노이즈 제외) — 훅을 건드리지 않는 중앙 경로. v5에서 onError는 재시도 소진 후 1회만 호출됨(중복 전송 없음, query-core 소스 확인)

3. React Query localStorage 영속화

  • PersistQueryClientProvider + createAsyncStoragePersister(localStorage) — sync persister는 v5.101+에서 deprecated라 async 사용
  • maxAge 1h(장애 시 stale 상한), 저장 키는 STORAGE_KEYS.QUERY_CACHE로 중앙 관리 (리뷰 반영)
  • shouldDehydrateQuery 화이트리스트를 queryKeys에서 파생 (리뷰 반영): 성공한 공개 쿼리(clubs/promotions/banner/game)만 저장, 개인·인증·어드민 데이터/검색 자동완성/실패 응답 제외
  • 목적: 백엔드 장애로 refetch 불가일 때 메인페이지가 마지막 데이터를 표시

4. buster = Vercel 커밋 SHA (__BUILD_ID__)

  • Vercel이 빌드마다 자동 주입하는 VERCEL_GIT_COMMIT_SHA를 vite define으로 주입해 buster로 사용
  • 배포마다 값이 자동으로 바뀌어 크로스-배포 캐시 무효화 보장 (스키마 변경 시 옛 캐시 hydrate 크래시 방지). 비Vercel 환경은 VITE_SENTRY_RELEASE 폴백

리뷰 대응 (gemini / harry / coderabbit 총 7건)

  • 수용 5건: signal 병합, 화이트리스트 queryKeys 파생, localStorage 키 상수화, fetchWithTimeout 테스트 추가, import alias 통일
  • 반박 2건 (소스 근거):
    • coderabbit "sync persister 써라" → createSyncStoragePersister는 5.101.2에서 @deprecated 명시 (async가 공식 대체)
    • gemini "재시도마다 Sentry 3중 전송" → QueryCache.onError는 retryer가 재시도를 소진한 최종 실패 시 1회만 호출 (query-core/src/query.ts catch 블록)

관련 인프라 수정 (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 값 주입 확인

- fetchWithTimeout 래퍼 추가(AbortController 타임아웃) 및 fetch/secureFetch 호출부 연결
- 타임아웃/네트워크 실패를 NetworkError로 변환
- QueryClient 지수 백오프 재시도(4xx 제외) + QueryCache.onError로 Sentry 전파
- 메인 공개 쿼리 localStorage 영속화(PersistQueryClientProvider, maxAge 1h, buster, 화이트리스트)
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Jul 15, 2026 2:56am

@github-actions github-actions Bot added 💻 FE Frontend 🚁AI claude, codex 활용 시 labels Jul 14, 2026
@reviewer-harry

reviewer-harry Bot commented Jul 14, 2026

Copy link
Copy Markdown

Claude finished @seongwon030's task in 3m 35s —— View job


🔍 harry 리뷰 (opened)

서버 워커 무응답 장애에 대한 프론트 방어(타임아웃·재시도·Sentry 전파·캐시 영속화)를 중앙 경로 위주로 깔끔하게 잡았어요. QueryCache.onError로 훅을 안 건드리고 Sentry를 태운 점, 4xx를 재시도·리포팅에서 제외해 노이즈를 줄인 점, 영속화 화이트리스트로 개인/인증/어드민 데이터를 배제한 방향성 모두 좋아요. 👍

다만 이 레포의 중앙 상수 관리 컨벤션테스트 누락 관점에서 몇 가지만 짚었어요. 상세는 인라인 코멘트로 남겼어요.

  • 리뷰 기준(.github/harry-review-rules.md) 확인
  • PR 전체 diff 분석
  • 변경 파일 상세 리뷰
  • 인라인 코멘트 + 총평 작성

짚은 점

  1. App.tsx 쿼리 루트 하드코딩PERSISTED_QUERY_ROOTS'suggestions' 리터럴이 constants/queryKeys.ts의 중앙 관리와 이중화돼 있어, 키가 바뀌면 화이트리스트가 조용히 어긋나요. queryKeys에서 파생하는 게 좋아요.
  2. App.tsx localStorage 키 하드코딩'MOADONG_QUERY_CACHE'constants/storageKeys.ts(STORAGE_KEYS)로 옮겨 다른 키와 일관성을 맞춰 주세요.
  3. fetchWithTimeout.ts 테스트 누락 — 이번 PR 핵심 로직(타임아웃/TypeErrorNetworkError 변환)에 대응 테스트가 없어요. 전용 테스트를 추가해 주세요.
  4. (경미) fetchWithTimeout signal 덮어쓰기 — 호출부가 init.signal을 넘겨도 무시돼요. 지금은 문제없지만 React Query signal 전달 시 취소가 무시될 수 있어 주석/합성 처리를 권해요.

기능 로직 자체는 방어 목적에 잘 맞고, 위 항목은 대부분 컨벤션 정리 수준이라 반영은 어렵지 않을 거예요.
· frontend-worker-failure-handling

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

API 요청에 fetchWithTimeout을 적용하고 네트워크 오류를 표준화했습니다. React Query에는 Sentry 오류 처리, 재시도 정책, localStorage 캐시 영속화와 배포 버전 무효화가 추가되었습니다.

Changes

네트워크 및 쿼리 안정성

Layer / File(s) Summary
타임아웃 fetch 유틸리티
frontend/src/apis/utils/fetchWithTimeout.ts, frontend/src/apis/utils/fetchWithTimeout.test.ts, frontend/src/apis/CLAUDE.md
AbortController 기반 타임아웃과 오류 변환을 추가하고 외부 취소, 타이머 정리 동작을 테스트와 문서에 반영했습니다.
API 요청 타임아웃 적용
frontend/src/apis/application.ts, frontend/src/apis/auth*, frontend/src/apis/banner.ts, frontend/src/apis/calendarOAuth.ts, frontend/src/apis/club.ts, frontend/src/apis/game.ts, frontend/src/apis/promotion*
지원서, 인증, 토큰 갱신, 보안 요청, 배너, 캘린더, 클럽, 게임, 프로모션 요청을 fetchWithTimeout 경로로 변경하고 프로모션 테스트에서 AbortSignal을 검증합니다.
React Query 오류 처리 및 캐시 영속화
frontend/package.json, frontend/src/App.tsx, frontend/src/constants/storageKeys.ts, frontend/src/constants/CLAUDE.md
Sentry 오류 전송, 4xx 재시도 제외 및 지수 백오프, 허용된 성공 쿼리의 localStorage 영속화와 PersistQueryClientProvider 구성을 추가합니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • Moadong/moadong#1371: Google OAuth 엔드포인트가 secureFetch를 사용하며, 이번 변경으로 해당 요청에 타임아웃 처리가 적용됩니다.

Suggested reviewers: suhyun113

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 서버 워커 장애에 대비한 프론트 방어 로직 추가라는 PR의 핵심 변경을 잘 반영합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch frontend-worker-failure-handling

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seongwon030
seongwon030 requested a review from suhyun113 July 14, 2026 14:28
@seongwon030 seongwon030 added ✨ Feature 기능 개발 and removed 🚁AI claude, codex 활용 시 labels Jul 14, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

✅ UI 변경사항 없음

구분 링크
📖 Storybook https://67904e61c16daa99a63b44a7-okgchjxtvp.chromatic.com/

전체 88개 스토리 · 32개 컴포넌트

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread frontend/src/apis/utils/fetchWithTimeout.ts
Comment thread frontend/src/App.tsx
Comment thread frontend/src/App.tsx Outdated
Comment thread frontend/src/App.tsx Outdated
Comment thread frontend/src/apis/utils/fetchWithTimeout.ts
Comment thread frontend/src/apis/utils/fetchWithTimeout.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 51cac31 and 74e7290.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • frontend/package.json
  • frontend/src/App.tsx
  • frontend/src/apis/application.ts
  • frontend/src/apis/auth.ts
  • frontend/src/apis/auth/refreshAccessToken.ts
  • frontend/src/apis/auth/secureFetch.ts
  • frontend/src/apis/banner.ts
  • frontend/src/apis/calendarOAuth.ts
  • frontend/src/apis/club.ts
  • frontend/src/apis/game.ts
  • frontend/src/apis/promotion.test.ts
  • frontend/src/apis/promotion.ts
  • frontend/src/apis/utils/fetchWithTimeout.ts

Comment thread frontend/package.json
Comment thread frontend/src/apis/utils/fetchWithTimeout.ts
Comment thread frontend/src/App.tsx
Comment thread frontend/src/App.tsx
- fetchWithTimeout: 호출부 signal 병합, 외부 취소는 원본 AbortError 전파
- fetchWithTimeout 테스트 6케이스 추가
- 영속화 화이트리스트를 queryKeys에서 파생, localStorage 키 STORAGE_KEYS로 중앙화
- refreshAccessToken import를 @/ alias로 통일, 폴더 CLAUDE.md 갱신
@seongwon030 seongwon030 changed the title [feature] 서버 워커 장애 대응 프론트 방어 (타임아웃·재시도·Sentry 전파·RQ 영속화) [feature] 서버 워커 장애 대응 프론트 방어 Jul 15, 2026
- Vercel이 빌드마다 자동 주입하는 VERCEL_GIT_COMMIT_SHA를 __BUILD_ID__로 define
- buster가 배포마다 자동 변경되어 크로스-배포 캐시 무효화 보장
- 비Vercel 환경은 기존 VITE_SENTRY_RELEASE 폴백 유지

@suhyun113 suhyun113 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서버에 장애가 있는 줄 몰랐는데 프론트 측에서도 서버 문제에 대비하여 캐시로 대응한거 좋습니다. 수고하셨어요!

Comment thread frontend/src/apis/utils/fetchWithTimeout.ts
Comment thread frontend/src/apis/utils/fetchWithTimeout.test.ts
} else {
externalSignal.addEventListener('abort', onExternalAbort);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

찾아보니 AbortSignal.any([controller.signal, externalSignal])를 사용하여 수동 관리 없이 코드를 더 간결하게 쓸 수도 있는 것 같네요.
다만 Chrome 116+/Safari 17.4+ 제한이 있네요. 타겟 브라우저 지원 범위 밖이었나요? 아니라면 써봐도 좋을 것 같네요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbortSignal.any는 Safari 17.4+라 Safari 16~17.3에서 모든 API 호출이 런타임 TypeError로 터질 것 같네요. 유저 타겟 버전을 올리면 다시 고려해 보시져

@seongwon030
seongwon030 merged commit aec46f6 into develop-fe Jul 19, 2026
23 checks passed
@seongwon030
seongwon030 deleted the frontend-worker-failure-handling branch July 19, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚁AI claude, codex 활용 시 💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants