본문 바로가기

FrontEnd

리액트 쿼리 : 서버 응답 변환 위치

반응형

원문 : https://tkdodo.eu/blog/react-query-data-transformations

 

어떤 방법이 가장 좋을까?

그때그때 다르다.
- 모든 개발자, 항상

0. GraphQL 사용과 백엔드 개발자의 지원

우리가 할 일이 없음. 하지만 항상 가능한 것이 아님.


1. 쿼리 함수 안에서(queryFn)

queryFn은 useQuery에 전달하는 함수입니다.
Promise를 반환할 것으로 예상하고 결과 데이터는 쿼리 캐시에 저장됩니다.
이 함수가 항상  백엔드가 제공하는 구조로 데이터를 반환할 필요는 없습니다.
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(['todos'], fetchTodos)​
프론트엔드에서 이 데이터를 "백엔드에서 이렇게 같이 가져온 것처럼" 작업할 수 있습니다.
즉, 원래 구조에 액세스할 수 없습니다. react-query-devtools를 보면 변형된 구조를 볼 수 있습니다.
 
네트워크 트레이스를 보면 원래 구조를 볼 수 있습니다. 헷갈릴 수 있으니 참고하세요.
🟢   코로케이션 측면에서 "백엔드와 매우 가까움"
🟡   변형된 구조가 캐시에 저장되므로 원래 구조에 액세스할 수 없습니다.
🔴   데이터를 가져올 때마다 실행
🔴   자유롭게 수정할 수 없는 공유 API 레이어가 있는 경우 불가능

2. 렌더 함수 안에서 (render function)

커스텀 훅을 사용하면 쉽게 변환을 수행할 수 있습니다.

변환 연산이 항상 수행되므로, 연산을 메모할 수 있는데,

data는 참조적으로 안정적이지만, queryInfo는 아니므로 조심하세요

const fetchTodos = async (): Promise<Todos> => {
  const response = await axios.get('todos')
  return response.data
}

export const useTodosQuery = () => {
  const queryInfo = useQuery(['todos'], fetchTodos)

  return {
    ...queryInfo,
    // 🚨 don't do this - the useMemo does nothing at all here!
    data: React.useMemo(
      () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
      [queryInfo]
    ),

    // ✅ correctly memoizes by queryInfo.data
    data: React.useMemo(
      () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
      [queryInfo.data]
    ),
  }
}

🟢   useMemo를 통해 최적화 가능

🟡   정확한 구조는 devtools에서 검사할 수 없습니다.

🔴   좀 더 복잡한 구문

🔴   데이터는 undefined일 수 있음


3. Select 옵션 사용

v3에는 데이터 변환에도 사용할 수 있는 빌트인 셀렉터가 도입되었습니다.
export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {
    select: (data) => data.map((todo) => todo.name.toUpperCase()),
  })
셀렉터는 데이터가 존재하는 경우에만 호출되므로 여기에서 undefined에 대해 신경 쓸 필요가 없습니다.
변환 비용이 많이 든다면 useCallback을 사용하거나 안정적인 함수 참조로 추출하여 메모화할 수 있습니다.
const transformTodoNames = (data: Todos) =>
  data.map((todo) => todo.name.toUpperCase())

export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {
    // ✅ uses a stable function reference
    select: transformTodoNames,
  })

export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {
    // ✅ memoizes with useCallback
    select: React.useCallback(
      (data: Todos) => data.map((todo) => todo.name.toUpperCase()),
      []
    ),
  })​

또한 select 옵션을 사용하여 데이터의 일부만 구독할 수도 있습니다.

HOC와 함께 useSelector 같은 API를 만들 수 있습니다.

export const useTodosQuery = (select) =>
  useQuery(['todos'], fetchTodos, { select })

export const useTodosCount = () => useTodosQuery((data) => data.length)
export const useTodo = (id) =>
  useTodosQuery((data) => data.find((todo) => todo.id === id))
🟢   최고의 최적화
🟢   부분 구독 허용
🟡   관찰자마다 구조가 다를 수 있음
🟡   구조적 공유(structural sharing)는 두 번 수행됩니다.
  • https://itchallenger.tistory.com/577 참고
    • queryFn에서 반환된 결과에 변경된 사항이 있는지 확인 후
    • 셀렉터 함수의 결과에 대해 한 번 더 수행됩니다.

 

반응형