Skip to content

API 호출 최적화 - React Query 도입과 서버 캐시 정비 - #92

Merged
pcwadarong merged 10 commits into
mainfrom
refactor/api-optimization
Jul 8, 2026
Merged

pcwadarong merged 10 commits into
mainfrom
refactor/api-optimization

Conversation

@pcwadarong

@pcwadarong pcwadarong commented Jul 8, 2026 •

Copy link
Copy Markdown
Owner

작업 내용

1. React Query 도입 (클라이언트 fetch 전면 이관)

  • SSR-safe QueryClient(서버 요청별 신규 / 브라우저 싱글턴) + 도메인별 쿼리 키 팩토리 구성
  • 클라이언트 fetch 10곳을 useQuery/useInfiniteQuery/fetchQuery로 이관 — 수동 AbortController·useState 로딩 상태 제거, 훅 반환 계약은 유지해 소비 컴포넌트 무변경
  • 무한스크롤 3곳(글/프로젝트 피드, 아카이브)은 서버 시드를 initialData로 재사용해 첫 페이지 중복 fetch 0회
  • 공용 커서 페이지네이션 훅(use-cursor-pagination-feed) 삭제
  • 서버 prefetch/HydrationBoundary는 의도적으로 미사용 — 첫 페인트는 기존 Server Component + 'use cache'가 담당, RQ는 상호작용 이후 데이터만

2. 댓글: 자체 Map 캐시(60s TTL) → React Query

  • 정렬/페이지 전환 시 keepPreviousData로 스켈레톤 플래시 제거
  • 수정/삭제 useMutation 전환, 변이 후 fresh 재조회 → setQueryData → scope 무효화(refetchType: none)로 결정적 갱신 (낙관적 업데이트는 서버 결정 로직(id·페이지 clamp)과의 불일치 위험으로 배제)

3. 서버 쿼리 왕복 축소

  • 게스트북/댓글 답글 N+1(1+N회) → .in() 단일 쿼리(2회)
  • 아카이브 윈도우의 독립 쿼리 2개 Promise.all 병렬화
  • 인기 태그 조합(태그 조회→라벨 매핑)을 'use cache' 함수로 승격 — warm 캐시에서 DB 0쿼리

4. 캐시 수명 정비

  • 전체 'use cache' 조회 13곳에 cacheLife 부여 — 예약 발행(publish_at) 글이 태그 무효화 없이는 영영 안 나타나던 문제 해소 (콘텐츠는 시간 단위, UGC는 분 단위)
  • generateMetadata+본문 중복 호출 3곳 React.cache() dedupe
  • 어드민 GSC: GoogleAuth 클라이언트 모듈 싱글턴화 + 15분 unstable_cache (날짜는 캐시 밖에서 계산해 인자로 전달, 에러 미캐시)

테스트

  • vitest 전 버킷 green (신규: 쿼리 키 안정성, use-browse-articles, 댓글 인터랙션 계약 추가)
  • main부터 깨져 있던 get-article/get-project 테스트 10건도 mock 체인 수리
  • UTC CI에서 실패하던 타임존 의존 테스트 3건 수정 — KST 하드코딩 기대값을 로컬 Date 생성자 기반으로 변경 (GSC 날짜 범위, 예약 발행 ISO 변환, 저장 시각 포맷). TZ=UTC/Asia/Seoul 양쪽 검증 완료

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • 댓글, 글 목록, 프로젝트 목록, 홈 프로젝트 미리보기, PDF 다운로드, 링크 미리보기 등이 더 안정적인 데이터 로딩 방식으로 개선되었습니다.
    • 인기 태그와 댓글 목록에 로컬라이즈된 결과가 반영되어, 언어에 맞는 표시가 더 자연스러워졌습니다.
  • Bug Fixes

    • 댓글 스레드와 아카이브 목록의 추가 로드 동작이 개선되어 중복 요청과 끊김이 줄었습니다.
    • 캐시 만료/갱신 기준이 정리되어 최신 콘텐츠가 더 일관되게 노출됩니다.

pcwadarong and others added 5 commits July 8, 2026 14:37
- SSR-safe QueryClient 팩토리(서버 요청별 신규, 브라우저 싱글턴)와 공통 기본값(staleTime 60s) 정의
- 루트 레이아웃에 QueryProvider 마운트, 도메인별 쿼리 키 팩토리와 테스트 헬퍼 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 무한스크롤 3곳(글/프로젝트 피드, 아카이브)을 useInfiniteQuery로 전환하고 서버 시드를 initialData로 재사용
- 지연 fetch(히어로 프리뷰·인기 태그·PDF 옵션·어드민 가용성)를 useQuery로 통일, 수동 AbortController/로딩 state 제거
- slug 중복 체크와 OG 링크 프리뷰는 fetchQuery로 dedupe(30s/24h), 공용 커서 페이지네이션 훅 삭제

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 60초 TTL 모듈 캐시 삭제, useQuery(staleTime 60s)+keepPreviousData로 정렬/페이지 전환 깜빡임 제거
- 수정/삭제를 useMutation으로 전환, 변이 후 fresh 재조회→setQueryData→scope 무효화(refetchType none)로 결정적 갱신

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 게스트북/댓글 답글 N+1을 .in() 단일 쿼리로, 아카이브 윈도우 독립 쿼리를 Promise.all로 병렬화
- 인기 태그 조합을 use cache 함수로 승격, 전체 use cache 조회에 cacheLife 부여(예약 발행 글 미노출 해소)
- generateMetadata/본문 중복 호출 React.cache dedupe, GSC 클라이언트 싱글턴화+15분 unstable_cache

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- React Query 캐시 모델(staleTime/gcTime/initialData), Next.js 4계층 캐시, DB 왕복 최소화 원리 정리
- gitignore에 학습 노트 2건 예외 등록

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
chaen Ready Ready Preview, Comment Jul 8, 2026 7:42am

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pcwadarong, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e4ce472a-a913-4202-91f4-bab55a9ed2bf

📥 Commits

Reviewing files that changed from the base of the PR and between 35bf281 and ffec09c.

📒 Files selected for processing (15)
  • src/app/[locale]/(home)/page.test.tsx
  • src/app/[locale]/project/page.test.tsx
  • src/app/[locale]/resume/page.test.tsx
  • src/app/api/(content)/article-popular-tags/route.test.ts
  • src/entities/pdf-file/api/get-pdf-file-availability.ts
  • src/entities/pdf-file/api/get-pdf-file-content.ts
  • src/features/article-comment/model/use-article-comments-page.ts
  • src/features/browse-articles/model/use-browse-articles.test.ts
  • src/features/edit-markdown-adapter/model/fetch-link-preview-meta.ts
  • src/shared/lib/query/query-keys.test.ts
  • src/shared/lib/query/query-keys.ts
  • src/widgets/article-comments/ui/article-comments-section.tsx
  • src/widgets/detail-page/archive/feed.tsx
  • src/widgets/editor/ui/core/editor-core.test.tsx
  • src/widgets/editor/ui/publish/publish-panel.test.tsx

Walkthrough

React Query(@tanstack/react-query)를 전역 도입해 QueryClient/Provider 및 공통 쿼리 키를 추가하고, 댓글·피드·태그 필터·PDF·에디터 등 다수 클라이언트 조회를 useQuery/useInfiniteQuery/useMutation으로 전환했습니다. 서버 측에서는 다수 캐시에 cacheLife 정책을 추가하고, 댓글/방명록 답글 조회를 N+1에서 IN 쿼리로 개선했으며, 인기 태그 로컬라이제이션 API와 페이지 데이터의 React cache() 래핑을 추가했습니다.

Changes

React Query 클라이언트 도입 및 UI 전환

Layer / File(s) Summary
QueryClient/Provider/쿼리 키 인프라
package.json, src/app/layout.tsx, src/shared/providers/*, src/shared/lib/query/*, src/shared/lib/test/render-with-query-client.tsx
@tanstack/react-query 의존성을 추가하고, getQueryClient/QueryProvider, 공통 쿼리 키 팩토리(articles, articleComments, projects, tags, pdf, detailArchive, og, editor), 테스트용 QueryClient 래퍼를 도입해 루트 레이아웃에 적용합니다.
아티클 댓글 섹션 React Query 전환
src/features/article-comment/model/use-article-comments-page.ts, src/widgets/article-comments/ui/*
useArticleCommentsPage 훅을 추가하고 댓글 페이지 로딩/수정/삭제를 useQuery/useMutation + queryClient 캐시 동기화로 재구성했습니다.
무한 스크롤 피드 전환
src/features/browse-articles/..., src/features/browse-projects/..., src/widgets/detail-page/archive/feed.tsx, 관련 테스트
아티클/프로젝트/디테일 아카이브 피드의 커서 페이지네이션을 useInfiniteQuery 기반으로 재구현했습니다.
인기 태그 로컬라이제이션 및 태그 필터 전환
src/app/api/(content)/article-popular-tags/route.ts, src/entities/article/api/list/get-popular-article-tags.ts, src/features/article-tag-filter/ui/*
getLocalizedPopularArticleTags를 추가해 라우트가 단일 API에 위임하도록 바꾸고, 태그 필터 목록 조회를 useQuery로 전환했습니다.
기타 UI 컴포넌트 React Query 전환
src/shared/ui/markdown/link-embed-card.tsx, src/shared/ui/pdf-download-popover/..., src/widgets/admin-pdf-upload/..., src/widgets/home-hero-scene/..., src/widgets/editor/ui/publish/publish-panel.tsx, 관련 테스트
링크 프리뷰, PDF 다운로드/업로드, 홈 히어로 프로젝트 프리뷰, 에디터 슬러그 중복검사를 각각 캐시 래퍼/useQuery/fetchQuery 기반으로 전환했습니다.

Estimated code review effort: 4 (Complex) | ~75 minutes

서버 캐시 정책 및 조회 최적화

Layer / File(s) Summary
캐시 수명(cacheLife) 정책 추가
src/entities/article/api/..., src/entities/project/api/..., src/entities/pdf-file/api/..., src/entities/tag/api/query-tags.ts
아티클/프로젝트/PDF/태그/방명록 등 다수 캐시 함수에 unstable_cacheLife로 만료/재검증/스테일 값을 지정했습니다.
댓글/방명록 답글 N+1 → IN 쿼리 전환
src/entities/article/comment/api/get-article-comments.ts, src/entities/guestbook/api/get-guestbook-threads.ts, 관련 테스트
fetchRepliesByParentIds를 parent_id별 개별 쿼리에서 단일 IN 쿼리로 바꾸고 그룹화 로직을 추가했습니다.
Google 검색 트래픽 캐싱
src/entities/article/api/list/get-admin-google-article-traffic.ts
Search Console 클라이언트를 재사용하고 unstable_cache로 15분 TTL 캐싱을 적용했습니다.
아티클/프로젝트 조회 병렬화 및 테스트 재구성
src/entities/article/api/detail/get-article-detail-list.ts, get-article.test.ts, get-project.test.ts
getArticleDetailListWindow 조회를 병렬화하고 테스트의 Supabase 모킹을 테이블 라우팅 방식으로 전환했습니다.
페이지 데이터 React cache() 래핑
src/views/articles/model/..., src/views/project/model/get-project-detail-page-data.ts
상세 shell/아티클 목록 조회를 cache()로 감싸 요청 범위 dedupe를 적용했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ArticleCommentsSection
  participant Mutation
  participant Server
  participant QueryClient

  User->>ArticleCommentsSection: 댓글 수정/삭제 확인
  ArticleCommentsSection->>Mutation: mutateAsync 호출
  Mutation->>Server: updateArticleCommentAction/deleteArticleCommentAction
  Server-->>Mutation: 결과 반환
  Mutation->>ArticleCommentsSection: refreshCommentsPage 호출
  ArticleCommentsSection->>QueryClient: setQueryData / invalidateQueries(refetchType: none)
Loading
sequenceDiagram
  participant User
  participant Feed as useBrowseArticles/DetailArchiveFeed
  participant ReactQuery as useInfiniteQuery
  participant API

  User->>Feed: loadMore() 호출
  Feed->>ReactQuery: fetchNextPage()
  ReactQuery->>API: cursor/limit/locale 요청
  API-->>ReactQuery: 다음 페이지 데이터
  ReactQuery-->>Feed: pages 병합 후 items 갱신
Loading

Possibly related PRs

  • pcwadarong/chaen#23: 댓글 섹션의 API/UI 흐름과 동일 코드 영역(article-comments-section.tsx)을 다룹니다.
  • pcwadarong/chaen#25: getPopularArticleTags/태그 라벨 매핑 흐름을 기반으로 getLocalizedPopularArticleTags를 확장합니다.
  • pcwadarong/chaen#79: 커서 페이지네이션 훅을 제거하고 useInfiniteQuery로 전환한 동일 방향의 리팩터링입니다.

Suggested labels: data fetch

Tip

📚 논문 대신 리뷰 노트: cacheLife 옵션(expire, revalidate, stale)의 정확한 의미가 헷갈리면 Next.js 공식 문서의 unstable_cacheLife 섹션을 참고하시고, React Query의 staleTime/gcTime/placeholderData: keepPreviousData 옵션은 TanStack Query 공식 가이드를 함께 확인하며 리뷰하시면 좋을 것 같아요! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning 작업 내용은 풍부하지만 템플릿의 주요 섹션 제목과 필수 내용(문제/해결 과정, 참고 문서)이 빠져 있습니다. 템플릿의 각 섹션 제목을 그대로 사용해 작업 내용 1~3, 문제/해결 과정, 참고 문서 링크를 보강하세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 React Query 도입과 서버 캐시 정비라는 주요 변경점을 잘 요약해 PR 내용과 일치합니다.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/api-optimization

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9fd7fe6c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/widgets/detail-page/archive/feed.tsx
pcwadarong and others added 2 commits July 8, 2026 15:10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- KST 하드코딩 기대값 탓에 UTC CI에서 실패하던 3건(GSC 날짜 범위, 예약 발행 ISO 변환, 저장 시각 포맷)을
  실행 타임존과 무관하게 검증하도록 로컬 Date 생성자 기반으로 변경

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 104135c에서 구현이 의도적으로 변경(canonical=실제 해석 locale, x-default=앱 기본 locale ko)됐으나
  테스트가 옛 en-폴백 계약을 기대해 실패하던 것을 현행 동작 기준으로 수정
- 해석 locale이 유효하지 않을 때 요청 locale로 돌아가는 분기 케이스 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dom-heavy 버킷 조기 종료에 가려져 있던 타임존 의존 2건(autosave 저장 라벨, 예약 발행 UTC 표시)을 로컬 컴포넌트 기준으로 수정
- 104135c 이후 계약과 어긋난 x-default 기대값 3건(home/resume/project)을 ko 기준으로, project 아카이브는 canonical 단독 노출로 갱신

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/app/api/(content)/article-popular-tags/route.test.ts (1)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

테스트 설명을 가이드라인 형식에 맞게 작성 권장

코딩 가이드라인에서는 테스트 설명을 "Under [Condition/Context], [Subject] must [Expected Behavior/State Change]" 형식으로 작성하도록 권장합니다. 현재 설명은 계약을 전달하고 있으나, 주체(Subject)가 명시되지 않고 "must/해야 한다" 형식이 누락되어 있습니다.

예시:

  • 'locale이 주어지면, GET 핸들러는 label이 결합된 인기 태그 목록을 반환해야 한다'
  • '인기 태그가 비어 있으면, GET 핸들러는 빈 배열을 반환해야 한다'

As per coding guidelines: "Test descriptions must describe a Contract between State and Result using the format: Under [Condition/Context], [Subject] must [Expected Behavior/State Change]"

Also applies to: 37-37

🤖 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 `@src/app/api/`(content)/article-popular-tags/route.test.ts at line 15, The
test description in the `it(...)` blocks for the article popular tags route
should be rewritten to match the guideline format: “Under [Condition/Context],
[Subject] must [Expected Behavior/State Change]”. Update the affected test names
in `route.test.ts` so each one names the subject explicitly (for example, the
GET handler) and uses “must” to state the expected result, keeping the existing
behavior intent unchanged.

Source: Coding guidelines

src/features/article-tag-filter/ui/deferred-article-tag-filter-list.tsx (1)

71-98: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

locale prop을 AppLocale로 타입 지정하는 것을 권장

locale prop이 string으로 타입되어 있어 queryKey에서 locale as AppLocale 캐스팅을 사용하고 있습니다. 이 캐스팅은 타입 안전성을 우회하며, 잘못된 locale 값이 전달될 경우 예상치 못한 캐시 키가 생성될 수 있습니다. prop 타입을 AppLocale로 직접 지정하면 컴파일 타임에 유효한 locale만 허용되도록 보장할 수 있습니다.

♻️ 제안하는 타입 개선
 type DeferredArticleTagFilterListProps = {
   activeTag?: string;
   defaultLabel: string;
   emptyText: string;
   hrefMode?: TagFilterHrefMode;
-  locale: string;
+  locale: AppLocale;
   source?: TagFilterSource;
   title?: string;
   onNavigationStart?: () => void;
 };

이후 queryKey에서 as AppLocale 캐스팅을 제거할 수 있습니다:

     queryKey:
-      source === 'all' ? tags.all(locale as AppLocale) : articles.popularTags(locale as AppLocale),
+      source === 'all' ? tags.all(locale) : articles.popularTags(locale),
🤖 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 `@src/features/article-tag-filter/ui/deferred-article-tag-filter-list.tsx`
around lines 71 - 98, The locale prop is still typed as string and forces unsafe
AppLocale casts in DeferredArticleTagFilterList. Update the component’s locale
prop type to AppLocale so useQuery can build queryKey with the strongly typed
locale directly, then remove the locale as AppLocale casts from the queryKey
branches. Make sure the change is reflected anywhere
DeferredArticleTagFilterList is called so only valid AppLocale values are passed
in.
src/shared/lib/query/query-keys.ts (1)

16-27: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

정규화 로직을 공용 유틸로 분리해 주세요. query-keys.ts와 get-articles.ts가 같은 trim/소문자화 규칙을 각각 구현하고 있어, 한쪽만 바뀌면 캐시 키와 RPC 파라미터가 어긋날 수 있습니다. src/shared/lib로 정규화 함수를 빼서 두 곳에서 재사용하는 편이 안전합니다.

🤖 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 `@src/shared/lib/query/query-keys.ts` around lines 16 - 27,
`normalizeSearchQuery` and `normalizeArticleTag` duplicate the same
trim/lowercase normalization logic, so extract this behavior into a shared
utility under `src/shared/lib` and make both `query-keys.ts` and
`get-articles.ts` call that shared helper. Keep the existing symbols
(`normalizeSearchQuery`, `normalizeArticleTag`) as thin wrappers or replace
their internal logic with the shared function so cache keys and RPC params
always use the same normalization rule.
🤖 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 `@src/app/api/`(content)/article-popular-tags/route.test.ts:
- Around line 1-27: The route handler test for article-popular-tags should run
in the Node environment instead of the default DOM-based setup. Add an explicit
Vitest environment directive to the route.test.ts file so the GET test and its
getLocalizedPopularArticleTags mock execute under node, matching the Route
Handler context and avoiding jsdom reliance.

In `@src/entities/pdf-file/api/get-pdf-file-availability.ts`:
- Around line 50-51: The comment in getPdfFileAvailability is copied from
article publishing logic and does not match this PDF availability check. Update
the inline comment near getPdfFileAvailability and cacheLife to describe PDF
storage existence/TTL behavior instead of publish_at/article wording, keeping
the explanation aligned with the function’s purpose.

In `@src/features/article-comment/model/use-article-comments-page.ts`:
- Around line 48-67: `useArticleCommentsPage`에서 `locale`이 queryFn 의존성인데
`articleComments.page(articleId, sort, page)` 키에 빠져 있어 로케일 전환 시 잘못된 캐시가 재사용됩니다.
`articleComments.page`가 `locale`까지 포함하도록 키 생성 로직을 수정하고, 이 훅의 `useQuery`와 함께 관련
`setQueryData` 호출도 같은 키 형태로 맞추세요. `articleComments.scope(articleId)` 기반의 상위 무효화는
그대로 유지하면 됩니다.

In `@src/features/browse-articles/model/use-browse-articles.test.ts`:
- Line 1: The test file for use-browse-articles is missing the required jsdom
environment directive, which is needed for renderHook and window.fetch spying.
Add the Vitest pragma at the top of the use-browse-articles.test.ts file so the
hook test runs in JSDOM, matching the project’s test guidelines for React hooks
and DOM-dependent logic.

In `@src/features/edit-markdown-adapter/model/fetch-link-preview-meta.ts`:
- Around line 44-51: createCachedFetchLinkPreviewMeta의 queryClient.fetchQuery
호출에 gcTime이 빠져 있어 기본 inactive GC(5분)로 캐시가 너무 빨리 사라집니다. staleTime과 동일하게
OG_PREVIEW_STALE_TIME을 gcTime에도 설정해서 fetchLinkPreviewMetaAdapter를 통해 가져온 프리뷰 메타가
24시간 동안 유지되도록 수정하세요.

In `@src/widgets/article-comments/ui/article-comments-section.tsx`:
- Around line 194-221: `refreshCommentsPage` currently returns early on a failed
`getArticleCommentsPageAction`, which skips `queryClient.invalidateQueries` and
can leave the article comments cache/UI stale. Update the `refreshCommentsPage`
callback in `article-comments-section.tsx` so the query invalidation for
`articleComments.scope(articleId)` always runs, even when the refresh fetch
fails; keep `setQueryData` and `setQueryState` only on successful `result.ok &&
result.data` paths, but move the invalidation into a path that executes
regardless of success.

---

Nitpick comments:
In `@src/app/api/`(content)/article-popular-tags/route.test.ts:
- Line 15: The test description in the `it(...)` blocks for the article popular
tags route should be rewritten to match the guideline format: “Under
[Condition/Context], [Subject] must [Expected Behavior/State Change]”. Update
the affected test names in `route.test.ts` so each one names the subject
explicitly (for example, the GET handler) and uses “must” to state the expected
result, keeping the existing behavior intent unchanged.

In `@src/features/article-tag-filter/ui/deferred-article-tag-filter-list.tsx`:
- Around line 71-98: The locale prop is still typed as string and forces unsafe
AppLocale casts in DeferredArticleTagFilterList. Update the component’s locale
prop type to AppLocale so useQuery can build queryKey with the strongly typed
locale directly, then remove the locale as AppLocale casts from the queryKey
branches. Make sure the change is reflected anywhere
DeferredArticleTagFilterList is called so only valid AppLocale values are passed
in.

In `@src/shared/lib/query/query-keys.ts`:
- Around line 16-27: `normalizeSearchQuery` and `normalizeArticleTag` duplicate
the same trim/lowercase normalization logic, so extract this behavior into a
shared utility under `src/shared/lib` and make both `query-keys.ts` and
`get-articles.ts` call that shared helper. Keep the existing symbols
(`normalizeSearchQuery`, `normalizeArticleTag`) as thin wrappers or replace
their internal logic with the shared function so cache keys and RPC params
always use the same normalization rule.
🪄 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: d9984aa4-4417-4bdd-8659-d4e6dfd13433

📥 Commits

Reviewing files that changed from the base of the PR and between b654793 and 35bf281.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (74)
  • package.json
  • src/app/api/(content)/article-popular-tags/route.test.ts
  • src/app/api/(content)/article-popular-tags/route.ts
  • src/app/layout.tsx
  • src/entities/article/api/detail/get-article-detail-list.test.ts
  • src/entities/article/api/detail/get-article-detail-list.ts
  • src/entities/article/api/detail/get-article.test.ts
  • src/entities/article/api/detail/get-related-articles.test.ts
  • src/entities/article/api/detail/get-related-articles.ts
  • src/entities/article/api/list/get-admin-google-article-traffic.test.ts
  • src/entities/article/api/list/get-admin-google-article-traffic.ts
  • src/entities/article/api/list/get-articles.test.ts
  • src/entities/article/api/list/get-articles.ts
  • src/entities/article/api/list/get-popular-article-tags.test.ts
  • src/entities/article/api/list/get-popular-article-tags.ts
  • src/entities/article/comment/api/get-article-comments.test.ts
  • src/entities/article/comment/api/get-article-comments.ts
  • src/entities/guestbook/api/get-guestbook-threads.test.ts
  • src/entities/guestbook/api/get-guestbook-threads.ts
  • src/entities/pdf-file/api/get-pdf-file-availability.test.ts
  • src/entities/pdf-file/api/get-pdf-file-availability.ts
  • src/entities/pdf-file/api/get-pdf-file-content.test.ts
  • src/entities/pdf-file/api/get-pdf-file-content.ts
  • src/entities/project/api/detail/get-project-detail-list.test.ts
  • src/entities/project/api/detail/get-project-detail-list.ts
  • src/entities/project/api/detail/get-project.test.ts
  • src/entities/project/api/list/get-projects.test.ts
  • src/entities/project/api/list/get-projects.ts
  • src/entities/tag/api/query-tags.test.ts
  • src/entities/tag/api/query-tags.ts
  • src/features/article-comment/model/article-comments-page-cache.test.ts
  • src/features/article-comment/model/article-comments-page-cache.ts
  • src/features/article-comment/model/use-article-comments-page.ts
  • src/features/article-tag-filter/ui/deferred-article-tag-filter-list.test.tsx
  • src/features/article-tag-filter/ui/deferred-article-tag-filter-list.tsx
  • src/features/browse-articles/model/use-browse-articles.test.ts
  • src/features/browse-articles/model/use-browse-articles.ts
  • src/features/browse-projects/model/use-browse-projects.ts
  • src/features/edit-markdown-adapter/index.ts
  • src/features/edit-markdown-adapter/model/fetch-link-preview-meta.ts
  • src/shared/lib/query/create-query-client.ts
  • src/shared/lib/query/query-keys.test.ts
  • src/shared/lib/query/query-keys.ts
  • src/shared/lib/react/cursor-pagination-feed-state.test.ts
  • src/shared/lib/react/cursor-pagination-feed-state.ts
  • src/shared/lib/react/use-cursor-pagination-feed.test.tsx
  • src/shared/lib/react/use-cursor-pagination-feed.ts
  • src/shared/lib/seo/canonical.test.ts
  • src/shared/lib/seo/metadata.test.ts
  • src/shared/lib/test/render-with-query-client.tsx
  • src/shared/providers/index.ts
  • src/shared/providers/query-provider.tsx
  • src/shared/ui/markdown/link-embed-card.test.tsx
  • src/shared/ui/markdown/link-embed-card.tsx
  • src/shared/ui/markdown/markdown-renderer.test.tsx
  • src/shared/ui/pdf-download-popover/deferred-pdf-download-popover.tsx
  • src/views/articles/model/get-article-detail-page-data.ts
  • src/views/articles/model/get-articles-page-data.ts
  • src/views/project/model/get-project-detail-page-data.ts
  • src/views/resume/ui/resume-page.test.tsx
  • src/widgets/admin-pdf-upload/ui/admin-pdf-upload-panel.test.tsx
  • src/widgets/admin-pdf-upload/ui/admin-pdf-upload-panel.tsx
  • src/widgets/article-comments/ui/article-comments-section.interactions.test.tsx
  • src/widgets/article-comments/ui/article-comments-section.test-support.tsx
  • src/widgets/article-comments/ui/article-comments-section.tsx
  • src/widgets/detail-page/archive/feed.test.tsx
  • src/widgets/detail-page/archive/feed.tsx
  • src/widgets/editor/ui/core/editor-core.utils.test.ts
  • src/widgets/editor/ui/publish/publish-panel.test.tsx
  • src/widgets/editor/ui/publish/publish-panel.tsx
  • src/widgets/editor/ui/publish/publish-panel.utils.test.ts
  • src/widgets/home-hero-scene/model/use-home-hero-project-preview.test.tsx
  • src/widgets/home-hero-scene/model/use-home-hero-project-preview.ts
  • src/widgets/home-hero-scene/ui/home-hero-scene.test.tsx
💤 Files with no reviewable changes (6)
  • src/shared/lib/react/cursor-pagination-feed-state.test.ts
  • src/features/article-comment/model/article-comments-page-cache.test.ts
  • src/shared/lib/react/cursor-pagination-feed-state.ts
  • src/shared/lib/react/use-cursor-pagination-feed.test.tsx
  • src/features/article-comment/model/article-comments-page-cache.ts
  • src/shared/lib/react/use-cursor-pagination-feed.ts

Comment thread src/app/api/(content)/article-popular-tags/route.test.ts
Comment thread src/entities/pdf-file/api/get-pdf-file-availability.ts Outdated
Comment thread src/features/article-comment/model/use-article-comments-page.ts
Comment thread src/features/browse-articles/model/use-browse-articles.test.ts
Comment thread src/features/edit-markdown-adapter/model/fetch-link-preview-meta.ts
Comment thread src/widgets/article-comments/ui/article-comments-section.tsx
- 아카이브 사이드바 쿼리 키에 현재 아이템(selectedPathSegment) 포함 — 동일 커서 시드의 상세 페이지 간 캐시 혼선 방지
- 댓글 페이지 쿼리 키에 locale 포함 — 클라이언트 locale 전환 시 이전 locale 캐시 재사용 방지
- 댓글 fresh 재조회 실패(액션 실패/예외) 시 scope 즉시 무효화로 UI 자동 복구 보장
- OG 프리뷰 fetchQuery에 gcTime 24h 동기화(기본 5분 GC로 staleTime이 무력화되던 문제)
- 라우트 핸들러 테스트 node 환경 명시, 훅 테스트 jsdom 명시, PDF cacheLife 오복사 주석 정정

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pcwadarong
pcwadarong merged commit a2cfed7 into main Jul 8, 2026
6 checks passed
@pcwadarong
pcwadarong deleted the refactor/api-optimization branch July 8, 2026 07:58
@pcwadarong pcwadarong mentioned this pull request Aug 29, 2026
6 of 7 tasks
@pcwadarong pcwadarong changed the title ♻️ refactor: API 호출 최적화 — React Query 도입과 서버 캐시 정비 API 호출 최적화 - React Query 도입과 서버 캐시 정비 Sep 1, 2026

This branch was successfully deployed

1 active deployment
Preview — ffec09cc Deployed Jul 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant