Skip to content

Conversation

@u-zzn
Copy link
Collaborator

@u-zzn u-zzn commented Jan 7, 2026

✏️ Summary

  • close [Init] TanStack Query 초기 세팅 #25

  • Comfit 서비스의 서버 상태 관리를 위해 TanStack Query(React Query) 초기 인프라를 세팅했습니다.

  • QueryClient 전역 옵션(캐싱/재시도/리패치)을 정의하고, QueryClientProvider를 앱 전역에 적용했습니다.

  • 개발 환경에서만 Devtools가 로드되도록 lazy import + DEV 조건 분기를 적용하여, 프로덕션 번들 영향을 최소화했습니다.

  • 전역 Provider가 늘어날 것을 대비해 AppProviders 엔트리로 구성해, main.tsx가 커지지 않도록 확장 가능한 구조를 만들었습니다.

📑 Tasks

✔️ Comfit이 TanStack Query를 도입한 이유는 무엇일까요?

설치한 의존성
  • @tanstack/react-query
  • @tanstack/react-query-devtools

Comfit은 이미 axios(swagger-typescript-api 기반)와 Zustand를 “역할 분리” 관점에서 세팅해둔 상태입니다.

  • Zustand: 로그인 여부(isLoggedIn) 등 “클라이언트 UI 상태” 최소화
  • axios + generator: API 호출 및 서버 스펙 동기화
  • TanStack Query: “서버 상태(비동기 데이터)의 캐싱/동기화/재검증” 담당

즉, Comfit에서 TanStack Query는 서버 상태 관리의 기본기(캐싱, stale 정책, refetch 정책)를 공통화하기 위해 도입합니다.


✔️ 왜 QueryClient 전역 옵션을 아래와 같이 설정했을까요?

☑️ retry: 0
Comfit의 주요 API는 다음과 같은 성격을 가집니다.

  • 기업 데이터 조회
  • 사용자 입력(경험/키워드)을 기반으로 한 매칭 결과 생성
  • 자소서 가이드/추천 결과 도출

이 과정에서 401/403(인증·권한), 400(입력 오류), 500(서버 처리 실패)가 발생했을 때 자동 재시도가 반복되면 같은 요청이 여러 번 서버로 전송되고 사용자는 “왜 안 되는지 모른 채 기다리다가 실패”하는 UX를 겪게 될 것 입니다. 초기 단계에서 “명확한 실패 → 명확한 피드백”이 중요하다고 판단하여 전역 retry를 끄고, 필요한 경우 도메인/쿼리 단위로만 override하는 전략을 선택해 세팅했습니다.

☑️ refetchOnWindowFocus: false
기본값(true)을 그대로 두면, 탭 이동이나 브라우저 포커스 이동, 다른 앱 → 다시 돌아오기 등과 같은 상황에서 매번 데이터가 자동으로 refetch 됩니다. Comfit에서는 기업 리스트/매칭 결과가 갑자기 다시 로딩되며 UI가 깜빡이는 등의 문제가 발생하면 안된다고 판단하여 포커스 이동 시 자동 갱신은 전역에서 비활성화하도록 세팅했습니다.

☑️ staleTime: 30 * 1000 (30초)
Comfit의 데이터는 다음 두 가지 성격이 섞여 있습니다.

  • 기업 정보, 직무 정보 → 비교적 안정적
  • 매칭 결과, 추천 가이드 → 요청 시점에 따라 달라질 수 있음
    아주 짧은 staleTime은 불필요한 요청을 늘리고, 너무 긴 staleTime은 “방금 본 정보가 너무 오래 유지되는” 문제를 만들기에 제가 생각했을 때 30초 정도로 초기 세팅해두는 것이 과도한 네트워크 요청을 줄이고 UX도 안정시킬 수 있다 판단하여 이렇게 설정했는데 더 좋은 의견 있으시면 편하게 말씀해주세요 ☺️

☑️ gcTime: 10 * 60 * 1000 (10분)
Comfit은 사용자가 여러 기업을 비교하고 앞서 보던 기업으로 다시 돌아오는 “왔다 갔다” 탐색 흐름이 잦은 서비스라고 생각합니다.
캐시를 너무 빨리 제거하면 페이지 복귀 시마다 동일한 요청이 반복되고, 너무 오래 유지하면 메모리 점유가 커지기에 10분으로 설정했는데, 이 또한 같이 이야기 나눠보면 좋을 것 같습니다 ☺️

☑️ throwOnError: false
현재 Comfit은 Suspense / ErrorBoundary 전역 전략이 확정되지 않았고 axios 공통 에러 핸들링 PR도 아직 merge되지 않은 상태입니다.
이 시점에서 에러를 전역 throw로 처리하면 의도치 않은 전체 렌더 중단이나 UX 설계가 아직 없는 에러 화면 노출될 수 있기에 일단 초기에는 isError, error 상태를 UI 레이어에서 명시적으로 다루는 방식을 택했고, 향후 팀 합의에 따라 Suspense/ErrorBoundary 전략 도입 시 후속 PR로 작업해보도록 하겠습니다 :)


✔️ 전역 에러 핸들 포인트 구성 (QueryCache / MutationCache)

queryCache: new QueryCache({
  onError: (error: unknown) => {
    // TODO: API 초기 세팅 PR merge 후 공통 에러 핸들러 연결
    console.error(error);
  },
}),
mutationCache: new MutationCache({
  onError: (error: unknown) => {
    // TODO: API 초기 세팅 PR merge 후 공통 에러 핸들러 연결
    console.error(error);
  },
}),

Comfit은 이미 axios 초기 세팅 PR에서 handleApiError(서버 정의 에러 타입가드 기반) 같은 공통 핸들러 방향이 논의되었습니다.
TanStack Query는 요청 함수(queryFn)에서 발생하는 에러를 일관된 곳에서 처리할 수 있도록 QueryCache/MutationCache의 전역 onError hook을 제공합니다.

다만 현재 시점에서는

  • API 초기 세팅 PR이 아직 merge되지 않아 @shared/api/error-handler를 import할 수 없고,
  • error handling 전략(토스트/리다이렉트/로그 정책)이 팀 합의로 확정되지 않은 상태입니다.

그래서 본 PR에서는 추후 연결할 자리만 주석으로 남겨놓았습니다. 이후 API PR merge 후 handleApiError를 연결하면 개별 컴포넌트에 중복 처리 코드를 작성하지 않아도 전역에서 일괄 처리가 가능해집니다.


✔️ QueryClientProvider 전역 적용 + Devtools DEV-only 로드

import.meta.env.DEV를 사용하여 개발 환경에서만 Devtools가 활성화되도록 했습니다.

☑️ Devtools를 조건부 + lazy import로 분리한 이유

Devtools는 프로덕션 빌드에는 불필요한 코드이고, 프로덕션 화면에 노출될 필요가 없다고 판단하여 개발 환경에서만 실제 import가 되도록 동적 import를 적용했습니다.

const ReactQueryDevtools = import.meta.env.DEV
  ? lazy(async () => {
      const mod = await import("@tanstack/react-query-devtools");
      return { default: mod.ReactQueryDevtools };
    })
  : null;

✔️ 전역 Provider 엔트리 구성 (app/providers/index.tsx) + main.tsx 최소화

초기 세팅 단계에서는 Provider가 하나뿐이지만, Comfit은 추후 다음과 같은 전역 Provider가 추가될 가능성이 높습니다.

  • ThemeProvider (디자인 시스템/테마)
  • Router 관련 Provider
  • Auth 초기 동기화 컴포넌트 (Zustand syncFromStorage 같은 초기화)
  • ErrorBoundary / Suspense 전략 적용

이때 main.tsx에 Provider를 계속 추가하면 진입 파일이 빠르게 커지고, 전역 설정 진입점이 분산될 수 있습니다.
따라서 AppProviders라는 단일 엔트리를 두어 “전역 Provider 조립은 app/providers에서만 한다”는 규칙을 만들어 세팅 진행했습니다.

  • main.tsx는 앱 부트스트랩에만 집중
  • Provider 확장은 app/providers/index.tsx에서만 처리

✔️ FSD 구조 기반 파일/폴더 위치 선정 근거

  • src/shared/lib/react-query/*
    QueryClient는 특정 도메인에 종속되지 않는 “공통 라이브러리 설정”이므로 shared/lib에 배치했습니다.
    (도메인별 queryKey factory나 queryFn 래퍼는 추후 entities/features 레이어에서 도입 가능합니다.)

  • src/app/providers/*
    app 레이어는 “앱 조립(Composition)” 책임을 가지므로 전역 Provider 조립은 app/providers가 적절하다고 판단했습니다.

  • src/shared/lib/react-query/index.ts (barrel export)
    내부 파일 구조 변경 시 import 경로가 흔들리지 않도록 공개 API를 한 곳으로 고정하는 역할을 합니다.
    @shared/lib/react-query로만 import하도록 유도해 유지보수 비용이 줄어듭니다.

👀 To Reviewer

  • 실제 도메인 API 연동(쿼리 생성), queryKey 컨벤션 정의, 공통 fetcher 래핑 등은 API 초기 세팅 PR merge 이후 또는 실제 기능 개발 단계에서 논의하여 도입하는 것이 더 적절하다고 판단하여 범위에서 제외했습니다.
  • QueryCache/MutationCache의 전역 onError hook에는 현재 console.error로만 처리했으며, 추후 API 초기 세팅 PR merge 후 handleApiError를 연결해 공통 에러 처리(토스트/리다이렉트 등)로 확장할 예정입니다.
  • 전역 Query 옵션 값들은 일단 임의로 설정해둔 상태라, 실제 Comfit 서비스 흐름에 비춰봤을 때 더 좋은 값이나 조정이 필요해 보이는 부분이 있다면 자유롭게 의견 주시면 감사합니다 ☺️🖤

📸 Screenshot

DEV 환경에서 TanStack Query Devtools가 정상적으로 노출되는 모습입니다.

image image

🔔 ETC

@github-actions
Copy link

github-actions bot commented Jan 7, 2026

🚀 빌드 결과

린트 검사 완료
빌드 성공

로그 확인하기
Actions 탭에서 자세히 보기

Copy link
Collaborator

@odukong odukong left a comment

Choose a reason for hiding this comment

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

수고 하셨습니다!! 각 옵션마다 설정한 이유에 대해 Comfit 서비스와 연결하여 고민하신 부분이 잘 드러난 PR 인 것 같아요! (^///^)

저는 현재 설정된 defaultOption으로 충분할 것 같고, staleTime 옵션의 경우, 말씀하신 대로 기업 정보/직무 정보는 거의 변하지 않는 정적 데이터에 가깝기 때문에 이런 특정 도메인의 쿼리들은 5분 정도의 별도 설정을 해줘도 좋을 것 같습니다!

몇 가지 제안에 대한 코멘트들, 아래에 시간 되실 때 확인 부탁드립니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

작성해주신 구조가 query-client인스턴스와 인스턴스를 제공하는 provider를 각각 sharedproviders 레이어로 분리한 점이 FSD구조 흐름과 잘 맞는다고 생각합니다!
한 가지 제안 드리고 싶은 점은 query-client.ts 파일의 위치인데요. query-client는 서버 상태, api 데이터에 대한 관리를 처리하는 역할을 담당하다보니 기존의 유틸 함수를 모아놓는 shared/lib보다 아래 구조처럼 axios-instance와 같이 API 관련 설정들이 모여있는 shared/api 내부에 두는 것이 더 직관적일 것 같다는 생각이 들었습니다!

📂 src
  └─📂 shared
      └─📂 api
          └─📄 query-client.ts

한 번 고려해주시면 좋을 부분 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

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

저도 수빈님의 의견에 동의합니다! 유틸함수가 아닌 것은 아니지만 api를 위한 파일이라는 명확한 목적이 있기 때문에 api 폴더에 넣어두면 충분할 거 같아요 ☺️

Comment on lines +5 to +7
export default function AppProviders({ children }: PropsWithChildren) {
return <QueryProvider>{children}</QueryProvider>;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

저는 query-provider로 바로 App.tsx를 감싸는 구조로 생각했는데, 여러 provider를 합성하는 역할을 수행하는 AppProviders를 별도로 둔 점이 인상깊었어요.
만약 다른 provider가 늘어날 경우, App.tsx가 여러 provider로 감싸지는 약간은 지저분한 구조가 되었을 것 같은데
앱 진입점을 깔끔하게 유지할 수 있는 방향이라 확장성 측면에서 너무 좋은 구조화같습니다❣️

Copy link
Contributor

Choose a reason for hiding this comment

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

index.ts에서 단순히 export만 모아주는 방식이 아닌 AppProviders로 묶어주는 구조를 처음 봐서 조금 찾아보았는데 App.tsx 이 깔끔해지고 추가되는 프로바이더를 해당 파일에서만 추가하면 된다는 점에서 유지보수성이 굉장히 좋은 코드인 거 같습니다~ 하나 배워가욥~!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Comfit은 컴포넌트 명칭은 파스칼케이스로 정의하지만, 파일이나 폴더의 이름은 케밥케이스을 사용하고 있죠!
QueryProvider.ts그리고 queryClient.ts를 query-provider.tsquery-client.ts로 컨벤션 맞춰주면 좋을 것 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

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

아우~꼼꼼해😍

Comment on lines +7 to +12
const ReactQueryDevtools = import.meta.env.DEV
? lazy(async () => {
const mod = await import("@tanstack/react-query-devtools");
return { default: mod.ReactQueryDevtools };
})
: null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

단순히 DEV && <ReactQueryDevtools />로 처리하다보면 ReactQueryDevtool 코드가 프로덕션 번들에 포함되었을텐데, lazy를 사용해 번들 사이즈를 줄인 부분 너무 좋습니다!!

Copy link
Contributor

@hummingbbird hummingbbird left a comment

Choose a reason for hiding this comment

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

수고많으셨어요~! 통상적으로 사용되는 값을 사용하는 게 아닌 서비스의 특성과 구조를 충분히 고려하여 defaultOptions를 작성해주신 점 아주 좋습니다~ query-client.ts 파일의 위치만 수빈님 말씀대로 이동해주시면 좋을 거 같아요! 수고하셨어요 😍

(eslint warning이 몇 가지 발생하는데 이것만 수정부탁드려요!)
image


queryCache: new QueryCache({
onError: (error: unknown) => {
// TODO: API 초기 세팅 PR merge 후 공통 에러 핸들러 연결
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO로 추후에 필요한 작업 정리해둔 점 아주 굿~

Comment on lines +5 to +7
export default function AppProviders({ children }: PropsWithChildren) {
return <QueryProvider>{children}</QueryProvider>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

index.ts에서 단순히 export만 모아주는 방식이 아닌 AppProviders로 묶어주는 구조를 처음 봐서 조금 찾아보았는데 App.tsx 이 깔끔해지고 추가되는 프로바이더를 해당 파일에서만 추가하면 된다는 점에서 유지보수성이 굉장히 좋은 코드인 거 같습니다~ 하나 배워가욥~!!

Copy link
Collaborator

@qowjdals23 qowjdals23 left a comment

Choose a reason for hiding this comment

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

! TanStack Query 초기 세팅 수고 많으셨습니다 !
특히 왜 이 옵션을 이렇게 잡았는지를 Comfit 서비스 맥락으로 풀어둔 것 !! 너무너무 좋네요 !!

QueryCache / MutationCache에 onError 붙여두고 추후 공통 에러 핸들러 연결 TODO까지 남겨둔 것도 좋았고 ! Devtools도 import.meta.env.DEV로 DEV에서만 + lazy import로 로딩되게 해서 프로덕션 번들 영향 최소화한 부분도 너무 좋습니다 !!

덕분에 이후에 쿼리 붙일 때 편해질 것 같아요 ! 수고하셨습니다 ❤️‍🔥

@@ -1 +1 @@
export { queryClient } from "./queryClient";
export { queryClient } from "../../api/queryClient"; No newline at end of file
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 queryClient인스턴스는 shared/api/query-client 경로로 이동되었는데, shared/lib/react-query 경로에 index.ts가 위치하는 이유가 무엇인지 알 수 있을까요?
app providers에서도 shared/lib/react-query 경로에서의 queryClient를 import하고 있더라구요, shared/api/query-client경로의 queryClient를 바로 import하지 않으신 이유가 궁금합니다!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

좋은 질문 감사합니다! 아까 리뷰 반영할 때도 고민했지만, 말씀해주신 부분 저도 다시 보면서 구조가 애매해졌다고 느꼈습니다.

처음에는 react-query 관련 설정을 하나의 진입점으로 묶어두고 싶어서 shared/lib/react-query/index.ts를 공개 엔트리로 두었는데, 생각해보니 리뷰 주신 대로 queryClient의 실제 책임이 shared/api로 이동한 상태에서는 해당 index 파일만 lib/react-query에 남아있는 것이 오히려 역할을 혼동시킬 수 있다고 생각합니다 ..

그래서 react-query 관련 공개 엔트리(index.ts)도 함께 shared/api로 이동하고, app providers에서는 shared/api 경로에서 queryClient를 직접 import하도록 구조를 정리하는 것이 더 명확하다고 생각합니다.

말씀해주신 방향에 동의하며, 해당 부분은 구조 정리 커밋으로 바로 반영하겠습니다. 감사합니다 🙇‍♀️

Copy link
Collaborator

Choose a reason for hiding this comment

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

오아아 빠른 피드백... 구조 정리까지 바로 반영해주셔서 감사합니다🩷

@u-zzn u-zzn requested a review from odukong January 8, 2026 17:31
Copy link
Contributor

Choose a reason for hiding this comment

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

query-client 파일이 이동해서 해당 index.ts 파일도 shared/api 로 옮겨주어야 할 거 같습니다!

@u-zzn u-zzn merged commit 3c7041e into dev Jan 8, 2026
2 checks passed
@u-zzn u-zzn deleted the init/#25/tanstack-query-init branch January 8, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Init] TanStack Query 초기 세팅

5 participants