React-Query

yii·2025년 1월 21일

article

목록 보기
1/2
post-thumbnail

🔍 React Query를 공부해보고 싶었던 이유

  • React Query를 프로젝트에 벌써 2회 이상 사용하고 있는데 React Query의 기능을 잘 사용하고 있는게 맞는가 하는 의문이 들었습니다. 새로 시작하는 프로젝트에서 React Query를 사용하는 만큼 제대로 공부하고 현 프로젝트에 필요한 기능이 있을지 알아보고 싶었습니다.
  • 이전 프로젝트에서 제가 느낀 React Query의 장점은 API 통신에 있어 단순히 axios나 fetch를 사용할 때보다 확연히 코드 양이 줄고 로딩, 에러 처리가 쉽다는 점이었습니다. 또한 invalidateQueries 메서드를 통해 쿼리를 무효화하여 새로운 데이터를 업데이트하는 데 유용했습니다.

React Query란? 장점은?

  • React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리입니다.

장점

  • 기존에 직접 만들어 사용했던 기능들을 별도의 옵션으로 지원하여 복잡하고 이해할 수 없는 수많은 코드를 React-Query 로직을 통해 짧은 코드로 대체할 수 있게 되었습니다.
  • 별도의 설정 없이 즉시 사용 가능합니다.
  • 같은 데이터에 대한 여러 번의 요청이 있을 시 중복을 제거합니다.
  • 백그라운드에서 오래된 데이터를 업데이트 합니다.
  • 페이징 처리, 지연 로딩 데이터와 같은 성능을 최적화해줍니다.
  • 서버 쪽 데이터를 가비지 컬렉션을 이용하여 자동으로 메모리를 관리해줍니다.
  • 리렌더링을 고려해야할 때, React Query는 refetchOnWindowFocus를 수행하는데, 이는 사용자가 다른 브라우저 탭으로 이동했다가 다시 탭으로 돌아오면 자동으로 백그라운데 데이터가 업데이트 되는 것입니다. 캐시의 데이터와 동일하다면 컴포넌트는 리렌더링되지 않는 것입니다.

gcTime과 staleTime

  • staleTime: 쿼리가 신선한 상태에서 오래된 상태로 전환되는 기간입니다. 쿼리가 신선하다면 캐시에서만 데이트를 읽습니다. (네트워크 요청은 일어나지 않습니다.)
  • gcTime: 비활성 쿼리가 캐시에서 제거되기까지의 기간입니다. 쿼리를 사용하는 모든 컴포넌트가 언마운트 되었을 때 비활성 상태로 전환됩니다.

쿼리 키를 의존성 배열처럼

  • 리액트 쿼리는 쿼리키가 변경될 때마다 리페치를 트리거합니다. 값이 변경될 때 데이터를 가져오기 위해 queryFn과 함께 쿼리키를 전달합니다.
export const useTodosQuery = (state: State) =>
  useQuery({
    queryKey: ['todos', state],
    queryFn: () => fetchTodos(state),
  })

initialData 옵션

  • 아래 코드에서 쿼리키 ['todos', state] 는 상태 (all, open,done) 에 따라 변경됩니다. 상태가 변경될 때마다 새로운 캐시 항목이 생성되어 초기에 로딩 상태가 발생할 수 잇습니다. 이를 initialData 옵션을 사용하여 새 캐시 항목을 미리 채워두면 불필요한 로딩 상태를 줄여 사용자 경험을 개선할 수 있게 됩니다.
type State = 'all' | 'open' | 'done'
type Todo = {
  id: number
  state: State
}
type Todos = ReadonlyArray<Todo>

const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`)
  return response.data
}

export const useTodosQuery = (state: State) =>
  useQuery({
    queryKey: ['todos', state],
    queryFn: () => fetchTodos(state),
    initialData: () => {
      const allTodos = queryClient.getQueryData<Todos>([
        'todos',
        'all',
      ])
      const filteredData =
        allTodos?.filter((todo) => todo.state === state) ?? []

      return filteredData.length > 0 ? filteredData : undefined
    },
  })

서버 상태와 클라이언트 상태 분리하기

const App = () => {
  const { data } = useQuery({
    queryKey: ['key'],
    queryFn,
    staleTime: Infinity,
  })
 
  return data ? <MyForm initialData={data} /> : null
}
  • useQuery로 가져온 데이터를 로컬 상태(state)에 직접 저장하지 않고, React Query의 자동 백그라운드 업데이트 기능을 활용하는 것이 좋습니다. 폼의 초기값과 같이 한 번만 데이터가 필요한 경우 staleTime을 Infinity로 설정해 불필요한 리페치를 방지할 수 있는 것입니다. 서버 상태와 사용자의 편집 내용을 분리해 항상 최신 서버 데이터를 보면서 사용자의 로컬 변경사항도 유지할 수 있습니다. staleTime의 기본값은 0입니다!

enabled 옵션

  • enabled 옵션은 boolean 값이나 조건식을 받아 쿼리의 실행 여부를 결정합니다. 이 옵션으로 데이터 fetching을 제어할 수 있습니다.
  • 종속 쿼리 : 한 쿼리의 결과에 따라 다른 쿼리를 실행할 수 있습니다.
  • 쿼리 켜고 끄기: refetchInterval과 함께 사용하여 주기적인 데이터 폴링을 제어할 수 있습니다. 모달이 열려있을 때 백그라운드 업데이트를 일시 중지 하는 경우 사용 가능합니다.
  • 사용자 입력 후 쿼리 비활성화: 서버 데이터보다 우선순위가 높은 로컬 초안 값이 있는 경우, 쿼리를 비활성화하여 서버 데이터 fetching을 방지할 수 있습니다.

쿼리 캐시를 올바르게 사용하는 법

  • 쿼리 캐시는 서버 상태를 관리하기 위한 것입니다.
queryClient.setQueryData

을 통한 직접적인 캐시 조작은 제한적으로 사용해야합니다. React Query는 다양한 조건에서 데이터를 리페치하는데, 수동으로 조작한 캐시 데이터가 기존 데이터를 덮어쓰여질 수 있기 때문입니다.

  • 서버 응답을 기다리지 않고 UI를 즉시 업데이트 하는 낙관적 업데이트의 경우나, 백엔드에서 데이터를 받은 뒤 캐시를 업데이트 하는 경우에 캐시를 조작하는 것이 좋습니다.
  • 서버에서 가져온 데이터와 로컬에서 관리하는 상태를 명확히 구분하는 것이 좋습니다.

데이터 변환

  • REST를 사용할 때 백엔드가 반환하는 형식에 제약을 받는 경우가 발생하는데 데이터를 반환하는 엔드포인트가 있다면, 데이터를 예상대로 제공 받는 것이 중요합니다.
const fetchTodos = async (): Promise<Todos> => {
  const response = await axios.get('todos')
  const data: Todos = response.data
 
  return data.map((todo) => todo.name.toUpperCase())
}
 
export const useTodosQuery = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
 
  • QueryFn은 Promise를 반환하며 결과 데이터가 쿼리 캐시에 저장되는데, 이때 백엔드가 전달하는 구조 그대로 반환해야하는 것은 아닙니다. 반환하기 전 데이터를 다음과 같이 변환할 수 있습니다. 그러나 데이터를 가져올 때마다 변환 작업이 실행되고, 변환된 구조가 캐시에 저장돼 원래 구조에 접근할 수 없다는 단점이 있습니다.
const fetchTodos = async (): Promise<Todos> => {
  const response = await axios.get('todos')
  return response.data
}
 
export const useTodosQuery = () => {
  const queryInfo = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
 
  return {
    ...queryInfo,
    data: queryInfo.data?.map((todo) => todo.name.toUpperCase()),
  }
}
 
export const useTodosQuery = () => {
  const queryInfo = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos
  })
 
  return {
    ...queryInfo,
    // 🚨 이렇게 하지 마세요 - useMemo가 아무 역할도 하지 않습니다!
    data: React.useMemo(
      () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
      [queryInfo]
    ),
 
    // ✅ queryInfo.data로 올바르게 메모이제이션 합니다
    data: React.useMemo(
      () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
      [queryInfo.data]
    ),
  }
}
 
  • 혹은 사용자 정의 훅을 만들어 변환 작업을 수행할 수 있는데, 모든 렌더링마다 실행되므로 useMemo를 통해 최적화도 가능합니다. 이때 의존성을 가능한 좁게 설정하는 것이 중요합니다. queryInfo 객체는 매 렌더링마다 새로운 참조를 가지게 되기 때문에 useMemo가 매 렌더링마다 재실행되어 최적화 효과가 사라집니다. 반면 queryInfo.data의 경우 React Query는 구조적 공유(structural sharing)를 사용하여 data의 참조 동일성을 유지하기 때문에 데이터가 실제로 변경되지 않으면 동일한 참조를 유지해 불필요한 재계산을 방지할 수 있는 것입니다.
export const useTodosQuery = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.map((todo) => todo.name.toUpperCase()),
  })
  • 마지막으로 select 옵션을 사용 가능합니다. 셀렉터는 data가 존재할 때만 호출하기 때문에 undefined를 걱정하지 않아도 됩니다.
export const useTodosQuery = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    // ✅ useCallback으로 메모이제이션
    select: React.useCallback(
      (data: Todos) => data.map((todo) => todo.name.toUpperCase()),
      []
    ),
  })
 
  • useCallback을 사용해 메모이제이션 하는 것도 좋은 방법입니다.
export const useTodosQuery = (select) =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select,
  })

export const useTodosCount = () =>
  useTodosQuery((data) => data.length)

export const useTodo = (id) =>
  useTodosQuery((data) => data.find((todo) => todo.id === id))
  • 위 예시에서 useTodosCount는 할 일의 총 개수만 반환하고 useTodo는 특정 ID를 가진 할 일 항목만 반환합니다. 이처럼 select 옵션을 사용하면 컴포넌트가 필요한 데이터만 구독하게 됩니다. useTodosCount를 사용하는 컴포넌트는 할 일의 내용이 변경되어도 개수가 변하지 않으면 리렌더링되지 않는 것입니다. 필요에 따라 데이터를 선택하고 변환할 수 있기 때문에 유용하고, 큰 데이터셋에서 특정 부분만 필요할 때 사용하기 좋습니다.

참고 사이트

https://www.codingmax.net/courses/ko-react-query/section01/lec0003

0개의 댓글