TanStackQuery 공식 홈페이지 따라하기

TanStackQuery (19) - 추가적인 정리(1)

vitamin3000 2025. 2. 3. 12:53

 

이번 포스트에서는 박영웅 강사님의 https://www.heropy.dev/p/HZaKIE를 참고하여 정리된 내용을 실제로 실행시켜보고자 한다.

 

 

프로젝트 설치 방법

CLI 환경에서 다음의 명령어를 실행한다.

npm create vite@latest .
npm i 
npm i @tanstack/react-query
npm i -D @tanstack/eslint-plugin-query

 

eslint를 사용하는 이유는 ESLint 플러그인의 권장 규칙을 사용하면 일반적인 실수를 피할 수 있기 때무ㄴ,

exends 옵션의 배열 아이템으로 아래 내용을 추가한다.

plugin:@tanstack/eslint-plugin-query/recommended

 

프로젝트 범위를 <QueryClientProvider>로 랩핑하고, 사용할 queryClinet 인스턴스를 연결한다

// src/App.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import DelaedData from '~/components/DelayedData'

const queryClient = new QueryClient()

export default function App() {
	return (
    	<QueryClientProvider client = {queryClient}>
        	<DelayedData/>
        </QueryClientProvider>
     )
  }
}

TanStack Query는 캐시한 데이터를 신선(Fresh)하거나 상한(State) 상태로 구분하여 관리한다.

캐시된 데이터가 신선하다면 바로 사용하고, 데이터가 상햇다면 서버에 다시 요청해 신선한 데이터를 가져온다

 

데이터가 상하는 데까지 걸리는 시간은 staleTime 옵션으로 지정 가능하고,

신선한지 상했는지에 대한 여부는 isStale로 확인할 수 있다.

import { useQuery } from '@tanstack/react-query'

export default function DelayedData() {
	const { data, isStale } = useQUery({
    	queryKey: ['delay'],
        queryFn: async () => (await fetch('https://api...'))
        staleTime: 1000 * 10 // 10초후 상함. 즉 10초동안 신선
    })
    
    return (
    	<>
        	<div>데이터가 {isStale ? '상했어요...' : '신선해요!'}</div>
            <div>{JSON.stringify(data)}</div>
        </>
    )

 

queryKey

쿼리 키(queryKey)는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정한다.

다중 아이템 쿼리 키를 사용할 때는 , 아이템의 순서가 중요하다

https://vitamin3000.tistory.com/200

 

TanStackQuery (4) - Query Keys

TanStack Query는 기본적으로 쿼리 키에 따라 쿼리 캐싱을 관리한다.단일문자열이 있는 배열처럼 간단할 수 있고, 여러 문자열과 중첩된 객체의 배열처럼 복잡할 수도 있다.쿼리키가 고유하다면 사

vitamin3000.tistory.com

기본적으로 쿼리 함수 (queryFn)에서 사용하는 변수는 쿼리 키에 포함돼야 한다.

이를 통해 변수가 변경될 때마다 자동으로 다시 가져올 수 있게 된다.

 

만약 변수와는 상고나없이 항상 하나의 쿼리로 처리하고 싶다면, ESLint exhaustive-deps 규칙을 비활성화할 수 있다.

// eslint-disable-next-line @tanstack/uqery/eshaustive-deps

 

 

queryFn

쿼리 함수(queryFn)는 데이터를 가져오는 비동기 함수로, 꼭 데이터를 반환하거나 오류를 던져야 한다.

던져진 오류는 반환되는 error 객체(기본적으로 null)로 확인할 수 있다.

 

const { data, error } = useQuery<CUSTOMTYPE>({
	// ...
    
    if(!data.time) {
		throw new Error('문제가 발생했습니다 {error.message}')
     }

 

Select 

선택 함수(Select)를 사용하면 가져온 데이터를 변형(선택)할 수 있다.

쿼리 함수가 반환하는 데이터를 인수로 받아 선택 함수에서  처리하고 반환하면 최종 데이터가 된다.

최종 데이터 타입은 useQuery의 3번째 제네릭 타입으로 선언할 수 있다.

2번째는 오류 타입(Error)이다.

 

type Users = User[]
interface User {
	id: string
    name: string
    age: number
}

export default function UserNames() {
	const { data } = useQuery<Users, Error, string[]>{{
    
    // ... 
    
    select: data => data.map(user => user.name)

 

쿼리 함수를 따로 선언

해주면 선택 함수를 통한 최종 데이터의 타입을 추론할 수 있다.

아래의 예제는 쿼리 함수의 반환은 Users 타입이고, 최종 데이터(선택 함수의 반환)은 string [] 타입으로 추론한다.

async function queryFn(): Promise<Users> {
	const res = await fetch('https://api.dev/v0/users')
    const { users } = await res.json()
    return users
}

export default function UserNames() {
	// data는 string[] 타입으로 추론
    const { data } = useQuery({
		queryKey: ['users'],
        queryFn,
        staleTime: 1000 * 10,
        select: data => data.map(user => user.name)
    })

 

placeholderData

새로운 데이터를 가져오는 과정에서는 쿼리가 무효화되어 일시적으로 데이터가 없는 상태(undefined)가 되면 데이터 출력 화면이 깜빡일 수 있다.

이러한 현상을 방지하기 위해 placeholderData 옵션을 사용하면, 쿼리 함수가 호출되는 대기 상태(Pending)에서 임시로 표시할 데이터를 미리 지정할 수 있다.

placeholderData 옵션에는 함수를 지정할 수 있으며, 이 함수는 새로운 데이터를 가져오기 직전의 이전(Previous)데이터를 받을 수 있어서 이를 반환해 임시 데이터로 사용할 수 있다.

 

export default function Movies() {
	const { data } = useQUery<Movie[]>({
    	// ...
        
        placeholderData: prev => prev
    })
  }

 

structuralSharing

새로운 데이터를 가져올 때 이전 데이터와 비교해 변경되지 않은 부분은 이전 데이터를 재사용하도록 지정할 수 있다.

이를 통해 메모리 사용량을 최적화하고 불필요한 리렌더링을 방지할 수 있다.

 

아래와 같이 중첩된 객체의 이전 데이터와 새로운 데이터가 있다고 가정해보자

// 이전 데이터
const prevUser = {
  id: 'abc123',
  name: 'Neo',
  age: 22,
  contact: {
    email: 'neo@gmail.com',
    address: {
      country: 'Korea',
      city: 'Seoul'
    }
  }
}

// 새로운 데이터
const newUser = {
  id: 'abc123',
  name: 'Neo',
  age: 22,
  contact: {
    email: 'neo@gmail.com',
    address: {
      country: 'Korea',
      city: 'Suwon' // 변경된 부분!
    }
  }
}

 

structuralSharing 옵션이 true이면 변경된 부분만 새롭게 업데이트하고 변경되지 않은 부분은 이저 ㄴ데이터의 참조를 재사용한다.

반대로 옵션이 false이면 모든 객체가 새로운 참조로 생성된다. 

 

1. 매우 큰 중첩 객체를 다루는 경우 구조적인 비교 자체가 성능에 부담이 될 수 있음

2. 데이터가 항상 새로운 참조여야 하거나 데이터가 단순해 깊은 비교가 필요하지 않은 경우

위 두 가지 경우에는 false로 지정하는 것이 좋다

 

여기서 발생한 궁금증

tanstackquery에서 이미 캐시에서 동일한 알고리즘으로 캐시에서 데이터를 가져오고 있는데 structureSharing이 왜 필요한가?

 

질문에 대한 AI의 대답은...

  • TanStack Query의 기본 캐싱
    • 쿼리 키와 관련된 데이터를 메모링 ㅔ저장
    • 동일한 쿼리 요청이 들어올 때 캐시된 데이터를 재사용
    • 네트워크 요청을 줄이는 것이 주 목적
  • structuralSharing의 역할
    • 데이터 업데이트 시 객체의 참조 동일성 유지
    • 변경되지 않은 중첩 객체의 참조를 보존
    • React의 랜더링 최적화에 도움
  • 결론 
    • Tansstack QUerty의 캐싱은 네트워크 요청을 최적화 하는 것이고
      structuralSharing은 애플리케이션의 메모리 사용과 랜더링 성능 최적화에 목적이 있다.
    • 즉 서로 보완적인 관게이다

 

meta

쿼리에 대한 추가 접오를 제공한다

예를 들어, 쿼리 함수에서 발생한 오류 메시지 출력을 전역적으로 처리할 수 있다.

쿼리 클라이언트 생성의 queryCache 옵션에서 호출 쿼리의 추가 정보(meta)를 얻을 수 있다.

 

// src/main.tsx
import {
  QueryClient,
  QueryClientProvider,
  QueryCache
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (_error, query) => {
      alert(query.meta?.myErrorMessage) // 오류 메시지 출력!
    }
  })
})

// src/components/Movies.tsx

export default function Movies() {
  // ...
  
  const { data: movies } = useQuery<Movie[]>({
    queryKey: ['movies', searchText], // 검색어
    queryFn: async () => {
      const res = await fetch(`https://omdbapi.com?apikey=7035c60c&s=${searchText}`)
      const { Search: movies } = await res.json()
      return movies
    },
    meta: {
      myErrorMessage: '영화를 검색할 수 없어요!'
    }
  })
  
  // ...
}