React 기술면접 3-6. Tanstack Query, Custom hooks

강연주·2025년 1월 6일

🙋‍♀️ 기술면접

목록 보기
102/112

Tanstack Query

18, 19. React-query의 장점과 사용법을 이해하고 있다. Custom Hooks를 작성하여 재사용 가능한 로직을 구현할 수 있다.

  1. React-query의 장점과 사용법을 이해하고 있다.
    • 답변)
      • 의도:
        • 지원자가 React-query의 장점과 이를 사용하는 방법을 이해하고 있는지 평가.
      • 팁:
        • React-query의 주요 기능과 장점을 설명하세요.
        • React-query를 사용하는 방법을 설명하세요.
        • React-query를 활용하는 예제를 떠올려 보세요.
      • 모범답안:
        • React-query는 서버 상태를 관리하고 비동기 데이터 페칭을 쉽게 해주는 라이브러리입니다.
        • React-query는 자동으로 데이터 캐싱, 리패칭, 에러 핸들링 등을 처리하여 개발자의 부담을 줄입니다.
        • useQuery, useMutation 훅을 사용하여 데이터를 가져오고 변이시킬 수 있습니다.

Tanstack Query의 주요 문법, 패턴

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}`,
    }));
  },
});

Tanstack Query 기능

기본적인 데이터 페칭과 캐싱

🖥️ 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>
  );
}

Tanstack Query Custom Hooks 패턴 예시

기본적인 데이터 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>
  );
}

➡️ 비즈니스 로직을 캡슐화해 재사용 가능, 컴포넌트 관심사를 분리 용이.
각 훅이 특정 기능에 집중하므로 코드의 유지보수성 제고.

        
profile
아무튼, 개발자

0개의 댓글