State management와 TanStack Query - 2

JUNG MINU·2024년 3월 27일
0
post-thumbnail

TanStack Query(Vue Query)

Vue Query란?

Vue Query는 Tanstack에서 개발한 server state management 라이브러리입니다. 공식적으로 React, Vue, Solid, Svelte 4가지의 프론트엔드 프레임워크를 지원하며, TanStack Query에서 각 프레임워크 별 이름을 따 Vue에서 작동하는 라이브러리의 경우 Vue Query라고 부릅니다.

개요

TanStack Query (FKA Vue Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze.

Vue Query는

  • 서버로부터 데이터 가져오기

  • 캐싱

  • 서버 데이터를 서버와 동기화하고 업데이트하기

주로 세 가지 기능을 수행하기 위해 만들어졌다고 설명합니다.

왜 사용해야 할까?

React, Vue 등 유명 웹 프레임워크에는 서버 데이터를 fetching, caching, synchronizing, updating하는 “일반적인” 또는 “공통화 된” 방식을 제공하지 않는 것을 문제로 제시합니다.

때문에 데이터를 가져오고, 관리하는 방법을 고민하고, 개발자마다 매우 다른 방식으로 개발되는 것에 대한 문제점을 이야기합니다.

또한 기존 상태 관리 라이브러리로 서버 데이터를 관리하는것도 적합하지 않다고 말합니다. Server state와 client state는 전혀 다르기 때문입니다.

Server state의 차이점 (Client state와 비교하여)

  • 프론트엔드 개발자가 통제할 수 없는 위치에 데이터가 저장되어 있다.

  • 가져오거나 업데이트를 하기 위해 비동기적으로 API를 이용해야 한다.

  • 다른 사람과 데이터를 공유하며, 본인이 모르게 다른 사람이 데이터를 변경할 수 있다.

  • 자칫 데이터가 “오래된” 데이터가 될 수 있다.

이 차이점을 해결하기 위해 개발하다보면 다음과 같은 문제점을 마주치게 될 것이라고 말합니다.

  • 캐싱 → 가장 어려운 일

  • 동일한 여러 요청을 단일 요청으로 보내 중복 제거하기

  • 백그라운드에서 “오래된” 데이터 업데이트

  • 데이터가 “오래된” 시점을 파악하기

  • 최대한 빠르게 데이터 업데이트를 반영하기

  • Pagenation, Lazy loading 등 성능 최적화

  • Server state의 메모리 관리, garbage collection

  • 서버 데이터 메모이제이션

개발자는 이 모든 문제들을 해결하기 매우 어렵고, 놓치기 쉽다고 설명하며, Vue Query는 이러한 Server state의 까다로운 문제점들을 쉽게 극복하게 해준다고 설명하고 있습니다. Vue Query를 사용하면 위 문제들을 크게 신경쓸 필요 없이 기본적으로 “알아서”, “잘” 작동합니다.

주요 개념

Queries

쿼리(query)는 QueryKey라는 unique한 key에 연결된 데이터의 비동기적인 출처(asynchronous source)에 대한 선언적 종속성(declarative dependency)입니다.

쉽게 설명하면, 데이터를 가져오는 함수와 가져온 데이터, 그리고 unique한 key값의 한 단위를 이야기한다고 할 수 있습니다.

컴포넌트 또는 hook에서 쿼리를 구독(subscribe)하기 위한 방법입니다.

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

const result = useQuery({ queryKey: ['todos'], queryFn: getTodoList })

Query Keys

전역적으로 쿼리를 refetch하고, caching하고, 공유하는 데 내부적으로 사용되는 key값입니다.

Query key는 기본적으로 상수값의 배열로 선언되며, 문자열, 숫자, 객체, 배열 등 다양한 값을 포함할 수 있습니다.

// 하나의 문자열도 배열로 선언해야 합니다.
useQuery({ queryKey: ['todos'], ... })

// 다양한 값들이 배열에 포함될 수 있으며, 모두 다른 key로 취급되어 각각 별도로 캐싱됩니다. 
useQuery({ queryKey: ['todos', 0 ], ... })
useQuery({ queryKey: ['todos', 1 ], ... })

// 객체 내부도 깊은 비교를 통해 다른 key로 취급합니다.
useQuery({ queryKey: ['todos', { filter: 'done' } ], ... })
useQuery({ queryKey: ['todos', { filter: 'unfinished' } ], ... })

// 배열은 순서가 다르면 다른 값으로 취급되지만,
useQuery({ queryKey: ['todos', [0, 1] ], ... })
useQuery({ queryKey: ['todos', [1, 0] ], ... })

// 객체는 순서가 달라도 같은 값으로 취급됩니다.
useQuery({ queryKey: ['todos', { filter: 'done', page: 3} ], ... })
useQuery({ queryKey: ['todos', { page: 3, filter: 'done'} ], ... })

Query Function에서 사용하는 모든 변수는 Query Key에 포함되어야 합니다. 변수에 의존하는 각각 다른 속성값(페이지, 필터 등)을 가진 데이터 배열을 별도로 캐싱할 수 있습니다.

function useTodo(page: number, filter: 'done' | 'unfinished') {
	return useQuery({ queryKey: ['todos', { page, filter } ], ... })
}

이 경우 0페이지와 1페이지는 별도로 캐싱됩니다.

만약 0페이지에서 1페이지에 방문했다가 빠르게 0페이지에 되돌아가더라도 굳이 0페이지를 다시 로드할 필요가 없어집니다.

  • 불필요한 API 호출을 줄여 성능을 최적화하고,

  • 빠른 데이터 표시를 통해 사용자 경험을 개선합니다.

Query Functions

해당 쿼리의 데이터 또는 error값의 Promise를 return하는 함수입니다.

쿼리 함수는 Promise를 반환하는 모든 형태의 함수입니다.

  • 데이터를 반환하거나

  • 오류를 발생시켜야 합니다.

발생한 오류는 error 에 저장됩니다.

const { error } = useQuery({ ... })

Query Function Variables

Query Key는 쿼리의 유니크한 구분값의 역할, 쿼리 함수가 의존하는 변수의 구분을 통한 캐싱 뿐 아니라 직접 쿼리 함수의 변수로도 사용될 수 있습니다.

const result = useQuery({
  queryKey: ['todos', { status, page }],
  queryFn: getTodoList,
})

// 쿼리 키를 매개변수로 받으면 status, page 변수에 접근할 수 있습니다.
function fetchTodoList({ queryKey }) {
  const [_key, { status, page }] = queryKey
  return new Promise()
}

Mutations

단순히 조회성 쿼리가 아닌, 데이터를 생성(POST), 업데이트(PUT), 삭제(DELETE)하는 데 사용됩니다.

import { useMutation } from '@tanstack/vue-query'

const { mutate } = useMutation({
  mutationFn: (newTodo) => postTodo(newTodo),
})

function addTodo() {
  mutate({ id: new Date(), title: '뷰쿼리 공부하기' })
}

데이터를 변형하는 API의 경우 useQuery가 아닌 useMutations를 사용해야 합니다.

Mutation Function 호출 이후 여러 상황에 대한 콜백을 지원합니다.

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => { },
  onError: (error, variables, context) => { },
  onSuccess: (data, variables, context) => { },
  onSettled: (data, error, variables, context) => { },
})

Query Invalidation

무언가 사용자의 작업으로 인해 쿼리가 변형되었다는 것이 매우 확실할 때에도 쿼리가 오래된(stale) 데이터가 되어 백그라운드에서 자동으로 refetch될 때 까지 기다리는 것은 조금 이상합니다.

Query Invalidation은 특정 Query Key를 가진 쿼리의 데이터가 더이상 유효하지 않음을 선언하고, 쿼리를 무효화하여 강제로 refetch되게 합니다.

예를 들어, todoList를 조회한 이후 특정 todo를 수정하거나, 새로운 todo를 생성해 todoList의 refetch가 필요할 경우, ‘todos’로 시작하는 Query Key를 가진 쿼리들의 데이터를 무효화합니다.

// `todos`로 시작하는 Query Key를 가진 캐시된 쿼리들을 무효화합니다.
queryClient.invalidateQueries({ queryKey: ['todos'] })

이렇게 되면 아래 Query Key를 가진 캐시된 쿼리가 있을 경우

[ 'todos', { page: 0, size: 10 } ]

[ 'todos', { page: 1, size: 10 } ]

[ 'todos', { page: 0 } ]

[ 'todos' ]

전부 refetch됩니다.

또는 모든 쿼리들이 유효하지 않아지는 일이 발생할 경우, 모든 쿼리를 무효화할 수도 있습니다.

// 모든 캐시된 쿼리를 무효화합니다.
queryClient.invalidateQueries()

invalidateQueries()가 실행될 경우 아래와 같은 일이 발생합니다.

  • 데이터가 stale하다고 표시

  • 쿼리가 렌더링되고 있을 경우 백그라운드에서 데이터 refetch

Vue Query가 알아서 하는 일들

Out of the box, TanStack Query is configured with aggressive but sane defaults.

Vue Query는 공격적이지만 정상적인 기본값이 설정되어 있다고 설명합니다. 즉, 서버 상태의 효과적인, 효율적인 관리를 위해 적극적으로 개입하는 라이브러리 라는 것입니다.

때문에 Vue Query를 사용하기 위해서는 어떤 부분들을 기본적으로 수행해주는지 필수적으로 알아야 당황하지 않고 Vue Query의 기능을 최대한 활용할 수 있습니다.

useQuery 또는 useInfiniteQuery를 이용해 생성한 쿼리 인스턴스는 캐시된 데이터를 오래된(stale) 데이터로 간주합니다.

오래된 데이터 → stale queries

  • staleTime을 전역적으로, 또는 쿼리별로 설정할 수 있습니다.

  • staleTime을 길게 설정하면 쿼리 데이터를 자주 가져오지 않습니다.

오래된 데이터(stale queries)는 다음과 같은 상황에 백그라운드에서 자동적으로 refetch됩니다.

자동적으로 refetch되는 조건

  • 새로운 query가 마운트될 때 - New instances of the query mount

  • 윈도우가 다시 focus될 때 - The window is refocused

  • 네트워크가 재연결 되었을 때 - The network is reconnected

  • Refetch 주기가 설정되어 있을 때 - The query is optionally configured with a refetch interval

위 조건들은 각각 다음과 같은 옵션으로 사용자화 할 수 있습니다.

  • refetchOnMount

  • refetchOnWindowFocus

  • refetchOnReconnect

  • refetchInterval

예상치 못한 refetch가 너무 자주 일어난다면 “윈도우가 다시 focus될 때”조건을 의심해봐야 합니다.

쿼리의 활성 인스턴스가 없을 경우 비활성(inactive) 레이블이 지정되며, 나중 사용을 대비해 캐시에 남아있습니다.

  • 비활성(inactive) 쿼리들은 기본적으로 5분 이후 garbage collect됩니다.

  • cacheTime설정을 변경하면 이 시간을 수정할 수 있습니다.

실패한 쿼리는 기하급수적인 백오프 지연(exponential backoff delay)을 사용해 자동으로 3번 재시도합니다.

  • retry 또는 retryDelay설정값을 변경할 수 있습니다.

  • 기본 시도 횟수 3 을 수정할 수 있고, 백오프 함수도 수정 가능합니다.

쿼리 결과는 구조적으로 공유되어(structurally shared) 데이터가 실제로 변경되었는지 감지합니다. 변경되지 않은 데이터는 참조된 변수값도 변경하지 않아 최적화에 도움을 줍니다.

  • 낯선 개념이지만 99.9%의 상황에서 도움이 되며 비용이 들지 않고, 앱의 성능을 향상할 수 있다고 말합니다.

  • JSON 형식의 데이터만 지원됩니다.

  • 만약 매우 큰 응답값으로 인해 성능 문제가 발생한다면 structuralSharing 을 비활성화할 수 있습니다.

뷰쿼리를 사용해 해결된 것들

  • 간단한 코드, fetch 코드의 스타일 통일화

    	- 대부분의 상황에서 알아서 server state를 관리합니다.
    
    	- 간단하고 명확한 메서드를 제공합니다.
    
    	- fetch코드가 매우 단순하고, 가독성이 높아집니다.
    
    	- 개발자마다 각기 다른 fetch 코드 스타일을 공통화해줍니다.
  • 기본 제공되는 손쉬운 캐싱, 다양한 라이프사이클에 대한 유기적인 대응

    	- 자동으로 서버 데이터를 캐싱해줍니다.
    
    	- 다양한 메서드를 이용해 여러 상황의 라이프사이클에 대응합니다.
    
    	- 필요한 경우 상세한 사용자 정의를 통해 쿼리를 관리할 수 있습니다.
  • fetch 코드의 재사용성 증가

    	- 전역적으로 관리되기때문에 굳이 캐싱을 위해 store를 거칠 필요가 없습니다. 
    
    	- 컴포저블로 useQuery를 사용하면 재사용성이 높은 코드를 작성할 수 있습니다.

읽어보면 좋을 글
공식문서가 매우 친절합니다. 공식문서에 쓰여진 대부분의 내용은 사용하는데 큰 도움이 됩니다.
Overview | TanStack Query Docs

큰 프로젝트에서 Query Key를 효과적으로 관리하기 위한 전략은 필수적입니다. Query Key가 중복될 위험이 있기 때문입니다.
Effective React Query Keys

공식 문서에서 중요한 개념과 내용들을 간단하게 정리해 놓은 글입니다.
React Query 공식 문서 간단 정리

Tanstack Query에 대한 자세한 내용들을 한국어로 정리해 놓은 문서입니다.
ssi02014/react-query-tutorial: 😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리

카카오페이에서 React Query를 도입한 이유를 작성한 글입니다.
카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유

참고 자료

https://vuejs.org/guide/scaling-up/state-management.html

https://simplevue.gitbook.io/intro/03.-state-props

https://velog.io/@peterhyun1234/Frontend-Development-What-is-State-Management

https://tanstack.com/query/v4/docs/vue/overview

profile
감각있는 프론트엔드 개발자 정민우입니다.

0개의 댓글