-
Notifications
You must be signed in to change notification settings - Fork 0
[Step2.3] hippo: TanStack Query 적용하기 #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: hippo-adv-2.2
Are you sure you want to change the base?
Changes from all commits
1bea119
49b51ba
2448f28
6b878d6
57b1606
8cf2959
8a598a7
ceb344e
4d2d567
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 02-3. 서버상태관리 - TanStack Query | ||
|
|
||
| 💡해당 미션은 **여러 사용자가 하나의 서버를 공유하고, 식당 목록이 주기적으로 업데이트되는 환경**을 가정하여 진행합니다. | ||
|
|
||
| ## 🎯 요구사항 | ||
|
|
||
| - TanStack Query를 사용해 서버 상태를 클라이언트 상태와 분리하고, 효율적인 데이터 캐싱과 요청 관리를 구현해 보세요. | ||
| - 쿼리 및 뮤테이션 설정은 명확한 이유가 있다면 자유롭게 변경해도 좋습니다. | ||
| - TanStack Query를 **왜** 사용하는지, 서버 상태와 클라이언트 상태를 분리하였을때 어떤 점이 달랐는지, 또 trade-off가 있는지 적어주세요. | ||
| - 기술적인 것도 좋고 개발자의 경험 측면에서도 좋습니다. | ||
| - TanStack Query Devtools를 이용하여 Query의 변화와 Mutation의 발생을 확인해보세요. | ||
| - (선택) 뮤테이션 로직에 낙관적 업데이트(Optimistic Update)를 적용해 보고 어떤 상황에서 낙관적 업데이트가 효과적인지, 그리고 주의해야 할 점은 무엇인지 적어주세요. | ||
| - Browser Throttling 기능을 활용하여 네트워크 속도를 느리게 설정한 뒤 낙관적 업데이트가 실제로 어떻게 동작하는지 확인해 보세요. | ||
|
|
||
| ### 😗구현 예시 | ||
|
|
||
| - 컴포넌트의 이름이나 구조를 정한 이유가 명확해야하며 타인에게 설명할 수 있어야합니다. | ||
| - 아래는 main.jsx의 설정 모습입니다. | ||
|
|
||
| ```javascript | ||
| import { createRoot } from "react-dom/client"; | ||
| import App from "./App.jsx"; | ||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
|
|
||
| const queryClient = new QueryClient(); | ||
|
|
||
| createRoot(document.getElementById("root")).render( | ||
| <QueryClientProvider client={queryClient}> | ||
| <App /> | ||
| </QueryClientProvider> | ||
| ); | ||
| ``` | ||
|
|
||
| ## ✅ 키워드 | ||
|
|
||
| - props drilling | ||
| - 서버 상태관리 | ||
| - TanStack Query | ||
| - QueryClient | ||
| - Query Key | ||
| - useQuery | ||
| - useMutation | ||
| - Optimistic Update | ||
|
|
||
| ## 🧙♀️ 진행 가이드 | ||
|
|
||
| - 진행시간 : 4시간 내에 완료하는 것을 목표로 합니다. | ||
|
|
||
| ## 🔗 참고 문서 | ||
|
|
||
| - [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) | ||
| - [TanStack Query 메인테이너 Tk Dodo 님의 블로그](https://tkdodo.eu/blog/tags/react-query) | ||
| - [테코톡(시모의 Tanstack Query)](https://www.youtube.com/watch?v=RfK15tw8H-I) |
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,16 @@ | ||
| import React from "react"; | ||
| import ReactDOM from "react-dom/client"; | ||
| import App from "./App.jsx"; | ||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
| import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; | ||
|
|
||
| const queryClient = new QueryClient(); | ||
|
|
||
| ReactDOM.createRoot(document.getElementById("root")).render( | ||
| <React.StrictMode> | ||
| <App /> | ||
| </React.StrictMode> | ||
| <QueryClientProvider client={queryClient}> | ||
| <App /> | ||
| <ReactQueryDevtools /> | ||
| </QueryClientProvider> | ||
| </React.StrictMode>, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const restaurantKeys = { | ||
| all: () => ["restaurants"], | ||
| }; |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [배움] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
| import { createRestaurant } from "../api"; | ||
| import { restaurantKeys } from "./queryKeys"; | ||
|
|
||
| export function useAddRestaurantMutation() { | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: createRestaurant, | ||
| onMutate: async (newRestaurant) => { | ||
| await queryClient.cancelQueries({ queryKey: restaurantKeys.all() }); | ||
|
|
||
| const previousRestaurants = queryClient.getQueryData(restaurantKeys.all()); | ||
|
|
||
| queryClient.setQueryData(restaurantKeys.all(), (old) => [ | ||
| ...old, | ||
| { ...newRestaurant, id: crypto.randomUUID() }, | ||
| ]); | ||
|
|
||
| return { previousRestaurants }; | ||
| }, | ||
| onError: (_err, _newRestaurant, context) => { | ||
| queryClient.setQueryData(restaurantKeys.all(), context.previousRestaurants); | ||
| }, | ||
|
Comment on lines
+22
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [배움] |
||
| onSettled: () => { | ||
| queryClient.invalidateQueries({ queryKey: restaurantKeys.all() }); | ||
| }, | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { getRestaurants } from "../api"; | ||
| import { restaurantKeys } from "./queryKeys"; | ||
|
|
||
| export function useRestaurantsQuery() { | ||
| return useQuery({ | ||
| queryKey: restaurantKeys.all(), | ||
| queryFn: getRestaurants, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { create } from "zustand"; | ||
| import { persist, createJSONStorage } from "zustand/middleware"; | ||
| import { ALL_CATEGORY } from "../constants/categories.js"; | ||
|
|
||
| const useFilterStore = create( | ||
| persist( | ||
| (set) => ({ | ||
| // 상태 | ||
| selectedCategory: ALL_CATEGORY, | ||
|
|
||
| // 액션 | ||
| setSelectedCategory: (category) => set({ selectedCategory: category }), | ||
| }), | ||
| { | ||
| name: "category-filter", | ||
| storage: createJSONStorage(() => sessionStorage), | ||
| }, | ||
| ), | ||
| ); | ||
|
|
||
| export default useFilterStore; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[논의]
onMutate에서 목록을 낙관적으로 업데이트하면서도 모달은onSuccess(서버 응답 후)에서 닫히도록 구현하셨는데, 이 조합에서 목록에는 새 항목이 이미 보이는데 모달은 아직 열려있는 상태가 짧게 발생하지 않나요? 저는 낙관적 업데이트라면 모달도onMutate에서 함께 닫는 게 일관성 있다고 판단해서 그렇게 구현했는데, 혹시 다르게 판단하신 이유가 있으신지 궁금합니다!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지적하신 부분은 맞습니다. 하지만 의도가 있는 구현이었는데 서버 실패 시 유저가 폼을 다시 채울 수 있게 하려고 했습니다. onMutate에서 모달을 먼저 닫아버리면 실패 시 유저가 입력했던 내용이 사라집니다. 하지만 onSuccess에서 모달을 닫으면 실패했을 때 모달이 그대로 남아 있어서 유저가 바로 수정하고 다시 제출할 수 있습니다.