{"id":15030290,"url":"https://github.com/ssi02014/react-query-tutorial","last_synced_at":"2025-05-14T11:12:44.782Z","repository":{"id":37433107,"uuid":"456514448","full_name":"ssi02014/react-query-tutorial","owner":"ssi02014","description":"😃  TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리 ","archived":false,"fork":false,"pushed_at":"2025-04-27T06:03:06.000Z","size":482,"stargazers_count":1580,"open_issues_count":0,"forks_count":159,"subscribers_count":10,"default_branch":"main","last_synced_at":"2025-04-27T07:18:57.921Z","etag":null,"topics":["caching","react","react-query","tanstack-query","typescript"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ssi02014.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-02-07T13:21:29.000Z","updated_at":"2025-04-27T06:03:10.000Z","dependencies_parsed_at":"2023-02-01T08:31:45.647Z","dependency_job_id":"88baa29a-7848-4c78-a753-e290106968d1","html_url":"https://github.com/ssi02014/react-query-tutorial","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ssi02014%2Freact-query-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ssi02014%2Freact-query-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ssi02014%2Freact-query-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ssi02014%2Freact-query-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ssi02014","download_url":"https://codeload.github.com/ssi02014/react-query-tutorial/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254129526,"owners_count":22019628,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["caching","react","react-query","tanstack-query","typescript"],"created_at":"2024-09-24T20:13:00.023Z","updated_at":"2025-05-14T11:12:44.765Z","avatar_url":"https://github.com/ssi02014.png","language":null,"readme":"# 💻 TanStack Query(React)\n\n- 해당 저장소는 TanStack Query(React)에서 자주 사용하는 개념들을 정리한 저장소입니다. TanStack Query(React)의 모든 활용 방법이 작성된 상태는 아니며, 필요한 내용은 추가, 보완할 예정입니다.\n\n\u003cbr /\u003e\n\n## Contributors\n\n- 기여해주신 모든 분께 감사드립니다.\n- 오탈자, 가독성이 좋지 않은 부분 또는 추가 내용은 `Pull Request`, `Issue` 등을 자유롭게 남겨주시면 검토 후에 반영하겠습니다.\n\n[![contributors](https://contrib.rocks/image?repo=ssi02014/react-query-tutorial)](https://github.com/ssi02014/react-query-tutorial/graphs/contributors)\n\n\u003cbr /\u003e\n\n## TanStack Query(React) v5\n\n- ⭐️ TanStack Query(React) `v5`가 23.10.17에 릴리즈됐습니다. 해당 문서는 `v5` 기준으로 작성되어 있습니다.\n- ⭐️ 기존 `v4` 문서는 [react query tutorial v4 문서](https://github.com/ssi02014/react-query-tutorial/tree/master/README.v4.md)를 확인해 주세요.\n\n![스크린샷 2023-10-18 오전 2 09 09](https://github.com/ssi02014/react-query-tutorial/assets/64779472/84de2a61-7e39-4d52-aed8-b0ab67af95bc)\n\n- `v3 -\u003e v4`, `v4 -\u003e v5` Migrating 정리 문서는 아래 문서들을 확인해 주시기 바랍니다.\n- [Migrating to TanStack Query(React) v5](https://github.com/ssi02014/react-query-tutorial/tree/master/document/v5.md)\n\n- [Migrating to TanStack Query(React) v4](https://github.com/ssi02014/react-query-tutorial/tree/master/document/v4.md)\n\n\u003cbr /\u003e\n\n## 주요 컨셉 및 가이드 목차\n\n1. [React Query 개요 및 기능](#개요)\n2. [기본 설정(QueryClientProvider, QueryClient)](#react-query-기본-설정)\n3. [React Query Devtools](#devtools)\n4. [React Query 캐싱 라이프 사이클](#캐싱-라이프-사이클)\n5. [useQuery](#usequery)\n6. [useQuery 주요 리턴 데이터](#usequery-주요-리턴-데이터)\n7. [staleTime과 gcTime](#staletime과-gctime)\n8. [마운트 될 때마다 재요청하는 refetchOnMount](#refetchonmount)\n9. [윈도우가 포커싱 될 때마다 재요청하는 refetchOnWindowFocus](#refetchonwindowfocus)\n10. [Polling 방식을 구현하기 위한 refetchInterval와 refetchIntervalInBackground)](#polling)\n11. [자동 실행의 enabled와 수동으로 쿼리를 다시 요청하는 refetch](#enabled-refetch)\n12. [실패한 쿼리에 대해 재요청하는 retry](#retry)\n13. [onSuccess, onError, onSettled](#onsuccess-onerror-onsettled) - 💡 **v5 @Deprecated**\n14. [select를 이용한 데이터 변환](#select)\n15. [쿼리가 pending 상태인 동안 보여 줄 수 있는 placeholderData](#placeholderdata)\n16. [Paginated 구현에 유용한 keepPreviousData](#keepPreviousData) - 💡 **v5 @Deprecated**\n17. [특정 쿼리 프로퍼티 변경 시에만 리렌더링을 트리거 할 수 있는 notifyOnChangeProps](#notifyOnChangeProps)\n18. [쿼리를 병렬(Parallel) 요청할 수 있는 useQueries](#parallel)\n19. [종속 쿼리(Dependent Queries)](#dependent-queries)\n20. [QueryClient 인스턴스를 반환하는 useQueryClient](#usequeryclient)\n21. [초기 데이터를 설정할 수 있는 initialData](#initial-query-data)\n22. [데이터를 미리 불러오는 PreFetching](#prefetching)\n23. [Infinite Queries(무한 쿼리) + useInfiniteQuery](#infinite-queries)\n24. [서버와 HTTP CUD관련 작업을 위한 useMutation](#usemutation)\n25. [쿼리 수동 취소 cancelQueries](#cancelqueries)\n26. [쿼리를 무효화할 수 있는 queryClient.invalidateQueries](#쿼리-무효화)\n27. [캐시 데이터 즉시 업데이트를 위한 queryClient.setQueryData](#캐시-데이터-즉시-업데이트)\n28. [사용자 경험(UX)을 올려주는 Optimistic Updates(낙관적 업데이트)](#optimistic-update)\n29. [에러가 발생했을 때 Fallback UI를 선언적으로 보여주기 위한 ErrorBoundary + useQueryErrorResetBoundary](#usequeryerrorresetboundary)\n30. [서버 로딩 중일 때 Fallback UI를 선언적으로 보여주기 위한 Suspense](#suspense)\n31. [앱 전체에 동일한 쿼리 함수를 공유하는 Default Query Function](#default-query-function)\n32. [리액트 쿼리에 타입스크립트 적용](#react-query-typescript)\n33. [리액트 쿼리 ESLint 적용](#react-query-eslint-plugin)\n34. [리액트 쿼리 지원 버전](#지원-버전)\n\n\u003cbr /\u003e\n\n## 📃 기타 참고 문서\n\n1. [QueryClient 주요 내용 정리 문서](https://github.com/ssi02014/react-query-tutorial/tree/master/document/queryClient.md)\n2. [기본적인 React Query 아키텍처 살펴보기: inside React Query](https://github.com/ssi02014/react-query-tutorial/tree/master/document/insideReactQuery.md)\n\n\u003cbr /\u003e\n\n## 👨🏻‍💻 주요 참고 페이지\n\n- [TanStack/query 공식 깃허브](https://github.com/TanStack/query)\n- [TkDodo 블로그(TanStack Query maintainer)](https://tkdodo.eu/blog/)\n\n\u003cbr /\u003e\n\n## 📃 React Query 개요 및 기능\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n### 개요\n\n- react-query는 리액트 애플리케이션에서 `서버 상태 가져오기`, `캐싱`, `동기화 및 업데이트`를 보다 쉽게 다룰 수 있도록 도와주는 라이브러리이다. 클라이언트 상태와 서버 상태를 명확히 구분하기 위해 만들어졌다.\n- react-query에서는 기존 상태 관리 라이브러리인 `redux`, `mobX`가 `클라이언트 상태 작업`에 적합하지만, `비동기 또는 서버 상태 작업`에는 그다지 좋지 않다고 언급한다.\n- 클라이언트 상태(Client State)와 서버 상태(Server State)는 완전히 다른 개념이며, 클라이언트 상태는 각각의 input 값으로 예를 들 수 있고, 서버 상태는 데이터베이스에 저장되어 있는 데이터로 예를 들 수 있다.\n\n\u003cbr /\u003e\n\n### 기능\n\n- 캐싱\n- 동일한 데이터에 대한 중복 요청을 단일 요청으로 통합\n- 백그라운드에서 오래된 데이터 업데이트\n- 데이터가 얼마나 오래되었는지 알 수 있다.\n- 데이터 업데이트를 가능한 빠르게 반영\n- 페이지네이션 및 데이터 지연 로드와 같은 성능 최적화\n- 서버 상태의 메모리 및 가비지 수집 관리\n- 구조 공유를 사용하여 쿼리 결과를 메모화\n\n\u003cbr /\u003e\n\n## React Query 기본 설정\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [QueryClient 공식 문서](https://tanstack.com/query/v5/docs/reference/QueryClient)\n- [QueryClientProvider 공식 문서](https://tanstack.com/query/v5/docs/react/reference/QueryClientProvider)\n\n```tsx\nimport { QueryClient } from \"@tanstack/react-query\";\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: Infinity,\n      // ...\n    },\n  },\n});\n```\n\n- QueryClient를 사용하여 `캐시`와 상호 작용할 수 있다.\n- QueryClient에서 모든 `query` 또는 `mutation`에 기본 옵션을 추가할 수 있으며, 종류가 상당하므로 공식 문서를 참고해 보자.\n\n```tsx\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\n\nconst queryClient = new QueryClient({ /* options */});\n\nfunction App() {\n  return (\n   \u003cQueryClientProvider client={queryClient}\u003e\n      \u003cdiv\u003e블라블라\u003c/div\u003e\n   \u003c/QueryClientProvider\u003e;\n  );\n}\n```\n\n- react-query를 사용하기 위해서는 `QueryClientProvider`를 최상단에서 감싸주고 `QueryClient` 인스턴스를 client props로 넣어 애플리케이션에 연결해야 한다.\n- 위 예시에서 App.js에 QueryClientProvider로 컴포넌트를 감싸고, client props에다 queryClient를 연결함으로써, 이 context는 앱에서 비동기 요청을 알아서 처리하는 `background` 계층이 된다.\n\n\u003cbr /\u003e\n\n## Devtools\n\n![스크린샷 2022-04-07 오후 11 53 32](https://user-images.githubusercontent.com/64779472/162228222-d1c7dd3e-ce62-484d-bfa0-8493f3e68cae.png)\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [React Query Devtools 공식 문서](https://tanstack.com/query/v5/docs/react/devtools)\n- react-query는 `전용 devtools`를 제공하며 별도의 패키지 설치가 필요하다.\n- devtools를 사용하면 React Query의 모든 내부 동작을 `시각화`하는 데 도움이 되며 문제가 발생하면 `디버깅 시간을 절약`할 수 있다.\n- devtools는 기본값으로 `process.env.NODE_ENV === \"development\"` 인 경우에만 실행된다, 즉 일반적으로 개발 환경에서만 작동하도록 설정되어 있으므로, 프로젝트 배포 시에 Devtools 삽입 코드를 제거해 줄 필요가 없다.\n- Next 13+의 App Dir에선 dev dependency로 설치해야 동작한다.\n\n```bash\n$ npm i @tanstack/react-query-devtools\n# or\n$ pnpm add @tanstack/react-query-devtools\n# or\n$ yarn add @tanstack/react-query-devtools\n# or\n$ bun add @tanstack/react-query-devtools\n```\n\n```tsx\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\n\nfunction App() {\n  return (\n    \u003cQueryClientProvider client={queryClient}\u003e\n      {/* The rest of your application */}\n      \u003cReactQueryDevtools initialIsOpen={false} /\u003e\n    \u003c/QueryClientProvider\u003e\n  );\n}\n```\n\n### options\n\n- initialIsOpen (Boolean)\n  - `true`이면 개발 도구가 기본적으로 열려 있도록 설정할 수 있다.\n- buttonPosition?: (\"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\" | \"relative\")\n  - 기본값: `bottom-right`\n  - devtools 패널을 여닫기 위한 로고 위치\n  - `relative`일 때 버튼은 devtools를 렌더링하는 위치에 배치된다.\n- 일반적으로 initialIsOpen, buttonPosition을 자주 사용하며 그 외에 position, client와 같은 옵션들도 존재한다.\n\n\u003cbr /\u003e\n\n## 캐싱 라이프 사이클\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [React Query 캐시 라이프 사이클 공식 문서](https://tanstack.com/query/v5/docs/react/guides/caching)\n\n```\n* Query Instances with and without cache data(캐시 데이터가 있거나 없는 쿼리 인스턴스)\n* Background Refetching(백그라운드 리패칭)\n* Inactive Queries(비활성 쿼리)\n* Garbage Collection(가비지 컬렉션)\n```\n\n- `gcTime`의 기본값 5분, `staleTime` 기본값 0초를 가정\n\n1. `A`라는 queryKey를 가진 A 쿼리 인스턴스가 `mount`됨\n2. 네트워크에서 데이터 fetch하고, 불러온 데이터는 A라는 queryKey로 `캐싱`함\n3. 이 데이터는 `fresh`상태에서 `staleTime(기본값 0)` 이후 `stale` 상태로 변경됨\n4. A 쿼리 인스턴스가 `unmount`됨\n5. 캐시는 `gcTime(기본값 5min)` 만큼 유지되다가 `가비지 콜렉터(GC)`로 수집됨\n6. 만일, gcTime 지나기 전이고, A 쿼리 인스턴스 fresh한 상태라면 새롭게 mount되면 캐시 데이터를 보여준다.\n\n\u003cbr /\u003e\n\n## useQuery\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n### useQuery 기본 문법\n\n- [useQuery 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQuery)\n- useQuery는 `v5`부터 인자로 단 하나의 `객체`만 받는다. 그중에 첫 번째 인자가 `queryKey`, `queryFn`가 필수 값이다.\n\n```tsx\nconst result = useQuery({\n  queryKey, // required\n  queryFn, // required\n  // ...options ex) gcTime, staleTime, select, ...\n});\n\nresult.data;\nresult.isLoading;\nresult.refetch;\n// ...\n```\n\n```tsx\n// 실제 예제\n// 💡 queryFn의 반환 타입을 지정해주면 useQuery의 타입 추론이 원활합니다.\nconst getAllSuperHero = async (): Promise\u003cAxiosResponse\u003cHero[]\u003e\u003e =\u003e {\n  return await axios.get(\"http://localhost:4000/superheroes\");\n};\n\nconst { data, isLoading } = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n});\n```\n\n\u003cbr \u003e\n\n**1. queryKey**\n\n```tsx\n// (1) queryKey는 데이터를 고유하게 식별에 더해 쿼리 함수에 아래와 같이 편리하게 전달할 수도 있다.\nconst getSuperHero = async ({\n  queryKey,\n}: {\n  queryKey: [\"super-hero\", number];\n}): Promise\u003cAxiosResponse\u003cHero\u003e\u003e =\u003e {\n  const heroId = queryKey[1]; // ex) queryKey: [\"super-hero\", \"3\"]\n\n  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);\n};\n\nconst useSuperHeroData = (heroId: string) =\u003e {\n  return useQuery({\n    queryKey: [\"super-hero\", heroId],\n    queryFn: getSuperHero, // (*)\n  });\n};\n```\n\n- useQuery의 queryKey는 `배열`로 지정해 줘야 한다.\n  - 이는 단일 문자열만 포함된 배열이 될 수도 있고, 여러 문자열과 중첩된 객체로 구성된 복잡한 형태일 수도 있다.\n\n```tsx\n// An individual todo\nuseQuery({ queryKey: [\"todo\", 5], ... })\n\n// An individual todo in a \"preview\" format\nuseQuery({ queryKey: [\"todo\", 5, { preview: true }], ...})\n```\n\n- useQuery는 `queryKey`를 기반으로 `쿼리 캐싱`을 관리하는 것이 핵심이다.\n  - 만약, 쿼리가 특정 변수에 `의존`한다면 배열에다 이어서 줘야 한다. `ex: [\"super-hero\", heroId, ...]`\n  - **이는 사실 굉장히 중요하다.** 예를 들어, `queryClient.setQueryData` 등과 같이 특정 쿼리에 접근이 필요 할 때 `초기에 설정해둔 포맷`을 지켜줘야 제대로 쿼리에 접근할 수 있다.\n  - 아래 options 예제를 살펴보면 useSuperHeroData의 queryKey는 `[\"super-hero\", heroId]`이다. 그렇다면 queryClient.setQueryData를 이용할 때 똑같이 `[\"super-hero\", heroId]` 포맷을 가져야 한다. 그렇지 않으면 원하는 쿼리에 접근할 수 없다.\n\n\u003cbr /\u003e\n\n**2. queryFn**\n\n- useQuery의 queryFn는 `Promise`를 반환하는 함수를 넣어야 한다.\n\n```tsx\n// (2) 상단의 queryKey 예제와 반대로 queryFn 자체적으로 인자를 받는 형태\nconst getSuperHero = async (heroId: string): Promise\u003cAxiosResponse\u003cHero\u003e\u003e =\u003e {\n  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);\n};\n\nconst useSuperHeroData = (heroId: string) =\u003e {\n  return useQuery({\n    queryKey: [\"super-hero\", heroId],\n    queryFn: () =\u003e getSuperHero(heroId), // (*)\n  });\n};\n```\n\n\u003cbr /\u003e\n\n**3. options**\n\n- [useQuery 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQuery)\n- useQuery의 `options`에 많이 쓰이는 옵션들은 차근차근 살펴볼 예정이다. 문서 외에 더욱 자세히 알고 싶다면 위 공식 문서를 참고하자.\n\n\u003cbr /\u003e\n\n```tsx\nconst useSuperHeroData = (heroId: string) =\u003e {\n  return useQuery({\n    queryKey: [\"super-hero\", heroId],\n    queryFn: () =\u003e getSuperHero(heroId),\n    gcTime: 5 * 60 * 1000, // 5분\n    staleTime: 1 * 60 * 1000, // 1분\n    retry: 1,\n    // ... options\n  });\n};\n```\n\n\u003cbr /\u003e\n\n### useQuery 주요 리턴 데이터\n\n- [useQuery 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQuery)\n\n```tsx\nconst {\n  data,\n  error,\n  status,\n  isPending,\n  fetchStatus,\n  isLoading,\n  isFetching,\n  isError,\n  refetch,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n});\n```\n\n- **data**: 쿼리 함수가 리턴한 `Promise`에서 `resolved`된 데이터\n- **error**: 쿼리 함수에 오류가 발생한 경우, 쿼리에 대한 오류 객체\n- **status**: `data`, 쿼리 결과값에 대한 상태를 표현하는 status는 문자열 형태로 `3가지`의 값이 존재한다.\n  - pending: 쿼리 데이터가 없고, 쿼리 시도가 아직 완료되지 않은 상태.\n    - `{ enabled: false }` 상태로 쿼리가 호출되면 이 상태로 시작된다.\n    - [Dependent Queries 공식 문서](https://tanstack.com/query/v5/docs/react/guides/dependent-queries)\n  - error: 에러 발생했을 때 상태\n  - success: 쿼리 함수가 오류 없이 요청 성공하고 데이터를 표시할 준비가 된 상태.\n- **isPending**: 쿼리가 아직 수행되지 않았고, 캐싱된 데이터가 없을 때 `true`를 반환한다.\n  - status(pending)에 파생된 boolean 값이다.\n- **fetchStatus**: `queryFn`에 대한 정보를 나타냄\n  - fetching: 쿼리가 현재 실행 중인 상태\n  - paused: 쿼리를 요청했지만, 잠시 중단된 상태 (network mode와 연관)\n  - idle: 쿼리가 현재 아무 작업도 수행하지 않는 상태\n- **isLoading**: `캐싱 된 데이터가 없을 때` 즉, 처음 실행된 쿼리일 때 로딩 여부에 따라 `true/false`로 반환된다.\n  - 이는 캐싱 된 데이터가 있다면 로딩 여부에 상관없이 `false`를 반환한다.\n  - status(pending)와 fetchStatus(fetching) 결합된 boolean이다. 즉, `isFetching \u0026\u0026 isPending` 와 동일하다.\n- **isFetching**: 캐싱된 데이터가 있더라도 서버에 요청을 보내면 `true`를 반환한다. \n  - fetchStatus(fetching)에 파생된 boolean 값이다.\n- **isSuccess**: 쿼리 요청이 성공하면 `true`\n- **isError**: 쿼리 요청 중에 에러가 발생한 경우 `true`\n- **refetch**: 쿼리를 수동으로 다시 가져오는 함수.\n- **그 외 반환 데이터들을 자세히 알고 싶으면 useQuery 공식 문서 참고**\n\n\u003cbr /\u003e\n\n### 💡 status, fetchStatus 나눠서 다루는 걸까?\n\n- [Why Two Different States 공식 문서](https://tanstack.com/query/v5/docs/react/guides/queries#why-two-different-states)\n- fetchStatus는 HTTP 네트워크 연결 상태와 좀 더 관련된 상태 데이터이다.\n  - 예를 들어, status가 `success` 상태라면 주로 fetchStatus는 `idle` 상태지만, 백그라운드에서 re-fetch가 발생할 때 `fetching` 상태일 수 있다.\n  - status가 보통 `loading` 상태일 때 fetchStatus는 주로 `fetching`를 갖지만, 네트워크 연결이 되어 있지 않은 경우 `paused` 상태를 가질 수 있다.\n- 정리하자면 아래와 같다.\n  - status는 `data`가 있는지 없는지에 대한 상태를 의미한다.\n  - fetchStatus는 쿼리 즉, `queryFn 요청`이 진행 중인지 아닌지에 대한 상태를 의미한다.\n\n\u003cbr /\u003e\n\n### 💡 isPending, isLoading 주요 차이점\n- isPending은 status(pending) 필드에서 직접 파생된 상태인 반면, isLoading은 status(pending)와 fetchStatus(fetching)의 결합된 상태입니다.\n- 쉽게 접근하자면 isPending은 \"아직 데이터가 없습니다\" 를 의미합니다. 그에 반해 isLoading은 \"아직 데이터가 없고, 데이터를 가져오는 중입니다\"를 의미합니다.\n- 이러한 차이는 enabled 옵션이 false일 때 예시로 들면 이해하가 쉽습니다. \n  - enabled가 false일 때, isPending은 true로 설정되지만, isLoading은 false로 설정됩니다.\n\n```ts\nuseQuery({ queryKey, queryFn, enabled: false });\n// isPending: true, isLoading: false\n```\n\n- [React Query v5부터는 isLoading 대신 isPending 사용을 권장합니다.](https://github.com/TanStack/query/discussions/6297#discussioncomment-7467010)\n  - 로더 표시 여부는 사용 사례마다 다르지만, 대부분의 경우 isPending만으로 충분합니다. isLoading의 경우 이론적으로 \"보류중이지만, 로딩되지 않은 경우(ex. enabled: false)\"를 항상 검증해야 합니다.\n\n\u003cbr /\u003e\n\n## useQuery 주요 옵션\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [useQuery 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQuery)\n\n\u003cbr /\u003e\n\n### staleTime과 gcTime\n\n- stale은 용어 뜻대로 `썩은`이라는 의미이다. 즉, 최신 상태가 아니라는 의미이다.\n- fresh는 뜻 그대로 `신선한`이라는 의미이다. 즉, 최신 상태라는 의미이다.\n\n```tsx\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  gcTime: 5 * 60 * 1000, // 5분\n  staleTime: 1 * 60 * 1000, // 1분\n});\n```\n\n\u003cbr /\u003e\n\n1. staleTime: `(number | Infinity)`\n   - staleTime은 데이터가 `fresh에서 stale` 상태로 변경되는 데 걸리는 시간, 만약 staleTime이 `3000`이면 fresh 상태에서 `3초` 뒤에 stale로 변환\n   - `fresh` 상태일 때는 쿼리 인스턴스가 새롭게 mount 되어도 네트워크 요청(fetch)이 일어나지 않는다.\n   - 참고로, staleTime의 기본값은 `0`이기 때문에 일반적으로 fetch 후에 바로 stale이 된다.\n2. gcTime: `(number | Infinity)`\n   - 데이터가 사용하지 않거나, `inactive` 상태일 때 `캐싱 된 상태로` 남아있는 시간(밀리초)이다.\n   - 쿼리 인스턴스가 unmount 되면 데이터는 `inactive 상태로 변경`되며, 캐시는 `gcTime`만큼 유지된다.\n   - gcTime이 지나면 `가비지 콜렉터`로 수집된다.\n   - gcTime이 지나기 전에 쿼리 인스턴스가 다시 mount 되면, 데이터를 fetch 하는 동안 캐시 데이터를 보여준다.\n   - gcTime은 staleTime과 관계없이, 무조건 `inactive` 된 시점을 기준으로 캐시 데이터 삭제를 결정한다.\n   - gcTime의 기본값은 `5분`이다. SSR 환경에서는 `Infinity`이다.\n\n- 여기서 주의할 점은 staleTime과 gcTime의 기본값은 각각 `0분`과 `5분`이다. 따라서 staleTime에 어떠한 설정도 하지 않으면 해당 쿼리를 사용하는 컴포넌트(Observer)가 mount 됐을 때 매번 다시 API를 요청할 것이다.\n- staleTime을 gcTime보다 길게 설정했다고 가정하면, staleTime만큼의 캐싱을 기대했을 때 원하는 결과를 얻지 못할 것이다. 즉, 두 개의 옵션을 적절하게 설정해 줘야 한다.\n  - 참고로, [TkDodo의 reply](https://github.com/TanStack/query/discussions/1685#discussioncomment-1876723)에 따르면 TkDodo는 `staleTime을 gcTime보다 작게 설정하는 것이 좋다.`는 의견에 동의하지 않는다고 한다.\n  - 예컨대, staleTime이 60분일지라도 유저가 자주 사용하지 않는 데이터라면 굳이 gcTime을 60분 이상으로 설정하여 메모리를 낭비할 필요가 없다.\n\n\u003cbr /\u003e\n\n### refetchOnMount\n\n```tsx\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  refetchOnMount: true,\n});\n```\n\n- refetchOnMount: `boolean | \"always\" | ((query: Query) =\u003e boolean | \"always\")`\n- refetchOnMount는 데이터가 `stale` 상태일 경우, mount마다 `refetch`를 실행하는 옵션이다.\n- refetchOnMount의 기본값은 `true`이다.\n- `always`로 설정하면 마운트 시마다 매번 refetch를 실행한다.\n- `false`로 설정하면 최초 fetch 이후에는 refetch 하지 않는다.\n\n\u003cbr /\u003e\n\n### refetchOnWindowFocus\n\n```tsx\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  refetchOnWindowFocus: true,\n});\n```\n\n- refetchOnWindowFocus: `boolean | \"always\" | ((query: Query) =\u003e boolean | \"always\")`\n- refetchOnWindowFocus는 데이터가 `stale` 상태일 경우 `윈도우 포커싱` 될 때마다 refetch를 실행하는 옵션이다.\n- refetchOnWindowFocus의 기본값은 `true`이다.\n- 예를 들어, 크롬에서 다른 탭을 눌렀다가 다시 원래 보던 중인 탭을 눌렀을 때도 이 경우에 해당한다. 심지어 F12로 개발자 도구 창을 켜서 네트워크 탭이든, 콘솔 탭이든 개발자 도구 창에서 놀다가 페이지 내부를 다시 클릭했을 때도 이 경우에 해당한다.\n- `always`로 설정하면 항상 윈도우 포커싱 될 때마다 refetch를 실행한다는 의미이다.\n\n\u003cbr /\u003e\n\n### Polling\n\n```tsx\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  refetchInterval: 2000,\n  refetchIntervalInBackground: true,\n});\n```\n\n```\nPolling(폴링)이란?\n실시간 웹을 위한 기법으로 \"일정한 주기(특정한 시간)\"를 가지고 서버와 응답을 주고받는 방식이 폴링 방식이다.\nreact-query에서는 \"refetchInterval\", \"refetchIntervalInBackground\"을 이용해서 구현할 수 있다.\n```\n\n1. refetchInterval: `number | false | ((data: TData | undefined, query: Query) =\u003e number | false)`\n\n- refetchInterval은 `시간(ms)`를 값으로 넣어주면 일정 시간마다 자동으로 refetch를 시켜준다.\n\n2. refetchIntervalInBackground: `boolean`\n\n- refetchIntervalInBackground는 `refetchInterval`과 함께 사용하는 옵션이다.\n- 탭/창이 백그라운드에 있는 동안 refetch 시켜준다. 즉, 브라우저에 focus 되어 있지 않아도 refetch를 시켜주는 것을 의미한다.\n\n\u003cbr /\u003e\n\n### enabled refetch\n\n```tsx\nconst {\n  data,\n  refetch,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  enabled: false,\n});\n\nconst handleClickRefetch = useCallback(() =\u003e {\n  refetch();\n}, [refetch]);\n\nreturn (\n  \u003cdiv\u003e\n    {data?.data.map((hero: Data) =\u003e (\n      \u003cdiv key={hero.id}\u003e{hero.name}\u003c/div\u003e\n    ))}\n    \u003cbutton onClick={handleClickRefetch}\u003eFetch Heroes\u003c/button\u003e\n  \u003c/div\u003e\n);\n```\n\n- enabled: `boolean`\n- enabled는 `쿼리가 자동으로 실행되지 않도록 할 때 설정`할 수 있다.\n- enabled를 `false`를 주면 쿼리가 자동 실행되지 않는다.\n  - useQuery 반환 값 중 status가 `pending` 상태로 시작한다.\n- refetch는 쿼리를 `수동`으로 다시 요청하는 기능이다. 쿼리 오류가 발생하면 오류만 기록된다.\n  - 오류를 발생시키려면 `throwOnError` 속성을 `true`로 해서 전달해야 한다.\n- 보통 자동으로 쿼리 요청을 하지 않고 `버튼 클릭`이나 특정 이벤트를 통해 요청을 시도할 때 같이 사용한다.\n- 💡 주의할 점은, `enabled: false`를 줬다면 `queryClient`가 쿼리를 다시 가져오는 방법 중 `invalidateQueries`와 `refetchQueries`를 무시한다.\n\n\u003cbr /\u003e\n\n### retry\n\n```tsx\nconst {\n  data,\n  refetch,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  retry: 10, // 오류를 표시하기 전에 실패한 요청을 10번 재시도합니다.\n});\n```\n\n- retry: `(boolean | number | (failureCount: number, error: TError) =\u003e boolean)`\n- retry는 쿼리가 `실패`하면 useQuery를 `특정 횟수`만큼 재요청하는 옵션이다.\n- retry가 `false`인 경우, 실패한 쿼리는 기본적으로 다시 시도하지 않는다. `true`인 경우에는 실패한 쿼리에 대해서 무한 재요청을 시도한다.\n- 값으로 `숫자`를 넣을 경우, 실패한 쿼리가 해당 숫자를 충족할 때까지 요청을 재시도한다.\n- 기본값은 클라이언트 환경에서는 `3`, 서버 환경에서는 `0`이다.\n\n\u003cbr /\u003e\n\n### onSuccess, onError, onSettled\n\n- _NOTE_: `v4`까지 있던 onSuccess, onError, onSettled Callback은 `useQuery` 옵션에서 [@Deprecated](https://github.com/TanStack/query/pull/5353) 됐다. 단, `useMutation`에서는 사용 가능하다.\n- [Breaking React Query's API on purpose](https://velog.io/@cnsrn1874/breaking-react-querys-api-on-purpose) 참고\n\n\u003cbr /\u003e\n\n### select\n\n```tsx\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  select: (data) =\u003e {\n    const superHeroNames = data.data.map((hero) =\u003e hero.name);\n    return superHeroNames;\n  },\n});\n\nreturn (\n  \u003cdiv\u003e\n    {data.map((heroName, idx) =\u003e (\n      \u003cdiv key={`${heroName}-${idx}`}\u003e{heroName}\u003c/div\u003e\n    ))}\n  \u003c/div\u003e\n);\n```\n\n- select: `(data: TData) =\u003e unknown`\n- select 옵션을 사용하여 쿼리 함수에서 `반환된 데이터의 일부를 변환하거나 선택할 수 있다.`\n- 참고로, 반환된 데이터 값에는 영향을 주지만 쿼리 캐시에 저장되는 내용에는 영향을 주지 않는다.\n\n\u003cbr /\u003e\n\n### placeholderData\n\n```tsx\nconst placeholderData = useMemo(() =\u003e generateFakeHeroes(), []);\n\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  placeholderData: placeholderData,\n});\n```\n\n- placeholderData: `TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) =\u003e TData`\n- placeholderData를 설정하면 쿼리가 `pending` 상태인 동안 특정 쿼리에 대한 placeholder data로 사용된다.\n- placeholderData는 캐시에 유지되지 않으며, 서버 데이터와 관계없는 보여주기용 가짜 데이터다.\n- placeholderData에 함수를 제공하는 경우 첫 번째 인자로 이전에 관찰된 쿼리 데이터를 수신하고, 두 번째 인자는 이전 쿼리 인스턴스가 된다.\n\n\u003cbr /\u003e\n\n### keepPreviousData\n\n- v4까지 있던 keepPreviousData은 `페이지네이션`과 같은 기능을 구현할 때 많이 사용하던 옵션이었다. 캐싱 되지 않은 페이지를 가져올 때 목록이 `깜빡거리는 현상을 방지`할 수 있다.\n- **하지만, v5부터 `keepPreviousData`, `isPreviousData`은 옵션은 제거됐다.**\n\n  - [Removed keepPreviousData in favor of placeholderData identity function](https://github.com/ssi02014/react-query-tutorial/blob/main/document/v5.md#9-%EF%B8%8F-removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function)\n\n- 이들은 각각 `placeholderData`와 `isPlaceholderData` 플래그와 거의 유사하게 동작하기 때문이다.\n- 아래 예제처럼 `placeholderData`를 활용하면서 이전 버전에서 `keepPreviousData의 값을 \"true\"`로 줬을 때와 동일한 기능을 수행할 수 있다.\n\n```tsx\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  placeholderData: keepPreviousData,\n});\n```\n\n- 아래 예시처럼 작성해서 위의 `keepPreviousData` 예시와 동일한 동작을 할 수 있다.\n\n```tsx\nimport { useQuery } from \"@tanstack/react-query\";\n\nconst {\n  data,\n  // ...\n} = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  placeholderData: (previousData, previousQuery) =\u003e previousData,\n});\n```\n\n\u003cbr /\u003e\n\n### notifyOnChangeProps\n\n```tsx\nimport { useQuery } from \"@tanstack/react-query\";\n\nconst { data, dataUpdatedAt } = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n  notifyOnChangeProps: [\"data\"], // data 값 변경시에만 리렌더링이 발생한다\n});\n```\n\n- notifyOnChangeProps: `string[] | \"all\" | (() =\u003e string[] | \"all\")`\n- 쿼리의 특정 프로퍼티들이 변경되었을 때만 리렌더링이 발생하도록 설정할 수 있다.\n- 별도로 설정하지 않으면, **컴포넌트에서 접근한 값이 변경되었을 때** 리렌더링이 발생한다 (기본 동작). 즉, 위 예시에서 `notifyOnChangeProps`에 설정값을 주지 않았다면, `data`, `dataUpdatedAt` 중 어느 하나가 변경되면 리렌더링이 발생한다.\n- `\"all\"`로 설정할 경우, 쿼리의 어떤 프로퍼티가 변경되든 컴포넌트가 리렌더링된다.\n- 참고: 기본 동작은 [Object.defineProperty()](https://github.com/TanStack/query/pull/1578/files#diff-93f379800fc8abf895eba249b2e2371eda98740aa40fc9f284a8088d190f46c3R506-R514)를 활용한다.\n\n\u003cbr /\u003e\n\n## Parallel\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [useQueries 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQueries)\n\n```tsx\nconst { data: superHeroes } = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n});\n\nconst { data: friends } = useQuery({\n  queryKey: [\"friends\"],\n  queryFn: getFriends,\n});\n```\n\n- 몇 가지 상황을 제외하면 쿼리 여러 개가 선언된 일반적인 상황일 때, 쿼리 함수들은 `그냥 병렬로 요청돼서 처리`된다.\n- 이러한 특징은 쿼리 처리의 `동시성`을 극대화시킨다.\n\n```tsx\nconst queryResults = useQueries({\n  queries: [\n    {\n      queryKey: [\"super-hero\", 1],\n      queryFn: () =\u003e getSuperHero(1),\n      staleTime: Infinity, // 다음과 같이 option 추가 가능!\n    },\n    {\n      queryKey: [\"super-hero\", 2],\n      queryFn: () =\u003e getSuperHero(2),\n      staleTime: 0,\n    },\n    // ...\n  ],\n});\n```\n\n- 하지만, 쿼리 여러 개를 동시에 수행해야 하는데, 렌더링이 거듭되는 사이사이에 계속 쿼리가 수행되어야 한다면 쿼리를 수행하는 로직이 hook 규칙에 어긋날 수도 있다. 이럴 때는 `useQueries`를 사용한다.\n- useQueries 훅은 모든 쿼리 결과가 포함된 배열을 반환한다. 반환되는 순서는 쿼리가 입력된 순서와 동일하다.\n\n### Queries Combine\n\n- [useQueries Combine 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQueries#combine)\n\n- useQueries 훅이 반환한 모든 쿼리 결과가 포함된 배열을 단일 값으로 결합하려면 combine 옵션을 사용할 수 있다.\n\n```tsx\nconst ids = [1,2,3]\nconst combinedQueries = useQueries({\n  queries: ids.map(id =\u003e (\n    { queryKey: [\"post\", id], queryFn: () =\u003e fetchPost(id) },\n  )),\n  combine: (results) =\u003e {\n    return ({\n      data: results.map(result =\u003e result.data),\n      pending: results.some(result =\u003e result.isPending),\n    })\n  }\n})\n```\n\n- combinedQueries는 `data`와 `pending` 프로퍼티를 갖는다.\n- _Note_: 참고로 결합하면 쿼리 결과의 나머지 다른 프로퍼티들은 손실된다.\n\n\u003cbr /\u003e\n\n## Dependent Queries\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [Dependent Queries 공식 문서](https://tanstack.com/query/v5/docs/react/guides/dependent-queries)\n- `종속 쿼리`는 어떤 A라는 쿼리가 있는데 이 A 쿼리를 실행하기 전에 사전에 완료되어야 하는 B 쿼리가 있는데, 이러한 B 쿼리에 의존하는 A 쿼리를 종속 쿼리라고 한다.\n- react-query에서는 `enabled` 옵션을 통해 종속 쿼리를 쉽게 구현할 수 있다.\n\n```tsx\n// 사전에 완료되어야 할 쿼리\nconst { data: user } = useQuery({\n  queryKey: [\"user\", email],\n  queryFn: () =\u003e getUserByEmail(email),\n});\n\nconst channelId = user?.data.channelId;\n\n// user 쿼리에 종속 쿼리\nconst { data: courses } = useQuery({\n  queryKey: [\"courses\", channelId],\n  queryFn: () =\u003e getCoursesByChannelId(channelId),\n  enabled: !!channelId,\n});\n```\n\n\u003cbr /\u003e\n\n## useQueryClient\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- useQueryClient는 `QueryClient` 인스턴스를 반환한다.\n- `QueryClient`는 캐시와 상호작용한다.\n- QueryClient는 다음 문서에서 자세하게 다룬다.\n  - [QueryClient](https://github.com/ssi02014/react-query-tutorial/tree/master/document/queryClient.md)\n\n```tsx\nimport { useQueryClient } from \"@tanstack/react-query\";\n\nconst queryClient = useQueryClient();\n```\n\n\u003cbr /\u003e\n\n## Initial Query Data\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [Initial Query Data 공식 문서](https://tanstack.com/query/v5/docs/react/guides/initial-query-data)\n- 쿼리에 대한 `초기 데이터`가 필요하기 전에 캐시에 제공하는 방법이 있다.\n- initialData 옵션을 통해서 쿼리를 미리 채우는 데 사용할 수 있으며, 초기 로드 상태도 건너뛸 수도 있다.\n\n```tsx\nconst useSuperHeroData = (heroId: string) =\u003e {\n  const queryClient = useQueryClient();\n\n  return useQuery({\n    queryKey: [\"super-hero\", heroId],\n    queryFn: () =\u003e getSuperHero(heroId),\n    initialData: () =\u003e {\n      const queryData = queryClient.getQueryData([\"super-heroes\"]) as any;\n      const hero = queryData?.data?.find(\n        (hero: Hero) =\u003e hero.id === parseInt(heroId)\n      );\n\n      if (hero) return { data: hero };\n    },\n  });\n};\n```\n\n\u003cbr /\u003e\n\n- 참고로 위 예제에서 `queryClient.getQueryData` 메서드는 기존 쿼리의 `캐싱 된 데이터`를 가져오는 데 사용할 수 있는 동기 함수이다. 쿼리가 존재하지 않으면 `undefined`를 반환한다.\n\n\u003cbr /\u003e\n\n## Prefetching\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [prefetching 공식 문서](https://tanstack.com/query/v5/docs/react/guides/prefetching)\n- prefetch는 말 그대로 미리 fetch해오겠다는 의미이다.\n- 비동기 요청은 데이터양이 클수록 받아오는 속도가 느리고, 시간이 오래 걸린다. 사용자 경험을 위해 데이터를 미리 받아와서 캐싱해 놓으면? 새로운 데이터를 받기 전에 사용자가 캐싱 된 데이터를 볼 수 있어 `UX에 좋은 영향`을 줄 수 있다.\n  - 예를 들어 페이지네이션을 구현했다고 가정하면, 페이지1에서 페이지2로 이동했을 때 페이지3의 데이터를 미리 받아놓는 것이다!\n- react query에서는 `queryClient.prefetchQuery`을 통해서 prefetch 기능을 제공한다.\n\n### prefetchQuery\n\n```tsx\nconst prefetchNextPosts = async (nextPage: number) =\u003e {\n  const queryClient = useQueryClient();\n  // 해당 쿼리의 결과는 일반 쿼리들처럼 캐싱 된다.\n  await queryClient.prefetchQuery({\n    queryKey: [\"posts\", nextPage],\n    queryFn: () =\u003e fetchPosts(nextPage),\n    // ...options\n  });\n};\n\n// 단순 예\nuseEffect(() =\u003e {\n  const nextPage = currentPage + 1;\n\n  if (nextPage \u003c maxPage) {\n    prefetchNextPosts(nextPage);\n  }\n}, [currentPage]);\n```\n\n- 참고로 prefetchQuery를 통해 가져오는 쿼리에 대한 데이터가 `이미 캐싱 되어 있으면 데이터를 가져오지 않는다.`\n\n\u003cbr /\u003e\n\n### prefetchInfiniteQuery\n\n- infinite 쿼리는 바로 아래에 나오겠지만 일반 쿼리들처럼 infinite 쿼리도 prefetch 할 수 있다.\n- 기본적으로 쿼리의 `첫 번째 페이지만 prefetch` 되며, 그 이상을 prefetch 하려면 `pages 옵션`을 활용해야 한다.\n  - 이 경우에는 `getNextPageParam` 함수를 무조건 제공해 줘야 한다는 점을 주의하자.\n\n```tsx\nconst prefetchTodos = async () =\u003e {\n  await queryClient.prefetchInfiniteQuery({\n    queryKey: [\"projects\"],\n    queryFn: fetchProjects,\n    initialPageParam: 0,\n    getNextPageParam: (lastPage, pages) =\u003e lastPage.nextCursor,\n    pages: 3, // prefetch the first 3 pages\n  });\n};\n```\n\n\u003cbr /\u003e\n\n## Infinite Queries\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [Infinite Queries 공식 문서](https://tanstack.com/query/v5/docs/react/guides/infinite-queries)\n- [useInfiniteQuery 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useInfiniteQuery)\n- Infinite Queries(무한 쿼리)는 `무한 스크롤`이나 `load more(더 보기)`과 같이 특정 조건에서 데이터를 추가적으로 받아오는 기능을 구현할 때 사용하면 유용하다.\n- react-query는 이러한 무한 쿼리를 지원하기 위해 useQuery의 유용한 버전인 `useInfiniteQuery`을 지원한다.\n\n```tsx\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\n\n// useInfiniteQuery의 queryFn의 매개변수는 `pageParam`이라는 프로퍼티를 가질 수 있다.\nconst fetchColors = async ({\n  pageParam,\n}: {\n  pageParam: number;\n}): Promise\u003cAxiosResponse\u003cPaginationColors\u003e\u003e =\u003e {\n  return await axios.get(`http://localhost:4000/colors?page=${pageParam}`);\n};\n\nconst InfiniteQueries = () =\u003e {\n  const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =\n    useInfiniteQuery({\n      queryKey: [\"colors\"],\n      queryFn: fetchColors,\n      initialPageParam: 1,\n      getNextPageParam: (lastPage, allPages) =\u003e {\n        return allPages.length \u003c 4 \u0026\u0026 allPages.length + 1;\n      },\n      // ...\n    });\n\n  return (\n    \u003cdiv\u003e\n      {data?.pages.map((group, idx) =\u003e ({\n        /* ... */\n      }))}\n      \u003cdiv\u003e\n        \u003cbutton disabled={!hasNextPage} onClick={() =\u003e fetchNextPage()}\u003e\n          LoadMore\n        \u003c/button\u003e\n      \u003c/div\u003e\n      \u003cdiv\u003e{isFetching \u0026\u0026 !isFetchingNextPage ? \"Fetching...\" : null}\u003c/div\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n### 주요 반환\n\n- `useInfiniteQuery`는 기본적으로 useQuery와 사용법은 비슷하지만, 차이점이 있다.\n- useInfiniteQuery는 반환 값으로 `isFetchingNextPage`, `isFetchingPreviousPage`, `fetchNextPage`, `fetchPreviousPage`, `hasNextPage` 등이 추가적으로 있다.\n  - data.pages: 모든 페이지 데이터를 포함하는 배열이다.\n  - data.pageParams: 모든 페이지 매개변수를 포함하는 배열이다.\n  - fetchNextPage: `다음 페이지`를 fetch 할 수 있다.\n  - fetchPreviousPage: `이전 페이지`를 fetch 할 수 있다.\n  - isFetchingNextPage: `fetchNextPage` 메서드가 다음 페이지를 가져오는 동안 true이다.\n  - isFetchingPreviousPage: `fetchPreviousPage` 메서드가 이전 페이지를 가져오는 동안 true이다.\n  - hasNextPage: 가져올 수 있는 `다음 페이지`가 있을 경우 true이다.\n  - hasPreviousPage: 가져올 수 있는 `이전 페이지`가 있을 경우 true이다.\n\n\u003cbr /\u003e\n\n### 주요 옵션\n\n1. initialPageParam: `TPageParam`\n\n- initialPageParam을 이용해서 첫 페이지를 가져올 때 사용할 기본 페이지 매개변수이다. `필수값`이다.\n\n2. getNextPageParam: `(lastPage, allPages, lastPageParam, allPageParams) =\u003e TPageParam | undefined | null`\n\n- getNextPageParam 을 이용해서 페이지를 증가시킬 수 있다. `필수값`이다.\n  - getNextPageParam의 첫 번째 인자 `lastPage`는 fetch 해온 가장 최근에 가져온 페이지 목록이다.\n  - 두 번째 인자 `allPages`는 현재까지 가져온 모든 페이지 데이터이다.\n  - 세 번째 인자 `firstPageParam` 는 첫 번째 페이지의 매개변수이다.\n  - 네 번째 인자 `allPageParams` 는 모든 페이지의 매개변수이다.\n- 사용 가능한 다음 페이지가 없음을 표시하려면 `undefined` 또는 `null`을 반환하면 된다.\n- `getPreviousPageParam`도 존재하며, `getNextPageParam`와 반대의 속성을 갖고 있다.\n\n3. maxPages: `number | undefined`\n\n- infinite 쿼리에 저장할 `최대 페이지 수`이다.\n- 최대 페이지 수에 도달했는데 새 페이지를 가져오면 `지정된 방향(next, previous)`에 따라 페이지 배열에서 첫 번째 페이지 또는 마지막 페이지가 제거된다.\n- `0` 또는 `undefined`라면 페이지 수는 무제한이다.\n\n\u003cbr /\u003e\n\n### 💡 pageParam\n\n- `queryFn`에 넘겨주는 pageParam가 단순히 다음 page의 값만을 관리할 수 있는 것은 아니다.\n- pageParam 값은 `getNextPageParam`에서 원하는 형태로 변경시켜 줄 수 있다.\n- 무슨 말인지 예시를 보면 이해가 쉽다. 👍 아래와 같이 getNextPageParam에서 반환 데이터가 단순히 다음 페이지값이 아닌 객체로 반환한다고 해보자.\n\n```tsx\nconst { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =\n  useInfiniteQuery({\n    queryKey: [\"colors\"],\n    queryFn: ({ pageParam }) =\u003e fetchColors(pageParam), // pageParam: { page: number; etc: string }\n    initialPageParam: {\n      page: number,\n      etc: \"hi\",\n    },\n    getNextPageParam: (lastPage, allPages) =\u003e {\n      return (\n        allPages.length \u003c 4 \u0026\u0026 {\n          page: allPages.length + 1,\n          etc: \"bye\",\n        };\n      )\n    },\n  });\n```\n\n- 그러면 `queryFn`에 넣은 pageParam에서 getNextPageParam에서 반환한 객체를 받아올 수 있다.\n\n```tsx\nconst fetchColors = async ({\n  page,\n  etc,\n}: {\n  page: number;\n  etc: string;\n}): Promise\u003cAxiosResponse\u003cPaginationColors\u003e\u003e =\u003e {\n  return await axios.get(\n    `http://localhost:4000/colors?page=${page}?etc=${etc}`\n  );\n};\n```\n\n- 즉, getNextPageParam의 반환 값이 pageParams로 들어가기 때문에 pageParams를 원하는 형태로 변경하고 싶다면 getNextPageParam의 반환 값을 설정하면 된다.\n\n\u003cbr /\u003e\n\n## useMutation\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [useMutation 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useMutation)\n- react-query에서 기본적으로 서버에서 데이터를 Get 할 때는 useQuery를 사용한다.\n- 만약 서버의 data를 post, patch, put, delete와 같이 수정하고자 한다면 이때는 useMutation을 이용한다.\n- 요약하자면 `R(read)는 useQuery`, `CUD(Create, Update, Delete)는 useMutation`을 사용한다.\n\n```tsx\nconst mutation = useMutation({\n  mutationFn: createTodo,\n  onMutate() {\n    /* ... */\n  },\n  onSuccess(data) {\n    console.log(data);\n  },\n  onError(err) {\n    console.log(err);\n  },\n  onSettled() {\n    /* ... */\n  },\n});\n\nconst onCreateTodo = (e) =\u003e {\n  e.preventDefault();\n  mutate({ title });\n};\n```\n\n- useMutation의 반환 값인 mutation 객체의 `mutate` 메서드를 이용해서 요청 함수를 호출할 수 있다.\n- mutate는 `onSuccess`, `onError` 메서드를 통해 성공했을 시, 실패했을 시 response 데이터를 핸들링할 수 있다.\n- `onMutate`는 mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한 변수가 전달된다.\n- `onSettled`는 try...catch...finally 구문의 `finally`처럼 요청이 성공하든 에러가 발생하든 상관없이 마지막에 실행된다.\n\n```tsx\nconst mutation = useMutation(addTodo);\n\ntry {\n  const todo = await mutation.mutateAsync(todo);\n  console.log(todo);\n} catch (error) {\n  console.error(error);\n} finally {\n  console.log(\"done\");\n}\n```\n\n- 만약, useMutation을 사용할 때 promise 형태의 response가 필요한 경우라면 `mutateAsync`를 사용해서 얻어올 수 있다.\n\n\u003cbr /\u003e\n\n### 💡 mutate와 mutateAsync는 무엇을 사용하는 게 좋을까?\n\n- 대부분의 경우에 우리는 mutate를 사용하는 것이 유리하다. 왜냐하면 mutate는 콜백(onSuccess, onError)를 통해 data와 error에 접근할 수 있기 때문에 우리가 특별히 핸들링해 줄 필요가 없다.\n- 하지만 mutateAsync는 Promise를 직접 다루기 때문에 이런 에러 핸들링 같은 부분을 직접 다뤄야 한다.\n  - 만약 이를 다루지 않으면 `unhandled promise rejection` 에러가 발생할 수 있다.\n- [TkDodo Blog: Mutate or MutateAsync](https://tkdodo.eu/blog/mastering-mutations-in-react-query#mutate-or-mutateasync)\n\n\u003cbr /\u003e\n\n### 💡 useMutation callback과 mutate callback의 차이\n\n- useMutation은 onSuccess, onError, onSettled와 같은 Callback 함수들을 가질 수 있다.\n- 그뿐만 아니라, mutate 역시 위와 같은 Callback 함수들을 가질 수 있다.\n- 둘의 동작은 같다고 생각할 수 있지만 약간의 차이가 있다. 다음과 같다.\n  - useMutation의 Callback 함수와 mutate의 Callback 함수는 독립적으로 실행된다.\n  - 순서는 `useMutation의 Callback -\u003e mutate의 Callback` 순으로 실행된다.\n  - mutation이 완료되기 전에 컴포넌트가 unmount된다면 mutate의 Callback은 실행되지 않을 수 있다.\n- `TkDodo`는 위와 같은 이유로 둘을 분리해서 사용하는 것이 적절하다고 한다.\n  - 꼭 필요한 로직(ex. `쿼리 초기화`)은 useMutation의 Callback으로 실행시킨다.\n  - 리다이렉션 및 UI 관련 작업은 mutate Callback에서 실행시킨다.\n- [TkDodo Blog: Some callbacks might not fire](https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire)\n\n\u003cbr /\u003e\n\n## cancelQueries\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [Query Cancellation 공식 문서](https://tanstack.com/query/v5/docs/react/guides/query-cancellation)\n- 쿼리를 `수동으로 취소`하고 싶을 수도 있다.\n  - 예를 들어 요청을 완료하는 데 시간이 오래 걸리는 경우 사용자가 취소 버튼을 클릭하여 요청을 중지하도록 허용할 수 있다.\n  - 또는, 아직 HTTP 요청이 끝나지 않았을 때, 페이지를 벗어날 때도 중간에 취소해서 불필요한 네트워크 리소스를 개선할 수 있다.\n- 이렇게 하려면 쿼리를 취소하고 이전 상태로 되돌리기 위해 `queryClient.cancelQueries(queryKey)`를 사용할 수 있다. 또한 react-query는 쿼리 취소뿐만아니라 queryFn의 Promise도 취소한다.\n\n```tsx\nconst query = useQuery({\n  queryKey: [\"super-heroes\"],\n  queryFn: getAllSuperHero,\n});\n\nconst queryClient = useQueryClient();\n\nconst onCancelQuery = (e) =\u003e {\n  e.preventDefault();\n\n  queryClient.cancelQueries({\n    queryKey: [\"super-heroes\"],\n  });\n};\n\nreturn \u003cbutton onClick={onCancelQuery}\u003eCancel\u003c/button\u003e;\n```\n\n\u003cbr /\u003e\n\n## 쿼리 무효화\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- invalidateQueries은 화면을 최신 상태로 유지하는 가장 간단한 방법이다.\n- 예를 들면, 게시판 목록에서 어떤 게시글을 `작성(Post)`하거나 게시글을 `제거(Delete)`했을 때 화면에 보여주는 게시판 목록을 실시간으로 최신화해야 할 때가 있다.\n- 하지만 이때, `query Key`가 변하지 않으므로 강제로 쿼리를 무효화하고 최신화를 진행해야 하는데, 이런 경우에 `invalidateQueries()` 메소드를 이용할 수 있다.\n- 즉, query가 오래되었다는 것을 판단하고 다시 `refetch`를 할 때 사용한다!\n\n```tsx\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nconst useAddSuperHeroData = () =\u003e {\n  const queryClient = useQueryClient();\n\n  return useMutation(addSuperHero, {\n    onSuccess(data) {\n      queryClient.invalidateQueries({ queryKey: [\"super-heroes\"] }); // 이 key에 해당하는 쿼리가 무효화!\n      console.log(data);\n    },\n    onError(err) {\n      console.log(err);\n    },\n  });\n};\n```\n\n- 참고로, queryKey에 `[\"super-heroes\"]`을 넘겨주면 queryKey에 \"super-heroes\"를 포함하는 모든 쿼리가 무효화된다.\n\n```tsx\nqueryClient.invalidateQueries({ queryKey: [\"super-heroes\"] });\n\n// 아래 query들 모두 무효화 된다.\nconst query = useQuery({\n  queryKey: [\"super-heroes\", \"superman\"],\n  queryFn: fetchSuperHero,\n});\nconst query = useQuery({\n  queryKey: [\"super-heroes\", { id: 1 }],\n  queryFn: fetchSuperHero,\n});\n```\n\n- 위에 `enabled/refetch`에서도 언급했지만 `enabled: false` 옵션을 주면`queryClient`가 쿼리를 다시 가져오는 방법 중 `invalidateQueries`와 `refetchQueries`를 무시한다.\n  - [Disabling/Pausing Queries 공식 문서](https://tanstack.com/query/v5/docs/react/guides/disabling-queries) 참고\n- 자세한 내용은 [queryClient.invalidateQueries 정리](https://github.com/ssi02014/react-query-tutorial/blob/master/document/queryClient.md#invalidateQueries)를 참고하자.\n\n\u003cbr /\u003e\n\n## 캐시 데이터 즉시 업데이트\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [queryClient.setQueryData 공식 문서](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientsetquerydata)\n- 바로 위에서 `queryClient.invalidateQueries`를 이용해 캐시 데이터를 최신화하는 방법을 알아봤는데 `queryClient.setQueryData`를 이용해서도 데이터를 즉시 업데이트할 수 있다.\n- `queryClient.setQueryData`는 쿼리의 캐시 된 데이터를 즉시 업데이트하는 데 사용할 수 있는 `동기 함수`이다.\n- setQueryData의 두 번째 인자는 `updater` 함수이다. 해당 함수의 첫 번째 매개변수는 `oldData`로 기존 데이터를 가져온다.\n\n```tsx\nconst useAddSuperHeroData = () =\u003e {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: addSuperHero,\n    onSuccess(data) {\n      queryClient.setQueryData([\"super-heroes\"], (oldData: any) =\u003e {\n        return {\n          ...oldData,\n          data: [...oldData.data, data.data],\n        };\n      });\n    },\n    onError(err) {\n      console.log(err);\n    },\n  });\n};\n```\n\n\u003cbr /\u003e\n\n## Optimistic Update\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- `Optimistic Update(낙관적 업데이트)`란 서버 업데이트 시 UI에서도 어차피 업데이트할 것이라고(낙관적인) 가정해서 `미리 UI를 업데이트` 시켜주고 서버를 통해 검증받고 업데이트 또는 롤백하는 방식이다.\n- 예를 들어 facebook에 좋아요 버튼이 있는데 이것을 유저가 누른다면, 일단 client 쪽 state를 먼저 업데이트한다. 그리고 만약에 실패한다면, 예전 state로 돌아가고 성공하면 필요한 데이터를 다시 fetch 해서 서버 데이터와 확실히 연동을 진행한다.\n- Optimistic Update가 정말 유용할 때는 인터넷 속도가 느리거나 서버가 느릴 때이다. 유저가 행한 액션을 기다릴 필요 없이 바로 업데이트되는 것처럼 보이기 때문에 사용자 경험(UX) 측면에서 좋다.\n\n```tsx\nconst useAddSuperHeroData = () =\u003e {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutateFn: addSuperHero,\n    onMutate: async (newHero: any) =\u003e {\n      await queryClient.cancelQueries({ queryKey: [\"super-heroes\"] });\n\n      // 이전 값\n      const previousHeroData = queryClient.getQueryData([\"super-heroes\"]);\n\n      // 새로운 값으로 낙관적 업데이트 진행\n      queryClient.setQueryData([\"super-heroes\"], (oldData: any) =\u003e {\n        return {\n          ...oldData,\n          data: [\n            ...oldData.data,\n            { ...newHero, id: oldData?.data?.length + 1 },\n          ],\n        };\n      });\n\n      // 값이 들어있는 context 객체를 반환\n      return { previousHeroData };\n    },\n    // mutation이 실패하면 onMutate에서 반환된 context를 사용하여 롤백 진행\n    onError(error, hero, context: any) {\n      queryClient.setQueryData([\"super-heroes\"], context.previousHeroData);\n    },\n    // 오류 또는 성공 후에는 항상 refetch\n    onSettled() {\n      queryClient.invalidateQueries({ queryKey: [\"super-heroes\"] });\n    },\n  });\n};\n```\n\n- 참고로 위 예제에서 `cancelQueries`는 쿼리를 `수동으로 취소`시킬 수 있다. 취소시킬 query의 queryKey를 cancelQueries의 인자로 보내 실행시킨다.\n- 예를 들어, 요청을 완료하는 데 시간이 오래 걸리는 경우, 사용자가 취소 버튼을 클릭하여 요청을 중지하는 경우 이용할 수 있다.\n\n\u003cbr /\u003e\n\n## useQueryErrorResetBoundary\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [useQueryErrorResetBoundary 공식 문서](https://tanstack.com/query/v5/docs/react/reference/useQueryErrorResetBoundary)\n- react-query에서 ErrorBoundary와 useQueryErrorResetBoundary를 결합해 `선언적`으로 에러가 발생했을 때 Fallback UI를 보여줄 수 있다.\n- ErrorBoundary에 대한 설명은 해당 문서 참고 [ErrorBoundary](https://github.com/ssi02014/react-query-tutorial/blob/master/document/errorBoundary.md)\n\n\u003cbr /\u003e\n\n- `useQueryErrorResetBoundary`는 `ErrorBoundary`와 함께 사용되는데 이는, 기본적으로 리액트 공식 문서에서 기본 코드 베이스가 제공되긴 하지만 좀 더 쉽게 활용할 수 있는 `react-error-boundary` 라이브러리가 존재하고, react-query 공식 문서에서도 해당 라이브러리 사용을 예시로 제공해 주기 때문에 `react-error-boundary`를 설치해서 사용해 보자.\n\n```bash\n$ npm i react-error-boundary\n# or\n$ pnpm add react-error-boundary\n# or\n$ yarn add react-error-boundary\n# or\n$ bun add react-error-boundary\n```\n\n- 설치 후에 아래와 같은 QueryErrorBoundary라는 컴포넌트를 생성하고, 그 내부에 `useQueryErrorResetBoundary` 훅을 호출해 `reset` 함수를 가져온다.\n- 아래 코드 내용은 단순하다.\n  - Error가 발생하면 ErrorBoundary의 `fallbackRender` prop으로 넘긴 내용이 렌더링 되고, 그렇지 않으면 children 내용이 렌더링 된다.\n  - 또한, fallbackRender에 넣어주는 콜백 함수 매개 변수로 `resetErrorBoundary`를 구조 분해 할당을 통해 가져올 수 있는데, 이를 통해 모든 쿼리 에러를 `초기화`할 수 있다. 아래 코드 같은 경우에는 button을 클릭하면 에러를 초기화하게끔 작성했다.\n\n```tsx\nimport { useQueryErrorResetBoundary } from \"@tanstack/react-query\"; // (*)\nimport { ErrorBoundary } from \"react-error-boundary\"; // (*)\n\ninterface Props {\n  children: React.ReactNode;\n}\n\nconst QueryErrorBoundary = ({ children }: Props) =\u003e {\n  const { reset } = useQueryErrorResetBoundary(); // (*)\n\n  return (\n    \u003cErrorBoundary\n      onReset={reset}\n      fallbackRender={({ resetErrorBoundary }) =\u003e (\n        \u003cdiv\u003e\n          Error!!\n          \u003cbutton onClick={() =\u003e resetErrorBoundary()}\u003eTry again\u003c/button\u003e\n        \u003c/div\u003e\n      )}\n    \u003e\n      {children}\n    \u003c/ErrorBoundary\u003e\n  );\n};\n\nexport default QueryErrorBoundary;\n```\n\n- 그리고 App.js에다 QueryErrorBoundary 컴포넌트를 추가하면 된다. 여기서 주의할 점은 queryClient 옵션에다 `{ throwOnError: true }`를 추가해야 한다는 점이다. 그래야 오류가 발생했을 때 `ErrorBoundary` 컴포넌트가 감지할 수 있다.\n\n```tsx\nimport { QueryClientProvider, QueryClient } from \"@tanstack/react-query\";\nimport QueryErrorBoundary from \"./components/ErrorBoundary\"; // (*)\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      throwOnError: true, // (*) 여기서는 글로벌로 세팅했지만, 개별 쿼리로 세팅 가능\n    },\n  },\n});\n\nfunction App() {\n  return (\n    \u003cQueryClientProvider client={queryClient}\u003e\n      \u003cQueryErrorBoundary\u003e{/* 하위 컴포넌트들 */}\u003c/QueryErrorBoundary\u003e\n    \u003c/QueryClientProvider\u003e\n  );\n}\n```\n\n\u003cbr /\u003e\n\n## Suspense\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- ErrorBoundary는 에러가 발생했을 때 보여주는 Fallback UI를 `선언적`으로 작성할 수 있고, 리액트 쿼리는 `Suspense`와도 결합해서 `서버 통신 상태가 로딩 중`일 때 Fallback UI를 보여줄 수 있게 선언적으로 작성할 수 있다.\n- 참고로, Suspense 컴포넌트는 리액트 v16부터 제공되는 `Component Lazy Loading`이나 `Data Fetching` 등의 비동기 처리를 할 때, 응답을 기다리는 동안 Fallback UI(ex: Loader)를 보여주는 기능을 하는 컴포넌트다.\n\n```tsx\nimport { Suspense } from \"react\";\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      // suspense: true, - 💡 v5부터 Deprecated\n      // useQuery/useInfiniteQuery와 같은 일반 훅 대신 useSuspenseQuery/useSuspenseInfiniteQuery와 같은 suspense 훅 사용\n      throwOnError: true,\n    },\n  },\n});\n\nfunction App() {\n  return (\n    \u003cQueryErrorBoundary\u003e\n      \u003cSuspense fallback={\u003cLoader /\u003e}\u003e{/* 하위 컴포넌트들 */}\u003c/Suspense\u003e\n    \u003c/QueryErrorBoundary\u003e;\n  );\n}\n```\n\n- 코드를 보면 우리는 서버 상태가 로딩일 때 Loader 컴포넌트를 보여주겠다!라고 이해할 수 있다.\n- Suspense컴포넌트 내부에서 어떤 로직이 동작하는지 우리는 신경 쓰지 않아도된다. 이처럼 `내부 복잡성을 추상화`하는 게 바로 `선언형 컴포넌트`이다.\n- 위와 같이 `react-query(useSuspenseQuery)`와 결합한 `Suspense`는 아래와 같은 과정으로 동작한다.\n\n```\n1. Suspense mount\n2. MainComponent mount\n3. MainComponent에서 useSuspenseQuery 훅을 사용하여 비동기 데이터 요청\n4. MainComponent unmount, fallback UI인 Loader mount\n5. 비동기 데이터 요청이 완료되면 fallback UI인 Loader unmount\n6. MainComponent mount\n```\n\n\u003cbr /\u003e\n\n### 💡 New hooks for suspense\n\n- [new hooks for suspense](https://github.com/ssi02014/react-query-tutorial/blob/main/document/v5.md#21-%EF%B8%8F-new-hooks-for-suspense)\n- v5에서는 `data fetching`에 대한 `suspense`가 마침내 안정화되었습니다.\n- `useSuspenseQuery`, `useSuspenseInfiniteQuery`, `useSuspenseQueries` 3가지 훅이 추가되었습니다.\n- 기존의 `suspense 옵션`은 제거되었습니다. 따라서 Suspense를 적용하려면 위 훅들을 활용해야 합니다.\n- 위 3가지 훅을 사용하게 되면 타입 레벨에서 `data`가 `undefined` 상태가 되지 않습니다.\n\n```tsx\nimport { useQuery, useSuspenseQuery } from \"@tanstack/react-query\";\n\nconst fetchGroups = async (): Promise\u003c{ data: Group[] }\u003e =\u003e {\n  const res = await axios.get(\"/groups\");\n  return res;\n};\n\n// as-is\nconst { data } = useQuery({\n  queryKey: [\"groups\"],\n  queryFn: fetchGroups,\n  select: (data) =\u003e data.data,\n});\n\n// to-be\nconst { data } = useSuspenseQuery({\n  queryKey: [\"groups\"],\n  queryFn: fetchGroups,\n  select: (data) =\u003e data.data,\n});\n```\n\n\u003cbr /\u003e\n\n### 💡 @suspensive/react-query\n\n- TanStack Query(React) 공식 문서의 `Community Resources`에서는 Suspense를 더 `타입 세이프`하게 잘 사용하기 위해 [useSuspenseQuery](https://suspensive.org/ko/docs/react-query/useSuspenseQuery), [useSuspenseQueries](https://suspensive.org/ko/docs/react-query/useSuspenseQueries), [useSuspenseInfiniteQuery](https://suspensive.org/ko/docs/react-query/useSuspenseInfiniteQuery)를 제공하는 [@suspensive/react-query](https://tanstack.com/query/v4/docs/react/community/suspensive-react-query)를 소개하고 있다.\n\n- suspensive/react-query의 훅(useSuspenseQuery, useSuspenseQueries, useSuspenseInfiniteQuery)은 @tanstack/react-query v5 버전에 추가([관련 Pull Request](https://github.com/TanStack/query/pull/5739))되고 공식 API로 [이 페이지](https://tanstack.com/query/v5/docs/react/guides/suspense)에서 확인할 수 있습니다.\n\n\u003cbr /\u003e\n\n## Default Query Function\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- [Default Query Function 공식 문서](https://tanstack.com/query/v5/docs/react/guides/default-query-function)\n- 앱 전체에서 동일한 쿼리 함수를 공유하고, `queryKey`를 사용해 가져와야 할 데이터를 식별하고 싶다면 `QueryClient`에 `queryFn` 옵션을 통해 Default Query Function을 지정해 줄 수 있다.\n\n```tsx\n// 기본 쿼리 함수\nconst getSuperHero = async ({ queryKey }: any) =\u003e {\n  const heroId = queryKey[1];\n\n  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);\n};\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      queryFn: getSuperHero,\n      // ...queries options\n    },\n  },\n});\n\nfunction App() {\n  return (\n    \u003cQueryClientProvider client={queryClient}\u003e{/* ... */}\u003c/QueryClientProvider\u003e\n  );\n}\n```\n\n- `QueryClient`에 앱 전체에서 사용할 쿼리 함수를 지정해 준다.\n\n```tsx\n// 사용 예시\nconst useSuperHeroData = (heroId: string) =\u003e {\n  return useQuery({ queryKey: [\"super-hero\", heroId] });\n};\n```\n\n```tsx\n// 다음 형태는 불가능\nconst useSuperHeroData = (heroId: string) =\u003e {\n  return useQuery({\n    queryKey: [\"super-hero\", heroId],\n    queryFn: () =\u003e getSuperHero(heroId),\n  });\n};\n```\n\n- useQuery의 첫 번째 인자로 `queryKey`만 넣어주면 두 번째 인자에 들어갈 `queryFn`은 자동으로 설정된 기본 쿼리 함수가 들어간다.\n- 일반적으로 `useQuery`를 사용할 때와 달리 `queryFn`을 지정하지 않기에 쿼리 함수에 직접 인자를 넣어주는 형태의 사용은 불가능하다.\n\n\u003cbr /\u003e\n\n## React Query Typescript\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- React Query는 TypeScript의 `제네릭(Generics)`을 많이 사용한다. 이는 라이브러리가 실제로 데이터를 가져오지 않고 API가 반환하는 데이터 유형을 알 수 없기 때문이다.\n\n\u003cbr /\u003e\n\n### useQuery\n\n현재 useQuery가 갖고 있는 제네릭은 `4개`이며, 다음과 같다.\n\n1. **TQueryFnData**: useQuery로 실행하는 query function의 `실행 결과`의 타입을 지정하는 제네릭 타입이다.\n2. **TError**: query function의 `error` 형식을 정하는 제네릭 타입이다.\n3. **TData**: useQuery의 `data에 담기는 실질적인 데이터`의 타입을 말한다. 첫 번째 제네릭과의 차이점은 `select`와 같이 query function의 반환 데이터를 추가 핸들링을 통해 반환하는 경우에 대응할 수 있는 타입이라고 생각하면 좋다.\n4. **TQueryKey**: useQuery의 첫 번째 인자 `queryKey`의 타입을 명시적으로 지정해 주는 제네릭 타입이다.\n\n```tsx\n// useQuery의 타입\nexport function useQuery\u003c\n  TQueryFnData = unknown,\n  TError = DefaultError,\n  TData = TQueryFnData,\n  TQueryKey extends QueryKey = QueryKey\n\u003e\n```\n\n```tsx\nimport { AxiosError } from \"axios\";\n\n// useQuery 타입 적용 예시\nconst { data } = useQuery\u003c\n  AxiosResponse\u003cHero[]\u003e,\n  AxiosError,\n  string[],\n  [\"super-heroes\", number]\n\u003e({\n  queryKey: [\"super-heroes\", id],\n  queryFn: getSuperHero,\n  select: (data) =\u003e {\n    const superHeroNames = data.data.map((hero) =\u003e hero.name);\n    return superHeroNames;\n  },\n});\n\n/**\n 주요 타입\n * data: string[] | undefined\n * error: AxiosError\u003cany, any\u003e\n * select: (data: AxiosResponse\u003cHero[]\u003e): string[]\n */\n```\n\n\u003cbr /\u003e\n\n### useMutation\n\nuseMutation도 useQuery와 동일하게 현재 4개이며, 다음과 같다.\n\n1. **TData**: useMutation에 넘겨준 mutation function의 `실행 결과`의 타입을 지정하는 제네릭 타입이다.\n   - data의 타입과 onSuccess(1번째 인자) 인자의 타입으로 활용된다.\n2. **TError**: useMutation에 넘겨준 mutation function의 `error` 형식을 정하는 제네릭 타입이다.\n3. **TVariables**: `mutate 함수`에 전달 할 인자를 지정하는 제네릭 타입이다.\n   - onSuccess(2번째 인자), onError(2번째 인자), onMutate(1번째 인자), onSettled(3번째 인자) 인자의 타입으로 활용된다.\n4. **TContext**: mutation function을 실행하기 전에 수행하는 `onMutate 함수의 return값`을 지정하는 제네릭 타입이다.\n   - onMutate의 결과값의 타입을 onSuccess(3번째 인자), onError(3번째 인자), onSettled(4번째 인자)에서 활용하려면 해당 타입을 지정해야 한다.\n\n```tsx\nexport function useMutation\u003c\n  TData = unknown,\n  TError = DefaultError,\n  TVariables = void,\n  TContext = unknown\n\u003e\n```\n\n```tsx\n// useMutation 타입 적용 예시\nconst { mutate } = useMutation\u003cTodo, AxiosError, number, number\u003e({\n  mutationFn: postTodo,\n  onSuccess: (res, id, nextId) =\u003e {},\n  onError: (err, id, nextId) =\u003e {},\n  onMutate: (id) =\u003e id + 1,\n  onSettled: (res, err, id, nextId) =\u003e {},\n});\n\nconst onClick = () =\u003e {\n  mutate(5);\n};\n\n/** \n 주요 타입\n * data: Todo\n * error: AxiosError\u003cany, any\u003e\n * onSuccess: (res: Todo, id: number, nextId: number)\n * onError: (err: AxiosError, id: number, nextId: number)\n * onMutate: (id: number)\n * onSettled: (res: Todo, err: AxiosError, id: number, nextId: number),\n*/\n```\n\n\u003cbr /\u003e\n\n### useInfiniteQuery\n\n현재 useInfiniteQuery 갖고 있는 제네릭은 `4개`이며, useQuery와 유사하다.\n\n1. **TQueryFnData**: useInfiniteQuery로 실행하는 query function의 `실행 결과`의 타입을 지정하는 제네릭 타입이다.\n2. **TError**: query function의 `error` 형식을 정하는 제네릭 타입이다.\n3. **TData**: useInfiniteQuery의 `data에 담기는 실질적인 데이터`의 타입을 말한다. 첫 번째 제네릭과의 차이점은 `select`와 같이 query function의 반환 데이터를 추가 핸들링을 통해 반환하는 경우에 대응할 수 있는 타입이라고 생각하면 좋다.\n4. **TQueryKey**: useInfiniteQuery의 첫 번째 인자 `queryKey`의 타입을 명시적으로 지정해 주는 제네릭 타입이다.\n\n```tsx\nexport function useInfiniteQuery\u003c\n  TQueryFnData = unknown,\n  TError = DefaultError,\n  TData = InfiniteData\u003cTQueryFnData\u003e,\n  TQueryKey extends QueryKey = QueryKey\n\u003e\n```\n\n```tsx\nconst {\n  data,\n  hasNextPage,\n  fetchNextPage,\n  //...\n} = useInfiniteQuery\u003c\n  AxiosResponse\u003cPaginationColors\u003e,\n  AxiosError,\n  InfiniteData\u003cAxiosResponse\u003cPaginationColors\u003e, number\u003e,\n  [\"colors\"]\n\u003e({\n  queryKey: [\"colors\"],\n  queryFn: fetchColors,\n  initialPageParam: 1,\n  getNextPageParam: (lastPage, allPages) =\u003e {\n    return allPages.length \u003c 4 \u0026\u0026 allPages.length + 1;\n  },\n  // ...options\n});\n\n/**\n 주요 타입\n * data: InfiniteData\u003cAxiosResponse\u003cPaginationColors\u003e, number\u003e | undefined\n * error: AxiosError\u003cany, any\u003e\n * select: (data: InfiniteData\u003cAxiosResponse\u003cPaginationColors\u003e, number\u003e): InfiniteData\u003cAxiosResponse\u003cPaginationColors\u003e, number\u003e\n * getNextPageParam: GetNextPageParamFunction\u003cnumber, AxiosResponse\u003cPaginationColors\u003e\u003e\n*/\n```\n\n\u003cbr /\u003e\n\n### 💡 Typescript Best Practice\n\n- [TypeScript 공식 문서](https://tanstack.com/query/v5/docs/react/typescript)\n- 위의 제네릭을 모두 사용하는 건 코드의 복잡도가 늘어난다. 하지만 react query는 타입을 잘 전달하므로 굳이 제네릭을 모두 직접 제공할 필요가 없다.\n- 가장 좋은 방법은 `queryFn`의 타입을 잘 정의해서 `타입 추론`이 원활하게 되게 하는 것이다.\n\n```tsx\nconst fetchGroups = async (): Promise\u003cAxiosResponse\u003cGroup[]\u003e =\u003e {\n  return await axios.get(\"/groups\");\n};\n\nconst { data } = useQuery({\n  queryKey: [\"groups\"],\n  queryFn: fetchGroups,\n  select: (data) =\u003e data.data,\n});\n\n/**\n 주요 타입\n * data: AxiosResponse\u003cGroup[]\u003e | undefined\n * error: Error | null\n * select: (data: AxiosResponse\u003cGroup[]\u003e): Group[]\n */\n```\n\n\u003cbr /\u003e\n\n## React Query ESLint Plugin\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- Tanstack Query는 `자체 ESLint Plugin`을 제공합니다. 해당 플러그인을 통해 모범 사례를 적용하고, 일반적인 실수를 방지할 수 있습니다.\n\n### 설치\n\n```bash\n$ npm i -D @tanstack/eslint-plugin-query\n# or\n$ pnpm add -D @tanstack/eslint-plugin-query\n# or\n$ yarn add -D @tanstack/eslint-plugin-query\n# or\n$ bun add -D @tanstack/eslint-plugin-query\n```\n\n\u003cbr /\u003e\n\n### 사용 방법(1)\n\n- 플러그인에 대한 `권장하는 모든 rule`을 적용하려면 아래와 같이 `.eslintrc.js` 파일의 `extends`배열 안에 `plugin:@tanstack/eslint-plugin-query/recommended`을 추가합니다.\n\n```js\nmodule.exports = {\n  // ...\n  extends: [\"plugin:@tanstack/eslint-plugin-query/recommended\"],\n  rules: {\n    /* 수정하고자 하는 rule 추가 가능... */\n  },\n};\n```\n\n- 물론, rule을 변경하고 싶다면 rules에 아래 `사용방법(2)`와 같이 rule을 추가하면 됩니다.\n\n\u003cbr /\u003e\n\n### 사용 방법(2)\n\n- 원하는 `rule`을 개별적으로 설정해서 적용하려면 아래와 같이 `.eslintrc.js` 파일의 `plugins`배열 안에 `@tanstack/query`를 추가하고, 적용하고자 하는 `rules`에 규칙을 추가합니다.\n\n```js\nmodule.exports = {\n  // ...\n  plugins: [\"@tanstack/query\"],\n  rules: {\n    \"@tanstack/query/exhaustive-deps\": \"error\",\n    \"@tanstack/query/no-rest-destructuring\": \"warn\",\n    \"@tanstack/query/stable-query-client\": \"error\",\n  },\n};\n```\n\n\u003cbr /\u003e\n\n## 지원 버전\n\n[목차 이동](#주요-컨셉-및-가이드-목차)\n\n- Tanstack Query v5에 필요한 TypeScript 최소 버전은 `v4.7`입니다.\n- Tanstack Query v5에 필요한 React 최소 버전은 `v18`입니다.\n\n  - React v18 이상에서 지원하는 `useSyncExternalStore` 훅을 사용하고 있기 때문입니다.\n\n- Tanstack Query v5의 브라우저별 지원 버전은 아래와 같습니다.\n\n```\nChrome \u003e= 91\nFirefox \u003e= 90\nEdge \u003e= 91\nSafari \u003e= 15\niOS \u003e= 15\nopera \u003e= 77\n```\n\n\u003cbr /\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fssi02014%2Freact-query-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fssi02014%2Freact-query-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fssi02014%2Freact-query-tutorial/lists"}