1. 기본 Hooks와 주요 옵션들
🖥️ typescript
// useQuery 기본 구조
const { data, isLoading, error } = useQuery({
queryKey: ['unique', 'key'], // 쿼리를 식별하는 고유 키
queryFn: async () => { // 데이터를 가져오는 함수
// 비동기 로직
},
staleTime: 1000 * 60, // 데이터가 'fresh'상태로 유지되는 시간
cacheTime: 1000 * 60 * 5, // 데이터가 캐시에 유지되는 시간
retry: 3, // 실패 시 재시도 횟수
enabled: boolean, // 쿼리 활성화 여부
refetchInterval: 1000, // 자동 리페칭 간격
select: (data) => data, // 데이터 변환 함수
});
2. 주요 반환값 설명
🖥️ typescript
const {
data, // 가져온 데이터
isLoading, // 최초 로딩 상태
isFetching, // 모든 로딩 상태 (백그라운드 포함)
error, // 에러 객체
status, // 'loading' | 'error' | 'success'
refetch, // 수동으로 리페칭하는 함수
} = useQuery({ /* ... */ });
3. Mutation 기본 구조
🖥️ typescript
const mutation = useMutation({
mutationFn: (variables) => {
// 데이터 변경 로직
},
onSuccess: (data, variables) => {
// 성공 시 실행될 콜백
},
onError: (error, variables) => {
// 에러 발생 시 실행될 콜백
},
onSettled: (data, error, variables) => {
// 성공/실패 상관없이 완료 시 실행될 콜백
},
});
// mutation 실행
mutation.mutate(variables);
// 또는 비동기로 실행
mutation.mutateAsync(variables);
4. QueryClient 주요 메서드
🖥️ typescript
const queryClient = useQueryClient();
// 쿼리 무효화
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true, // 정확한 키만 무효화
});
// 쿼리 데이터 직접 설정
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// 쿼리 데이터 가져오기
const data = queryClient.getQueryData(['todos']);
// 쿼리 리페칭
queryClient.refetchQueries({
queryKey: ['todos'],
exact: true,
});
5. 인피니트 쿼리 구조
🖥️ typescript
const {
data, // { pages: [], pageParams: [] }
fetchNextPage, // 다음 페이지 로드 함수
hasNextPage, // 다음 페이지 존재 여부
isFetchingNextPage,// 다음 페이지 로딩 상태
} = useInfiniteQuery({
queryKey: ['infinite'],
queryFn: ({ pageParam = 0 }) => fetchPage(pageParam),
getNextPageParam: (lastPage, allPages) => {
// 다음 페이지 파라미터 계산 로직
return lastPage.nextCursor;
},
});
6. TypeScript 관련 주요 타입들
🖥️ typescript
// 기본 쿼리 타입
interface QueryResult<TData> {
data: TData | undefined;
isLoading: boolean;
error: Error | null;
}
// 쿼리 함수 타입
type QueryFunction<TData> = (
context: QueryFunctionContext
) => Promise<TData>;
// 뮤테이션 타입
interface MutationResult<TData> {
data: TData | undefined;
isLoading: boolean;
error: Error | null;
reset: () => void;
}
7. 주요 에러 처리 패턴
🖥️ typescript
// 전역 에러 처리
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error) => {
// 글로벌 에러 핸들링
},
},
},
});
// 개별 쿼리 에러 처리
const { error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
onError: (error) => {
// 개별 에러 핸들링
},
});
8. 캐시 조작 패턴
typescript
// 낙관적 업데이트
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 이전 쿼리 취소
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 이전 값 저장
const previousTodos = queryClient.getQueryData(['todos']);
// 낙관적 업데이트
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// 롤백을 위한 컨텍스트 반환
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 에러시 롤백
queryClient.setQueryData(['todos'], context.previousTodos);
},
});
9. 데이터 변환 패턴
🖥️ typescript
// select 옵션을 통한 데이터 변환
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
return users.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`,
}));
},
});
기본적인 데이터 페칭과 캐싱
🖥️ typescript
import { useQuery } from '@tanstack/react-query';
// 기본적인 데이터 페칭
function TodoList() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('https://api.example.com/todos');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
자동 리페칭과 백그라운드 업데이트
🖥️ typescript
import { useQuery } from '@tanstack/react-query';
function UserProfile() {
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserData(userId),
// 자동 리페칭 설정
refetchInterval: 5000, // 5초마다 리페칭
staleTime: 1000 * 60, // 1분 동안 데이터를 fresh로 취급
cacheTime: 1000 * 60 * 5 // 5분 동안 캐시 유지
});
return <div>{data?.name}</div>;
}
데이터 변이(Mutation) 처리
🖥️ typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('https://api.example.com/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
});
},
// 변이 성공 시 캐시 업데이트
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
// 또는 직접 캐시 업데이트
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
}
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ title: 'New Todo' });
};
return <button onClick={handleSubmit}>Add Todo</button>;
}
에러 핸들링
🖥️ typescript
function TodoList() {
const { data, error, isError, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3, // 실패 시 3번 재시도
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error) => {
console.error('Failed to fetch todos:', error);
}
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
병렬 쿼리 처리
🖥️ typescript
function Dashboard() {
// 여러 쿼리를 동시에 실행
const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
const user = useQuery({ queryKey: ['user'], queryFn: fetchUser });
const notifications = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications
});
if (todos.isLoading || user.isLoading || notifications.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<UserInfo data={user.data} />
<TodoList data={todos.data} />
<NotificationList data={notifications.data} />
</div>
);
}
의존적 쿼리(Dependent Queries)
🖥️ typescript
function UserPosts() {
// 유저 정보를 먼저 가져옴
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
// 유저 정보가 있을 때만 포스트를 가져옴
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user.id),
// user가 있을 때만 쿼리 실행
enabled: !!user,
});
return (
<div>
<h1>{user?.name}'s Posts</h1>
{posts?.map(post => (
<Post key={post.id} post={post} />
))}
</div>
);
}
기본적인 데이터 fetch 커스텀 훅
🖥️ typescript
// hooks/useUsers.ts
interface User {
id: number;
name: string;
email: string;
}
export const useUsers = () => {
return useQuery<User[]>({
queryKey: ['users'],
queryFn: async () => {
const response = await axios.get('/api/users');
return response.data;
}
});
};
// 사용 예시
function UserList() {
const { data: users, isLoading } = useUsers();
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
무한 스크롤/페이지네이션 커스텀 훅
🖥️ typescript
// hooks/useInfiniteUsers.ts
export const useInfiniteUsers = () => {
return useInfiniteQuery({
queryKey: ['users', 'infinite'],
queryFn: async ({ pageParam = 1 }) => {
const response = await axios.get(`/api/users?page=${pageParam}`);
return response.data;
},
getNextPageParam: (lastPage, pages) => {
if (lastPage.hasMore) return pages.length + 1;
return undefined;
},
});
};
// 사용 예시
function InfiniteUserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteUsers();
return (
<div>
{data?.pages.map((page, i) => (
<React.Fragment key={i}>
{page.users.map(user => (
<UserItem key={user.id} user={user} />
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
);
}
CRUD 작업 커스텀 훅
🖥️ typescript
// hooks/useTodoOperations.ts
export const useTodoOperations = () => {
const queryClient = useQueryClient();
const addTodo = useMutation({
mutationFn: (newTodo: Todo) =>
axios.post('/api/todos', newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const updateTodo = useMutation({
mutationFn: (updatedTodo: Todo) =>
axios.put(`/api/todos/${updatedTodo.id}`, updatedTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const deleteTodo = useMutation({
mutationFn: (todoId: number) =>
axios.delete(`/api/todos/${todoId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return {
addTodo,
updateTodo,
deleteTodo
};
};
// 사용 예시
function TodoManager() {
const { addTodo, updateTodo, deleteTodo } = useTodoOperations();
return (
<div>
<button onClick={() => addTodo.mutate(newTodo)}>
Add Todo
</button>
{/* ... */}
</div>
);
}
실시간 데이터 업데이트 커스텀 훅
🖥️ typescript
// hooks/useRealtimeData.ts
export const useRealtimeData = (resourceId: string) => {
return useQuery({
queryKey: ['realtime', resourceId],
queryFn: () => fetchRealtimeData(resourceId),
refetchInterval: 1000, // 1초마다 리페칭
select: (data) => transformData(data), // 데이터 변환
});
};
// 사용 예시
function RealtimeChart({ resourceId }) {
const { data, isLoading } = useRealtimeData(resourceId);
if (isLoading) return <div>Loading...</div>;
return <LineChart data={data} />;
}
검색 기능 커스텀 훅
🖥️ typescript
// hooks/useSearch.ts
export const useSearch = (initialQuery = '') => {
const [searchQuery, setSearchQuery] = useState(initialQuery);
const searchResults = useQuery({
queryKey: ['search', searchQuery],
queryFn: () => searchApi(searchQuery),
enabled: searchQuery.length > 0,
staleTime: 1000 * 60 * 5, // 5분
});
const debouncedSearch = useCallback(
debounce((query: string) => {
setSearchQuery(query);
}, 500),
[]
);
return {
searchResults,
debouncedSearch,
searchQuery,
};
};
// 사용 예시
function SearchComponent() {
const { searchResults, debouncedSearch } = useSearch();
return (
<div>
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
{searchResults.data?.map(result => (
<SearchResult key={result.id} result={result} />
))}
</div>
);
}
➡️ 비즈니스 로직을 캡슐화해 재사용 가능, 컴포넌트 관심사를 분리 용이.
각 훅이 특정 기능에 집중하므로 코드의 유지보수성 제고.