๐ก 02. [์ํคํ ์ฒ] ํจ๊ณผ์ ์ธ Query Key ์ค๊ณ ์ํฌ์ต
๐ ๊ฐ์
์ ์ญ ์บ์๋ฅผ ํํธํ ์์ด ๊ณ์ธต์ ์ผ๋ก ์ค๊ณํ๋ ๊ธฐ๋ฒ๊ณผ, ์ค๋ฌด์ ๊ฝ์ธ Query Key Factory ํจํด(๋ฐ Query Options API)์ ๋์ ํ์ฌ ํ์ ์์ ์ฑ์ ํ๋ณดํฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "๋ฌดํจํ๋ฅผ ๋ ๋ ธ๋๋ฐ ์๋ฑํ ์ ๋ค์ด ์ฌ๋ผ์ ธ์..."
- ๐ค ์ ์์์ผ ํ๋๊ฐ: ๋ฌธ์์ด ์คํ๊ฐ ๋ถ๋ฅด๋ ์บ์ ๋์ฐธ์ฌ
- 1. ๋ฐฐ์ด ํค์ ํฌํจ ๊ด๊ณ (Fuzzy Matching) ํด๋ถ
- 2. ์ค์ ์ ์ฉ: Query Key Factory ์ํคํ ์ฒ
- 3. ๊ถ๊ทน์ Next Step: Query Options API (v5)
- ๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
"์์ฒ ๋, ์๊น
invalidateQueries(['todos'])์น์ จ์ฃ ? ๋ฐฉ๊ธ ์์ ๋์ด ๋ณด๋ ํ๋ฉด ์บ์๊น์ง ์น ๋ ์๊ฐ์ต๋๋ค. ์ฟผ๋ฆฌ ํค ์คํ ๋ด์ จ๊ฑฐ๋ ์."
โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "๋ฌดํจํ๋ฅผ ๋ ๋ ธ๋๋ฐ ์๋ฑํ ์ ๋ค์ด ์ฌ๋ผ์ ธ์..."
(ํ์์ผ ์ ์ฌ ์ง์ , ๋ฒ๊ทธ ์ ๋ณด๋ฅผ ๋ฐ๊ณ ์ฐฝ๋ฐฑํด์ง ์์ฒ )
๐ฃ ์์ฒ : ์ ๋ฆฌ๋ ๋ ํฐ์ผ ๋ฌ์ด์! ์ ์ ๊ฐ '์๋ฃ๋ ํ ์ผ' ๋ชฉ๋ก์์ ์์ดํ ํ๋ ์ญ์ ํ๋๋ฐ, ๊ฐ์๊ธฐ ๋ฉ์ธ ํ๋ฉด์ ์๋ '์งํ ์ค์ธ ํ ์ผ' ๋ฆฌ์คํธ๋ '์ด๋ฒ ๋ฌ ์ ์ฒด ํต๊ณ' ์บ์๊น์ง ๋ชฝ๋ ํญํ(Invalidate)๋ผ์ ์ ๋ถ ๋ฏธ์น ๋ฏ์ด ๋ฆฌํจ์นญ ์น๊ณ ์๋ฒ ๋ถํ๊ฐ ํ์์ด์;;
๐ฆ ์ํธ: ์ฝ๋ ์ค ๋ณด์ธ์. (ํ๋ฅํ๋ฅ)
queryClient.invalidateQueries({ queryKey: ['todos'] }) ์ด๋ ๊ฒ ์์
จ๊ตฐ์.
์์ฒ ๋์ด ๋ชจ๋ ํ
๋ค์ ์ฟผ๋ฆฌ ํค๋ฅผ ['todos', 'done'], ['todos', 'in-progress'], ์ฌ์ง์ด ['todos', 'stats', 'monthly'] ์ด๋ ๊ฒ ์ค๊ตฌ๋๋ฐฉ ๋ฌธ์์ด๋ก ๋ฌ์๋์
จ์์์.
React Query์ invalidate ๋ ์์ ํค(['todos']) ๋ง ํฌํจ๋์ด ์์ผ๋ฉด ๊ทธ ๋ฐ์ ๊ผฌ๋ฆฌํ๊ฐ ๋ฌ๋ฆฐ ํ์ ํธ๋ฆฌ ์ ์ฒด๋ฅผ ์๋น ์์ด ๋ฐ์ด ๋ด๋ฒ๋ฆฝ๋๋ค.
๐ฃ ์์ฒ : ํ! ๊ทธ๋ผ ์ง์ฐ๊ณ ์ถ์ '์๋ฃ๋ ๋ฆฌ์คํธ'๋ง ๋ฑ ๊ผฌ์ง์ด์ ์ง์ฐ๋ ค๋ฉด ์ด๋กํด์?
๐ฆ ์ํธ: ๊ทธ๋์ 5๋ ์ฐจ ์ค๋ฌด์ ์์์ "์ฟผ๋ฆฌ ํค ํฉํ ๋ฆฌ(Query Key Factory)" ๊ณ์ธตํ ์ํคํ ์ฒ ๋ฅผ ์ง๋ ๊ฒ๋ถํฐ ์ถ๋ฐํ๋ ๊ฒ๋๋ค. ์ค์ ํต์ ์๊ฐ ์์ผ๋ฉด ์ด๋ฐ ์คํ๊ฒํฐ ๋ฌดํจํ ํ์์ด ํฐ์ง๋๋ค.
๐ค ์ ์์์ผ ํ๋๊ฐ: ๋ฌธ์์ด ์คํ๊ฐ ๋ถ๋ฅด๋ ์บ์ ๋์ฐธ์ฌ
React Query์ ์ฝ์ด ์ฌ์ฅ์ QueryCache ๋ผ๋ ๊ฑฐ๋ํ ๊ธ๊ณ ์
๋๋ค.
๊ทธ ๊ธ๊ณ ์ ๋ฐฉ ๋ฒํธ๊ฐ ๋ฐ๋ก ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด๋ก ๋๊ฒจ์ฃผ๋ Query Key ์ฃ .
์ฌ๊ธฐ์ ๋ฐ์ํ๋ ๋ ๊ฐ์ง ์น๋ช ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
- ํ์
์ ๋ถ์ฌ: ๊ฐ๋ฐ์ A๋
useQuery({ queryKey: ['todo', 1] })๋ผ๊ณ ์ง๊ณ , ๊ฐ๋ฐ์ B๋ ์ญ์ ์๋ฃ ํ ์๋ฑํ๊ฒqueryClient.invalidateQueries({ queryKey: ['todos', 1] })๋ผ๊ณ ์คํ๋ฅผ ๋ ๋๋ค. ์๋ฌด์ผ๋ ์ผ์ด๋์ง ์๊ณ ์ ์ ๋ ์์ํ ๋ก์ ํ๋ฉด์ ๋ด ๋๋ค. (TypeScript๋ ๋ฌธ์์ด ์คํ๋ ๋ชป ์ก์์ค๋๋ค!) - ๋ถ๋นํธ๋ฉ(Fuzzy Matching): ๋ฐฐ์ด์ ํฌํจ ๊ด๊ณ ์๋ฆฌ๋ฅผ ๋ชจ๋ฅด๋ฉด, ์์ฒ ์ด์ฒ๋ผ ์ค์๋ก ์ฑ ์ ์ฒด์ ๋คํธ์ํฌ๋ฅผ ์ฃ๋ค ์ด๊ธฐํ์์ผ๋ฒ๋ฆฌ๋ ๋ฏธ์น ์ฑ๋ฅ ์ ํ ํญ๋ฐ์ด ๋ฐ์ํฉ๋๋ค.
์ด ์ฑํฐ์์๋ ์ ์ญ ์์ ๊ฐ์ฒด ๋ก ์ฟผ๋ฆฌ ํค๋ฅผ ์ค๊ณํ์ฌ ํด๋จผ ์๋ฌ๋ฅผ 0%๋ก ๋ฐ๋ฉธํ๋ ์ต๊ฐ์ ๊ตฌ์กฐ๋ฅผ ๋ฐฐ์๋๋ค.
1. ๋ฐฐ์ด ํค์ ํฌํจ ๊ด๊ณ (Fuzzy Matching) ํด๋ถ
TkDodo์ ์ํฐํด #8์ ๋ช
์๋ ๊ฐ์ฅ ์ค์ํ ๋ฌดํจํ ๋ฃฐ์
๋๋ค.
"์์์๋ถํฐ ๊ณ์ธต(๋ฐฐ์ด ์์)์ด ์ผ์นํ๋ฉด ๋ฌด์กฐ๊ฑด ํ๊ฒ ๋์์ด ๋๋ค!"
queryClient.invalidateQueries({ queryKey: ['todos'] }) ํ๋๋ฅผ ๋ฐ์ฌํ์ ๋,
- โ
['todos'](๋ง์ -> ๋ฌดํจํ) - โ
['todos', 'list', 'done'](todo๊ณ์ด ๋ง์ -> ๋ฌดํจํ ๋ฒ์ ์ ๋จ!) - โ
['todos', 'detail', 1](todo๊ณ์ด ๋ง์ -> ์ญ์ !) - โ
['users', 'todos'](ํ๋ฆผ, ๋งจ ์์ดusers์ -> ์ด๋ ค์ค)
๋ง์ฝ ์์ฒ ์ด๊ฐ "๋ฑ 'done' ์ํ์ธ ๋ฆฌ์คํธ๋ง" ์ฐ์ํ๊ฒ ์ง์ฐ๊ณ ์ถ์๋ค๋ฉด?
๋ฐ๋์ ์ ํํ๊ฒ queryClient.invalidateQueries({ queryKey: ['todos', 'list', 'done'] }) ๊น์ง ๊ตฌ์ฒด์ ์ผ๋ก ์ง์ด๋ฃ์ด์ผ๋ง ๋ค๋ฅธ ํ์ ์บ์๋ค(['todos', 'detail', 1])์ด ํญํ๋๋ ๊ฑธ ์ด๋ฆด ์ ์์ต๋๋ค.
2. ์ค์ ์ ์ฉ: Query Key Factory ์ํคํ ์ฒ
๋ฌธ์์ด ํ๋์ฝ๋ฉ์ ์์ฒ ๋ด์ํ๊ธฐ ์ํด ์ ์ญ ํฉํ ๋ฆฌ ํ์ผ(keys.ts ๋๋ queries.ts)์ ์์ํ๋ ๊ฐ์ฒด(Factory Pattern) ๋ฅผ ๋ง๋ญ๋๋ค.
// ๐ constants/queryKeys.ts
// ๐ ๋ชจ๋ ์ฟผ๋ฆฌ ํค๋ ์ด ๊ฐ์ฒด ํธ๋ฆฌ๋ฅผ ํตํด ์์ฑ๋๋ค! (๊ณต์ฅ)
export const todoKeys = {
// 1๋จ๊ณ: ๊ธฐ๋ณธ ๋๋ฉ์ธ ํค (์ ์ฒด ๋ฌดํจํ ๋ ์ธ ๊ฑฐ๋ํ ํญํ ์ค์์น)
all: ['todos'] as const,
// 2๋จ๊ณ: ๋ฆฌ์คํธ ๊ณ์ด ๋ฌถ์ ํค
lists: () => [...todoKeys.all, 'list'] as const,
// 3๋จ๊ณ: ๊ตฌ์ฒด์ ์ธ ๊ฒ์/ํํฐ ๋ฆฌ์คํธ (์์ ํค๋ค์ ์ ๋ถ ํ๊ณ ๋ด๋ ค์ด)
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
// 2๋จ๊ณ: ๋จ๊ฑด(๋ํ
์ผ) ๊ณ์ด ๋ฌถ์ ํค
details: () => [...todoKeys.all, 'detail'] as const,
// 3๋จ๊ณ: ํน์ 1๊ฑด ๋ํ
์ผ
detail: (id: number) => [...todoKeys.details(), id] as const,
};์ด๋ ๊ฒ ๊ณต์ฅ์ ์ฐจ๋ ค๋๋ฉด, ์ฌ์ฉํ๋ ํด๋ผ์ด์ธํธ(View, Hook) ์ชฝ ์ฝ๋๊ฐ 100% ์์ ํ๊ณ ์คํ ์๋ ์ฝ๋๋ก ์งํํฉ๋๋ค.
// 1๏ธโฃ ๋ฐ์ดํฐ ๊ฐ์ ธ์ฌ ๋
import { todoKeys } from '@/constants/queryKeys';
function TodoDetail({ id }: { id: number }) {
// ์คํ ๋ ํ๋ฅ 0%, id ํ๋ผ๋ฏธํฐ๋ฅผ ์ ๋ฃ์ผ๋ฉด TypeScript๊ฐ ์ฆ์ ์๋ํฐ์์ ์๋ฌ ๋ฟ์!
const { data } = useQuery({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodoDetail(id),
});
}// 2๏ธโฃ ์ญ์ /์์ ์๋ฃ ํ ๋ฌดํจํ ํ ๋ (์ ๋ฐ ํ๊ฒฉ)
const deleteMutation = useMutation({
mutationFn: deleteTodo,
onSuccess: () => {
// ๐ฅ "todos ๋ํ
์ผ ์บ์๋ ๋ค ์ด๋ ค๋๊ณ , ๋ฆฌ์คํธ ๊ณ์ด ๋ ๋๋ง๋ง ์น ๋ค์ ํด!"
queryClient.invalidateQueries({ queryKey: todoKeys.lists() });
}
})3. ๊ถ๊ทน์ Next Step: Query Options API (v5)
์ํคํ
์ฒ์ ์์ฌ์ด ๋ง์ ์๋์ด๋ค์ ํ ํต ๋ ๋์๊ฐ๋๋ค.
ํค(queryKey) ๋ฐ๋ก, ํ์
(queryFn) ๋ฐ๋ก ๊ด๋ฆฌํ๋ ๊ฒ๋ ๊ท์ฐฎ์ต๋๋ค. ์ด์ฐจํผ ['todos', id] ๋ฉด ๋ฌด์กฐ๊ฑด fetchTodoDetail(id) ํจ์๋ฅผ ํธ์ถํด์ผ ํ๋ ๊ฒ ํ์ฐ์ ์ธ ์ด๋ช
(Coupling)์ด์์์?
๊ทธ๋์ ์ต์ React Query v5 ์์๋ ์ด ๋์ ํ ๋ชธ์ผ๋ก ๋ฌถ์ด๋ฒ๋ฆฐ queryOptions ๊ธฐ๋ฐ์ ํฉํ ๋ฆฌ๊ฐ ํ์ค์ด ๋์์ต๋๋ค (TkDodo #24 ์ฐธ์กฐ). ์์ Basic 07๊ฐ์ ์ฌํ ๋ณต์ต์
๋๋ค.
import { queryOptions } from '@tanstack/react-query';
export const todoQueries = {
all: () => ['todos'],
// ํค + ํ์นญ ๋ก์ง + ๋ง๋ฃ๊ธฐ๊ฐ(staleTime) ๊น์ง ์์ํ ํ๋๋ก ๋ฌถ์ด๋ฒ๋ฆผ!
detail: (id: number) => queryOptions({
queryKey: [...todoQueries.all(), 'detail', id],
queryFn: () => axios.get(`/todos/${id}`).then(res => res.data as Todo),
staleTime: 5 * 60 * 1000,
}),
};// ๋ฐ์ดํฐ ์์ ๋ฌดํจํ ๋, ๊ทธ๋ฅ ํต์ง ์ต์
๊ฐ์ฒด๋ฅผ ๋ฐ์ฌํด ๋ฒ๋ฆฝ๋๋ค.
queryClient.invalidateQueries(todoQueries.detail(1));๋จ ํ ์ค์ ๋ฌธ์์ด ์คํ(String Typo)๋ ๋ฐ์ ๋ถ๊ฐ๋ฅํ, ๋นํ์๋ ํฐํ๋ ์ฅ๋ฒฝ์ด ํ๋ก ํธ์๋ ์ ์ฒด์ ๊น๋ฆฌ๊ฒ ๋๋ ์ ์ด์ฃ !
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
ํ... todoKeys.all, todoKeys.lists() ์ด๋ ๊ฒ ๋ถ๋ชจ ๋ฐฐ์ด ๊ป๋ฐ๊ธฐ ๊ทธ๋๋ก ํ์ฅ(...spread)ํด์ ๊ฐ์ ธ๋ค ๋ถ์ด๋๊น ๋ฌดํจํ์ํฌ ๋ ์ค์ฝํ(Scope) ๋ฑ๋ฑ ๋๋ ์ ์ ์ดํ๋ ๊ฑฐ ์์ ์ด๋ค ์ง์ง.
๐ก ์ค๋์ ๊ตํ: "ํ๋์ฝ๋ฉ ๋ฐฐ์ด
['foo', 'bar']์ ์ฐ๋ ์๋ ์๋ฌ ์์ต์ ๋ฐค์ ์์ฐ๋ฆฌ๋ผ! ๋ฌด์กฐ๊ฑด ๊ฐ์ฒด ๊ธฐ๋ฐ์ Query Key Factory ํจํด(๋๋ Query Options ํฉํ ๋ฆฌ)์ ๊ตฌ์ถํด์ ํ์ผ ํ๋์ ๋ชจ๋ ์บ์ ๊ธ๊ณ ์ด๋ฆ์ ํต์ ํ์."
์๊น ์คํ ๋์ ์ ์ฒด ๋ฌดํจํ ์ณค์ ๋ ์์(๋ฐฑ์๋) ๋ ๋ชจ๋ํฐ์ ํฐ์ ธ๋์ค๋ ์๋ฒ ๋ถํ ํธ๋ํฝ ๋ก๊ทธ๊ฐ ์์ง๋ ๋์์ ์ ํ๋ค ใ
ใ
ใ
ใ
... ๋ด์ผ ์ผ์ฐ ์ถ๊ทผํด์ ์ฑ ๊ณณ๊ณณ์ ์คํ ํ๋ฅ ์จ์ด์๋ [] ๋ฆฌํฐ๋ด ํค๋ค ์น ๋ค ์ฐพ์์ queryOptions ์ ๊ตญ์ผ๋ก ๋ํต์ผ ์์ผ๋์ผ์ง!!
๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
Q. ๋ค์๊ณผ ๊ฐ์ ๊ณ์ธต์ ๊ฐ์ง๋ ์ฟผ๋ฆฌ ํค ํฉํ ๋ฆฌ๋ฅผ ๊ตฌ์ฑํ์ต๋๋ค. ํน์ Mutation ์ดํ์ ์๋์ invalidateQueries ๋ฅผ ์คํํ์ ๋, ๋ฌดํจํ(์ญ์ ๋ฐ ๋ฆฌํจ์น ๋งํน)๋ฅผ 'ํผํ๊ฒ ๋๋(์์กดํ๋)' ์บ์๋ ์ด๋ ๊ฒ์ผ๊น์?
queryClient.invalidateQueries({ queryKey: ['orders', 'shipping'] });- A)
['orders', 'shipping', 'list', { status: 'done' }] - B)
['orders', 'shipping', 12345] - C)
['orders', 'payment', 'list']
โ
์ ๋ต: C
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: React Query์ ๋ฌดํจํ(invalidate) ๋ด๋ถ ์๊ณ ๋ฆฌ์ฆ์ "๋ฐฐ์ด์ ์์์๋ถํฐ ๊ฐ์ด ์๋ฒฝํ ์ผ์นํ๋(ํฌํจ ๊ด๊ณ์ธ) ๋ชจ๋ ํ์ ๋
ธ๋๋ฅผ ํญํ" ์ํค๋ ํผ์ง ๋งค์นญ(Fuzzy Matching)์ ์ฌ์ฉํฉ๋๋ค. ์ฐ๋ฆฌ๊ฐ ์
๋ ฅํ ์์ค๋
['orders', 'shipping']์ด๋ฏ๋ก, ์ ๋ ์๋ฆฌ๊ฐ ์ ๊ธ์๋ก ์์ํ๋ A๋ฒ๊ณผ B๋ฒ์ ๋ฌด์ํ ๋ง์ ์์ธ ํธ๋ฆฌ ๊ฐ์ง๋ค์ ๋ชจ์กฐ๋ฆฌ ํญํ๋ฉ๋๋ค. - ์ค๋ต ํผ๋๋ฐฑ: "์์ฒ ๋, C๋ฒ ์บ์๋
orders๊น์ง๋ ๊ฐ์ง๋ง ๋ ๋ฒ์งธ ๊ณ์ธต์์shipping์ด ์๋๋ผpayment๋ก ๊ฐ๋ผ์ก์ฃ ? ๊ทธ๋์ ๋ฐฉํญ ํ ํธ ์์ ์์ ํ๊ฒ ์ด์๋จ์ต๋๋ค. ์ด๋ ๊ฒ ๊ณ์ธต(Hierarchy) ์์ ์ค๊ณ๊ฐ ๋ฌดํจํ์ ํญ๋ฐ ๋ฐ๊ฒฝ(Blast Radius)์ ๊ฒฐ์ ํ๋ ๊ฒ๋๋ค!" - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ์์์๋ถํฐ ์คํ ๋ง&์์ ์ผ์น = ํฌํจ๋ ๋ชจ๋ ์์ ๋ฐ์ด. ์๋๊ฐ ๊ฐ์ง = ์์กด!