๐ก 07. ์ปค์คํ ํ (Custom Hooks)์ผ๋ก Query ์ถ์ํํ๊ธฐ
๐ ๊ฐ์
์ปดํฌ๋ํธ ๋ด๋ถ์ ์ฐ์ฌํ useQuery ๋ก์ง์ ์ปค์คํ ํ ๊ณผ queryOptions API๋ก ๋ถ๋ฆฌํ์ฌ ์ฌ์ฌ์ฉ์ฑ๊ณผ ํ์ ์์ ์ฑ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "์ฝ๋๊ฐ ๋๋ฌด ๊ธธ๊ณ ๋ฑ๋ฑํด์!"
- ๐ค ์ ์์์ผ ํ๋๊ฐ: ๋น์ฆ๋์ค ๋ก์ง์ ๊ฒฉ๋ฆฌ
- 1. ์ปค์คํ ํ ์ถ์ํ์ 3๋จ๊ณ ์ค๊ณ
- 2. ์งํ์ ๋:
queryOptionsAPI (v5์ ๋น๊ธฐ) - ๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
"์์ฒ ๋, ๋๊ฐ์
useQuery์ค์ ์ ์ปดํฌ๋ํธ 5๊ตฐ๋ฐ์ ๋ค ๋ณต๋ถํด ๋์ผ์ จ๋ค์. ๋์ค์ API ์ฃผ์ ๋ฐ๋๋ฉด 5๊ตฐ๋ฐ ๋ค ์ฐพ์์ ๊ณ ์น์ค ๊ฑด๊ฐ์?"
โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "์ฝ๋๊ฐ ๋๋ฌด ๊ธธ๊ณ ๋ฑ๋ฑํด์!"
(์์์ผ ์คํ, ์ฝ๋๋ฅผ ์คํฌ๋กคํ๋ค ์๊ฐ๋ฝ์ ์ฅ๊ฐ ๋ ์์ฒ )
๐ฃ ์์ฒ : ์ ๋ฆฌ๋ ๋... ์ ํฌ ์ปค๋ฎค๋ํฐ ์ฑ ์ฌ๊ธฐ์ ๊ธฐ์ '์ธ๊ธฐ ๊ฒ์๊ธ' ๋ชฉ๋ก์ ๋์์ผ ํ๊ฑฐ๋ ์? ๊ทธ๋์ ๋ฉ์ธ ํ์ด์ง, ๋ง์ด ํ์ด์ง, ๊ฒ์ ํ์ด์ง๋ง๋ค useQuery ์จ์ ์ฟผ๋ฆฌ ํค ๋ฃ๊ณ , queryFn ๋ฃ๊ณ , staleTime 5๋ถ์ผ๋ก ๋ง์ถ๊ณ ... ๋๊ฐ์ ์ฝ๋๋ฅผ ๊ณ์ ์น๋ค ๋ณด๋๊น ํ์ผ ํ๋๋น ์ฝ๋๊ฐ ๋ง 200์ค์ฉ ๋์ด๊ฐ์ ใ
ใ
๐ฆ ์ํธ: ์์ฒ ๋, UI๋ฅผ ๊ทธ๋ฆฌ๋ ์ปดํฌ๋ํธ(View) ์์ ๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ(Logic)์ ์์ผ๋ก ๋๋ ค ๋ฐ์ผ๋๊น ๋ฑ๋ฑํด์ง๋ ๊ฒ๋๋ค. React Query์ ๋ชจ๋ ํธ์ถ์ ๋ฌด์กฐ๊ฑด ์ปค์คํ
ํ
(Custom Hooks) ์ผ๋ก ์ถ์ํํด์ ๋นผ๋ด๋ ๊ฒ์ด 5๋
์ฐจ ์ค๋ฌด์ ๊ตญ๋ฃฐ์
๋๋ค. ๊ฒ๋ค๊ฐ TypeScript ํ์
์ถ๋ก ๊น์ง ์น์ผ๋ฉด ์๋ฒฝํด์ง์ฃ .
๐ค ์ ์์์ผ ํ๋๊ฐ: ๋น์ฆ๋์ค ๋ก์ง์ ๊ฒฉ๋ฆฌ
๋ง์ฝ ์ปดํฌ๋ํธ ์์์ ์์์ ์ธ useQuery๋ฅผ ๊ทธ๋๋ก ์ฐ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์น๋ช
์ ์ธ ๋จ์ ์ด ์๊น๋๋ค.
- ์ค๋ณต์ ์ ์ฃผ: ์ฟผ๋ฆฌ ํค ๋ฌธ์์ด ์คํ,
staleTime์ค์ ๋๋ฝ ๋ฑ ์ฌ๊ธฐ์ ๊ธฐ์ ํด๋จผ ์๋ฌ๊ฐ ํฐ์ง๋๋ค. - ํ์
์ ๋ช: ์๋ฒ์์ ๋ด๋ ค์ค๋ Response ํ์
์ ์ปดํฌ๋ํธ๋ง๋ค ๋งค๋ฒ ์ ๋ค๋ฆญ(
<T>)์ผ๋ก ๋ฌ์์ค์ผ ํฉ๋๋ค. - ๊ฐ๋ ์ฑ ํ๊ดด: UI ๋ ๋๋ง ์ฝ๋๋ณด๋ค ๋น๋๊ธฐ ์ํ ๋ฝ์๋ด๋ ์ฝ๋๊ฐ ๋ ๊ธธ์ด์ง๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ์ฐ๋ฆฌ๋ ์ฟผ๋ฆฌ ํธ์ถ๋ถ๋ฅผ useTodos, useUser ๊ฐ์ ์ปค์คํ
ํ
์ผ๋ก ์์๊ฒ ํฌ์ฅํ์ฌ ์ปดํฌ๋ํธ๊ฐ "์ด๋ป๊ฒ(How) ๊ฐ์ ธ์ค๋์ง ์ ํ์ ์์ด, ๋ฌด์(What)๋ง ์ฐ๋๋ก" ์๋ฒฝํ ๊ฒฉ๋ฆฌํด์ผ ํฉ๋๋ค.
1. ์ปค์คํ ํ ์ถ์ํ์ 3๋จ๊ณ ์ค๊ณ
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ปค์คํ ํ ๋ถ๋ฆฌ ๋ฐฉ์์ TypeScript ๊ท๊ฒฉ์ ๋ง์ถฐ ์ดํด๋ด ์๋ค.
โ ๋จ๊ณ 0: ๋ฑ๋ฑํ ์ปดํฌ๋ํธ (Before)
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
// ์ปดํฌ๋ํธ๊ฐ ๋๋ฌด ๋ง์ ๊ฑธ ์๊ณ ์๋ค!
function Dashboard() {
const { data, isLoading } = useQuery<Todo[], Error>({
queryKey: ['todos', 'done'],
queryFn: async () => {
const res = await axios.get('/todos?status=done');
return res.data;
},
staleTime: 5 * 60 * 1000, // 5๋ถ
});
return <div>...</div>;
}โ ๋จ๊ณ 1: ํ ์ผ๋ก ๋ถ๋ฆฌ (After)
์๋ก์ด ํ์ผ hooks/queries/useTodos.ts ๋ฅผ ๋ง๋ค๊ณ ๋ก์ง์ ๋ชฝ๋
๋ชฐ์๋ฃ์ต๋๋ค. ์ปดํฌ๋ํธ๋ ์ค์ง ์ด ํ
๋ง ํธ์ถํ๋ฉด ๋ฉ๋๋ค.
// ๐ hooks/queries/useTodos.ts
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
// 1. DTO ํ์
์ ์ธ (์ด ํ์ผ ์์์๋ง ์์ง๋ ๋๊ฒ ๊ด๋ฆฌ)
export type Todo = { id: number; title: string; status: 'open' | 'done' };
// 2. ์์ ํ์นญ ํจ์ (์ถํ ํ
์คํ
์ด๋ ์๋ฒ ์ฌ์ด๋์์ ์ฌ์ฌ์ฉ ์ฉ์ด)
const fetchTodos = async (status: string): Promise<Todo[]> => {
const { data } = await axios.get(`/todos?status=${status}`);
return data;
};
// 3. โจ ๋๋์ด ์ปค์คํ
ํ
(ํ์
์ถ๋ก ์ด ์๋ฒฝํ๊ฒ ํ๋ฌ๊ฐ)
export const useTodos = (status: string) => {
return useQuery({
queryKey: ['todos', status],
queryFn: () => fetchTodos(status),
staleTime: 5 * 60 * 1000,
});
};// ๐จ View ์ปดํฌ๋ํธ๋ ๊ทน๋๋ก ๊ฐ๋ฒผ์์ง๋ค!
import { useTodos } from '@/hooks/queries/useTodos';
function Dashboard() {
// Query Key๊ฐ ๋ญ์ง, staleTime์ด ์ผ๋ง์ง ์ปดํฌ๋ํธ๋ ๋ชจ๋ฅธ๋ค. ๊ทธ์ ๊บผ๋ด ์ธ ๋ฟ!
const { data: todos, isLoading } = useTodos('done');
return <div>...</div>;
}2. ์งํ์ ๋: queryOptions API (v5์ ๋น๊ธฐ)
๐ฃ ์์ฒ : ์ค, ์ฝ๋๊ฐ ์ง์ง ๋ฐํ ๋ง ๋ฌ๋ค์! ๊ทผ๋ฐ ๋ฆฌ๋ ๋, ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR)์ด๋ ํ๋ฆฌํ์นญ(Prefetching) ํ ๋ ์๋ฒ ์ฝ๋์์ useQuery ํ
์ ๋ชฌ ์ธ ํ
๋ฐ, ๊ทธ๋ผ ๊ทธ๋ ์ฟผ๋ฆฌ ํค(['todos', status])๋ฅผ ๋ ์์ผ๋ก ๋ฌธ์์ด ์ณ์ผ ํ๋ ๊ฑฐ ์๋๊ฐ์?
๐ฆ ์ํธ: ํต์ฌ์ ์ฐ๋ ๊ตฐ์! ํ
๋ด๋ถ๋ก ํค๋ฅผ ์์ ํ ์จ๊ฒจ๋ฒ๋ฆฌ๋ฉด ์ปดํฌ๋ํธ ๋ฐ(๋ฃจํฐ ์ถฉ๋, SSR ํ๋ฆฌํ์นญ, QueryCache ์ง์ ์ ์ด)์์ ๊ทธ ํค์ ์ ๊ทผํ๊ธฐ ํ๋ค์ด์ง๋๋ค. ์ด๋ฅผ ์๋ฒฝํ๊ฒ ํด๊ฒฐํ๊ธฐ ์ํด React Query v5์์ ์ถ์๋ queryOptions ๋ผ๋ ํจ์๋ฅผ ์จ์ผ ํฉ๋๋ค.
queryOptions๋ ์คํํ์ง ์๊ณ '์ต์
๋ฉ์ด๋ฆฌ(๊ฐ์ฒด)' ๋ง ๋ง๋ค์ด์ฃผ๋ ํฉํ ๋ฆฌ ํจ์ ์
๋๋ค.
// ๐ queries/todoOptions.ts
import { queryOptions } from '@tanstack/react-query';
// ๐ Option ํฉํ ๋ฆฌ ์์ฑ (ํ์
์คํฌ๋ฆฝํธ ์์ ์ง์)
export const todoOptions = {
all: () => queryOptions({
queryKey: ['todos'],
queryFn: () => axios.get('/todos').then(res => res.data as Todo[]),
}),
detail: (status: string) => queryOptions({
queryKey: ['todos', status],
queryFn: () => axios.get(`/todos?status=${status}`).then(res => res.data as Todo[]),
staleTime: 5 * 60 * 1000,
}),
};์ด ์ต์ ๊ฐ์ฒด๊ฐ ์์ผ๋ฉด ํด๋ผ์ด์ธํธ ํ ์ด๋ , ์๋ฒ ๋ผ์ฐํฐ ๋ ์ธ์ ์ด๋์๋ ๋ ๊ณ ๋ธ๋ก์ฒ๋ผ ๋ผ์ ์ธ ์ ์์ต๋๋ค!
// 1๏ธโฃ ์ผ๋ฐ ์ปดํฌ๋ํธ(Client)์์ ์ธ ๋
function Dashboard() {
// Option ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก useQuery์ ์ ๊ฐํด์ ์ง์ด๋ฃ์
const { data } = useQuery(todoOptions.detail('done'));
}
// 2๏ธโฃ ๋ก๋(SSR API)๋ ์๋ฒ ์ก์
์์ ํ๋ฆฌํ์นญ ํ ๋
async function prefetchDashboard() {
const queryClient = new QueryClient();
// ๋์ผํ ์ต์
๊ฐ์ฒด ์ฌ์ฌ์ฉ! ํค ์คํ๊ฐ ๋ ํ๋ฅ 0%
await queryClient.prefetchQuery(todoOptions.detail('done'));
}๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์... ๋ ์ค๋ useTodos ํ์ผ ํ๋ ๋ง๋ค์ด๋๊ณ queryOptions ์์์ ๊น์๋ ๋ค์, ๋ง์ดํ์ด์ง๋ ๋์๋ณด๋์์ useQuery(todoOptions.xxx()) ๋ก ์ซ ๊ฐ์๋ผ์ ๋ค.
์ฝ๋ ์๋ฐฑ ์ค์ด ๊ทธ๋ฅ ์ฆ๋ฐํ๋ ๋ง์ ;;
๐ก ์ค๋์ ๊ตํ: "UI ํ์ผ ์์์ ์ง์
useQueryํจํด์ ๋ฌด๋ฐฉ๋นํ๊ฒ ๋ณต๋ถํ๋ ๊ฑด ์ํฐํจํด์ด๋ค. ์ฟผ๋ฆฌ ํค์ ํ์นญ ๋ก์ง์ ์ปค์คํ ํ ์ด๋queryOptions๊ฐ์ฒด๋ก ์๋ฒฝํ ๊ฒฉ๋ฆฌํด์, ์ปดํฌ๋ํธ๊ฐ '๋ฐ์ดํฐ ์ค'๋ผ๊ณ ์๋ฆฌ๋ง ์น๊ฒ ๋ง๋ค์!"
๊ทธ๋ฆฌ๊ณ TypeScript ์ผ์ ๋ ์ ์ผ ์ข์ ์ ! todoOptions.detail(123) ์น๋๊น ๋นจ๊ฐ ์ค ๋จ๋ฉด์ ์ํ(status)๋ ๋ฌธ์์ด์ด์ผ, ๋ฐ๋ณด์ผ! ๋ผ๊ณ ํ๋ด์ฃผ๋๋ผ. ํด... TS ์ ์ผ์ผ๋ฉด ๋ฐฑ์๋ํํ
์ซ์ 123 ๋๊ฒผ๋ค๊ฐ 400 ์๋ฌ ๋จ๊ณ ์ด๋ฆฌ๋ฅ์ ํ ๋ป. ์ค๋ ํด๊ทผ๊ธธ ๋ฐ๊ฑธ์ ๊ฐ๊ฐ๋ณ๋ค ํซใ
.
๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
Q. ๊ธฐ์กด์๋ ๋ฐ์ดํฐ ๋ณํ ์์
์ ์ปดํฌ๋ํธ ์์์ data.filter(...)๋ data.map(...)์ผ๋ก ์ฒ๋ฆฌํ์ต๋๋ค. ์ปค์คํ
ํ
์ ๋ถ๋ฆฌํ ์ดํ, ์๋ณธ DB ๋ฐ์ดํฐ๋ฅผ ํ๋ก ํธ์๋ UI ์
๋ง์ ๋ง๊ฒ ๊ฐ๊ณต(Transformation)ํ๊ณ ์ถ์ ๋ ๊ฐ์ฅ ๊ถ์ฅ๋๋ React Query์ ๋ด์ฅ ์ต์
์ ๋ฌด์์
๋๊น?
- A)
queryFn์์์axios๋ก ๋ฐ์์จ ์งํ์filter๋ฅผ ๋๋ฆฐ๋ค. - B) ์ปค์คํ
ํ
๋ด๋ถ์์
useQuery์ค์ ์คselect์ต์ ์ ์ฌ์ฉํ์ฌ ๊ฐ๊ณตํ๋ค. - C) ์ปค์คํ
ํ
์์ ๋ฐํํ
data๋ฅผ, ์ฌ์ฉํ UI ์ปดํฌ๋ํธ์์useMemo๋ก ๋ค์ ๊ฐ์ผ๋ค.
โ
์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: TkDodo ์ํฐํด #2์ ๋ฐ๋ฅด๋ฉด, React Query์
select์ต์ ์ "์๋ณธ ๋ฐ์ดํฐ(์บ์)๋ ํผ์ํ์ง ์๊ณ ๋ ๋ ์ฑ, ํน์ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ๋ ํ ๋๋ง ๋ด๊ฐ ์ํ๋ ์กฐ๊ฐ์ผ๋ก ์๋ผ๋ด์ด(Memozation) ๋์ ธ์ฃผ๋" ์ต๊ฐ์ ๋ฐ์ดํฐ ๋ณํ ๋๊ตฌ์ ๋๋ค.select์ต์ ์ ๋ด๋ถ์ ์ผ๋ก ๊ฒฐ๊ณผ๊ฐ์ ์บ์ฑํ๋ฏ๋ก ๋ถํ์ํ ์ฐ์ฐ ๋ญ๋น๋ฅผ ์๋ฒฝํ ์ฐจ๋จํฉ๋๋ค. - ์ค๋ต ํผ๋๋ฐฑ: "์์ฒ ๋, A๋ฒ์ฒ๋ผ
queryFn์์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์๋ฒ๋ฆฌ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ๋ค๋ฅธ ํ์ด์ง์์ ์๋ณธ ์ ์ฒด ๋ชฉ๋ก์ ๋ฌ๋ผ๊ณ ํ์ ๋ ์บ์ํต์ ์ด๋ฏธ ๋๋์ง๋นํ ์กฐ๊ฐ๋ฐ์ ์ ๋จ์์๊ฒ ๋ฉ๋๋ค! C๋ฒ์ฒ๋ผ ๋งค๋ฒ ๋ทฐ ์ปดํฌ๋ํธ๊ฐ ์ง์ด์ง๊ฒ ํ์ง ๋ง๊ณ , ๊น๋ํ๊ฒuseQuery์select์๊ฒ ์์ํ์ธ์." - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ์๋ณธ ํผ์ ์๋ ์ฐ์ํ ๋ฐ์ดํฐ ๊ฐ๊ณต์ ๋ฌด์กฐ๊ฑด
select์ต์ !