Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b42ecde
chore: axios 설치
4BFC Feb 7, 2026
f05849f
chore: react-router-dom, vite-plugin-pages 설치
4BFC Feb 7, 2026
fbe3ca5
feat: React page routing 구현
4BFC Feb 7, 2026
7948197
feat: tanstckquery 페이지 생성
4BFC Feb 7, 2026
fc2eed4
feat: AppRoutes에서 micro provider 설정 구현
4BFC Feb 7, 2026
68b8a76
feat: queryClient 안티 패턴 주석 추가 및 config 객체 추가
4BFC Feb 9, 2026
fce9869
feat: `reqres` user api에 사용되는 service 힘수 구현
4BFC Feb 9, 2026
8c0acf6
feat: user를 조회하고 생성할 수 있는 훅 구현
4BFC Feb 10, 2026
ed3382e
feat: 간단한 interface 구현
4BFC Feb 10, 2026
723d45d
chore: reqres API 등록 및 CORS로 인한 vite-proxy 설정
4BFC Feb 10, 2026
f45ba3c
refactor: reqres 응답 구조에 맞게 네이밍을 스네일 케이스로 변경
4BFC Feb 10, 2026
2f3cc0a
chore: env gitignore에 추가
4BFC Feb 10, 2026
6477a93
refactor: reqres에서 mockapi 로 수정
4BFC Feb 10, 2026
66b6d6e
feat: update 함수 구현 및 훅 추가
4BFC Feb 17, 2026
5623253
feat: user 디테일 페이지 구현
4BFC Feb 17, 2026
26e89bf
docs: 작업 중 발생한 트러블 슈팅 문서화 작성
4BFC Feb 17, 2026
330316b
docs: stale cache의 상호작용 문제점 문서화
4BFC Feb 18, 2026
133deed
docs: reference 링크 추가
4BFC Feb 18, 2026
852cc0e
fix: 문서 수정
4BFC Feb 18, 2026
62e1b28
chore: axios 설치
4BFC Feb 7, 2026
e7b2f7b
chore: react-router-dom, vite-plugin-pages 설치
4BFC Feb 7, 2026
f4b7fe5
feat: React page routing 구현
4BFC Feb 7, 2026
57c3049
feat: tanstckquery 페이지 생성
4BFC Feb 7, 2026
f83869e
feat: AppRoutes에서 micro provider 설정 구현
4BFC Feb 7, 2026
8ed2471
feat: queryClient 안티 패턴 주석 추가 및 config 객체 추가
4BFC Feb 9, 2026
96106f7
feat: `reqres` user api에 사용되는 service 힘수 구현
4BFC Feb 9, 2026
2802818
feat: user를 조회하고 생성할 수 있는 훅 구현
4BFC Feb 10, 2026
cae8df5
feat: 간단한 interface 구현
4BFC Feb 10, 2026
831d416
chore: reqres API 등록 및 CORS로 인한 vite-proxy 설정
4BFC Feb 10, 2026
601c530
refactor: reqres 응답 구조에 맞게 네이밍을 스네일 케이스로 변경
4BFC Feb 10, 2026
7ab1b14
refactor: reqres에서 mockapi 로 수정
4BFC Feb 10, 2026
c65274e
feat: update 함수 구현 및 훅 추가
4BFC Feb 17, 2026
cd79195
feat: user 디테일 페이지 구현
4BFC Feb 17, 2026
a6bd7fa
docs: 작업 중 발생한 트러블 슈팅 문서화 작성
4BFC Feb 17, 2026
977606e
docs: stale cache의 상호작용 문제점 문서화
4BFC Feb 18, 2026
11012e5
docs: reference 링크 추가
4BFC Feb 18, 2026
5edd7cc
fix: 문서 수정
4BFC Feb 18, 2026
b09d657
docs: feedback 문서화
4BFC Feb 25, 2026
3998626
fix: feedback을 반영한 useUsers 구현 및 Network 순서 확인
4BFC Feb 25, 2026
91aab47
merge: origin/poby/tanstackquery-cache-data 병합
4BFC Feb 25, 2026
2f18616
docs: feedback 반영한 내용들을 담아 문서화
4BFC Feb 26, 2026
451e072
fix: 오타 수정
4BFC Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions log/data/poby/tanstackquery-cache-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# TanStack Query Stale Cache

## 배경

상세 정보에서 사용자의 데이터를 변경 또는 수정을 하고 다시 리스트로 돌아가면 리스트에서는 과거의 데이터가 남아 있는 경우가 있다. 이럴 때, cache를 invalidateQueries를 사용해서 무력화 하는데, invalidateQueries가 동작하는 방식과 invalidateQueries 대시 다른 대안과 해결책을 모색하는 것이 목표이자, 배경이다.

## 문제점

1. 불필요한 네트워크 요청 - 이미 mutation 응답에 새 데이터가 있는데 또 fetch
2. Stale cache 문제 - 상호작용 되어야하는 페이지간 querykey를 여러번 호출
3. UX 저하 - 업데이트 후 깜빡임

## 원인

> 해당 문서는 문제점 2번을 중점으로 바라본 문서이다.

invalidateQueries를 사용하면 cache를 강제로 refetch를 한다고 이해를 했다. 하지만, invalidateQueries는 cache의 상태를 stale 상태로 변경을 하고 마운트 언마운트 상태에 따라 refetch의 수행이 이루어진다. invalidateQueries의 동작방식은 ~~(TQ는 cache를 신선도에 따라 상태를 분리한다.)~~ 현재 key의 cache가 fresh 하던 stale 하던 상관 없이 stale로 상태를 둔다. 그리고 현 페이지가 ~~(invalidateQueries를 호출하고)~~ 마운트가 된 상태라면 refetch가 된다. 정리하자면, 상세 페이지에서는 invalidateQueries로 stale mark를 찍고 마운트가 되면 refetch가 이루어진다. 단, 목록 페이지에서는 cache의 상태값을 변경하는 로직이 없기 때문에, 변경된 값이 반영되지 않고 이전 데이터가 보이는 것이다.

## 해결 방안

> useUsers.ts의 [useUpdateUser](../../workspace/react/ts/src/hooks/useUsers.ts) 참고

- setQueryData
- setQueryData를 사용해서 직접 특정 querykey를 업데이트, 리스트도 함께 업데이트 하는 방법이 있다.

```ts
export function useUpdateUser() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: ({ id, ...payload }) => updateUser(id, payload),
onSuccess: (newUser) => {
// 서버 응답으로 캐시 직접 업데이트
queryClient.setQueryData(
userKeys.detail(newUser.id),
newUser
)
// 리스트도 업데이트
queryClient.setQueryData(userKeys.lists(), (old) =>
old?.map(user => user.id === newUser.id ? newUser : user)
)
}
})
}
```

- Optimistic update
- 낙관적 업데이트 방법은 실문에서 자주 사용했다. 변경된 UI를 Network 요청 결과와는 무관하게 즉시 없데이트를 하고, onError와 같은 실패 시 롤백 할 수 있게 구현을 방법이다.

```ts
useMutation({
mutationFn: updateUser,
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: userKeys.lists() })
const previous = queryClient.getQueryData(userKeys.lists())

// 즉시 UI 업데이트
queryClient.setQueryData(userKeys.lists(), (old) =>
old?.map(u => u.id === newData.id ? {...u, ...newData} : u)
)
return { previous }
},
onError: (err, newData, context) => {
// 실패 시 롤백
queryClient.setQueryData(userKeys.lists(), context.previous)
}
})
```

## 결론

처음에는 InvalidateQueries가 좋지 않은 anti-pattern이거나 TQ에서 권장하지 않는 방식이라 생각했다. 개념을 다시 정리하고 살펴보면 단순히 cache의 신선도를 stale로 변경하고 해당 query가 호출, 구독된 컴포넌트(QueryObserver)를 마운트 언마운트에 따라 refetch가 결정되는 방식이란 것을 이해하고 다시 코드 useUsers.ts의 useUpdateUser함수를 보면 lists도 다음과 같이 invalidateQueries를 하면 되는 것 아닌가 싶었다.

```ts
onSuccess: () => {
queryClient.invalidateQueries({queryKey: userKeys.details()})
queryClient.invalidateQueries({queryKey: userKeys.lists()})
}
```

즉, querykey가 체이닝되는 관계성만 명확하게 명시, 기획이 되어 있다면, InvalidateQueries가 성능을 저하하거나 좋지 않은 방식은 아니라 생각이 된다. 이외의 다른 관점으로 InvalidateQueries의 평가 문제 정의가 필요하다고 생각한다.

---

## 트러블슈팅

현재 TQ provider의 query 설정 외부 주입할 수 있게 작성을 했다. 이런 방법은 현재로는 매우 오버스팩일 수 있겠다는 생각을 했다.

### createQueryClinet anti-pattern

- 현재 createQueryClient는 micro 디렉토리 전략 방식에 사용하기 위해서 분리를 했으나 이로 인해서 발생하는 문제는 다음과 같다.
- 각 페이지간 서로의 캐시를 공유할 수 없게된다. 즉, Provider간 key 값이 달라지기 때문에 그렇다. 그로 인해서 메모리 낭비가 발생한다. 매번 QueryClinet가 여러개 새로 생성되기 때문이다.

- 그렇다면 createQueryClinet는 어떻게 사용하면 좋을까?
- 테스트 환경에서 사용하면 좋다.
- 환경별(dev/prod)와 같이 분리된 공간에서 사용하면 좋다.
- 임베디드 위젯과 같이 독립된 곳에서 사용하면 좋다.
- Storybook과 같은 곳에서 사용하면 좋다.
- 즉, 모노레포와 같은 곳에서 사용, 각기 다른 도메인과 같은 곳에서 사용하면 좋다. 따라서 해상 createQueryClient는 common이나 shared같은 곳에서 관리하면서 사용하면 좋다.

- 결론은 개별 쿼리 훅(ex.useUsers)에서 관리하면서 사용하는 것이 좋다.

### 참고 문서

- [micro frontend](https://jobkaehenry.tistory.com/64)
- [query-invalidation](https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation)
- [QueryClient](https://tanstack.com/query/latest/docs/reference/QueryClient)
- [Mutation 이후 전체 Query를 invalidation 하기](https://yogjin.tistory.com/130)

---

## Feedback 반영

### Feedback List

> 현재는 리스트 페이지와 상세 페이지가 cache로 인해서 동기화가 되고 있지 않은 상태이다. <br><br>
> feedback <br>
> link: [inavlidate](https://github.com/4BFC/delllog/pull/2#discussion_r2830779295) <br>
> link: [fetchQuery](https://github.com/4BFC/delllog/pull/2#discussion_r2831265897) <br>
> link: [캐싱데이터 공유](https://github.com/4BFC/delllog/pull/2#discussion_r2830828635) <br>

- onSuccess에서 invalidate를 하지 않고 fetchQuery를 사용 후 update 반영이 잘되는지 문서화
- invalidate with cache-key 방법은 실무와 평소에 많이 해봤기 때문에 패스한다.
- fetchQuery를 깔끔하게 사용하려면 queryFn을 포함한 query options factory로 확장이 필요하다.
- 캐싱데이터를 공유하면서 옵션만 따로 제공하면서, 하나의 QueryClient를 최상단에서 제공하고, 하위에서 옵션만 분리적으로 주입할 수 있는 방법 리서치

### fetchQuery의 필요성

fetchQuery는 내가 특정 fetching 함수를 직접 호출해서 미리 fetch를 해두는 역할로, cache-key를 가지고 관리하는 것을 한단계 넘어서, 다음 동작, 상태를 업데이트 하기 위한 디테일함을 가지고 있다. invalidate같은 경우에는 cache의 신선도 상태를 변경시키고 필요한 곳에서 fetch가 이루어지면 fresh한 상태로 상태 값을 받을 수 있다. 따라서, 만약 fetching, Network 순서나 시나리오, 케이스가 성립된게 없다면, invalidate가 안정적이지만, 특정 시나리오나 케이스가 명확하다면 fetchQuery가 유용하다.

### 비동기 동작을 async await으로 Network 동기 순서 보장

invalidateQueries, fetchQuery는 Promise이기 때문에 async/await을 사용해서 network의 순서를 관리하고 순서 보장을 해주면 더욱 디테일한 fetching 로직이 완성 될 것같다.
> F12 + `.d.ts` 를 활용해서 타입 확인을 적극적으로 했다.

- Extentions : [TypeScript Nightly](https://marketplace.cursorapi.com/items/?itemName=ms-vscode.vscode-typescript-next)
Loading