리액트 쿼리 공식문서 번역 및 요약
React Query
리액트 쿼리 공식문서를 정주행하면서 중요한 내용을 정리해보았습니다.
Overview
리액트 쿼리는 종종 누락된 데이터를 가져오는 라이브러리로 설명되곤 하지만, server state를 fetching, caching, synchronizing, updating할 수 있습니다. 기존의 상태관리 라이브러리들은 비동기적인 업무를 처리하거나 서버 상태를 관리하는데 좋지 않았습니다.
서버 상태는 우리가 통제할 수 없는 원격에서 유지되고 있으며, 비동기적인 API 호출을 요구하고, 다른 사용자에 의해 변경될 수 있고, 당신의 클라이언트에 있는 서버 상태는 oudated 상태가 될 수 있습니다.
서버상태를 관리하다보면, 캐싱, 요청의 중복 처리, 데이터의 최신 상태 관리, 최적화와 lazy loading 등에 문제를 겪게 됩니다. React Query는 복잡한 데이터 요청 로직을 단순화 시켜주고, 서버상태를 최신으로 유지시켜주고, 고객에게 빠르고 responsive한 경험을 느끼도록 도와줍니다.
Installation
$ npm i react-query # or $ yarn add react-query
위 코드를 터미널에 입력하여 설치합니다.
Queries
Query는 unique key
에 연결된 선언적 의존성입니다. 쿼리는 데이터를 가져오는 Promise-based method에 사용됩니다. 만약 서버의 상태를 변경하고 싶다면 Mutations 를 사용하여야 합니다. 쿼리를 구독하려면 useQuery
훅을 사용합니다. useQuery
는 unique key와 데이터를 Promise형태로 가져오는 콜백을 필요로 합니다.
import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery(['todos'], fetchTodoList) }
unique key는 내부적으로 refetching, caching, 쿼리 공유를 위해 쓰입니다.
const result = useQuery(['todos'], fetchTodoList)
쿼리를 통해 리턴되는 결과는 데이터를 다루는데 필요한 정보를 담고 있습니다. 아래와 같이 destructuring해서 사용할 수 있습니다.
const { status, isLoading, isError, isSuccess, data, error } = useQuery(['todos'], fetchTodoList)
아래와 같이 결과에 대해서 다른 컴포넌트를 리턴하는 방식으로 사용할 수 있습니다.
function Todos() { const { status, data, error } = useQuery(['todos'], fetchTodoList) if (status === 'loading') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) }
Query Keys
query key는 직렬화가 가능하다면 배열 안에 담아서 쓸 수 있습니다. 쿼리키는 직렬화가 가능하고, 고유해야합니다. 쿼리 키는 single string이 될 수도 있고, serializable의 배열이 될 수도 있습니다. 쿼리키는 deterministically 하게 해시됩니다. 아래 세 쿼리 키는 동일하게 간주됩니다.
useQuery(['todos', { status, page }], ...) useQuery(['todos', { page, status }], ...) useQuery(['todos', { page, status, other: undefined }], ...)
그러나, 순서가 달라지면 다른 키로 간주됩니다.
useQuery(['todos', status, page], ...) useQuery(['todos', page, status], ...) useQuery(['todos', undefined, page, status], ...)
쿼리 함수가 변수에 의존하고 있다면, 해당 변수는 쿼리 키 배열에 담겨있어야 합니다.
function Todos({ todoId }) { const result = useQuery(['todos', todoId], () => fetchTodoById(todoId)) }
Query Functions
쿼리 함수는 프로미스를 리턴한다면 무엇이든 될 수 있습니다. 즉, data를 resolve하거나 에러를 던져야 합니다.
useQuery(['todos'], fetchAllTodos) useQuery(['todos', todoId], () => fetchTodoById(todoId)) useQuery(['todos', todoId], async () => { const data = await fetchTodoById(todoId) return data }) useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))
에러가 발생한다면 반드시 던져줘야 합니다. 에러는 result.error로 받을 수 있습니다.
const { error } = useQuery(['todos', todoId], async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } return data })
axios
나 graphql
등은 에러를 자동으로 던져주지만, 자바스크립트의 fetch API
는 에러를 던져주지 않기 때문에 반드시 핸들링을 해줘야 합니다.
useQuery(['todos', todoId], async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() })
query key 배열은 고유하게 쿼리를 관리하는 역할도 하지만, 쿼리함수에게 주는 인자값이 되기도 합니다.
function Todos({ status, page }) { const result = useQuery(['todos', { status, page }], fetchTodoList) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() }
query function은 QueryFunctionContext
를 인자로 받습니다. 이는 아래으 ㅣ구성으로 이루어져 있습니다.
queryKey: QueryKey
: Query Keys- pageParam: unknown | undefined
- only for Infinite Queries
- the page parameter used to fetch the current page
- signal?: AbortSignal
- AbortSignal instance provided by react-query
- Can be used for Query Cancellation
- meta?: Record<string, unknown>
- an optional field you can fill with additional information about your query
Network Mode
리액트 쿼리는 네트워크 연결이 없을 때 Queries
와 Mutations
를 구분할 수 있는 세 가지 모드를 가지고 있습니다. 이는 쿼리별/뮤테이션 별로 설정될 수 있습니다. 리액트 쿼리가 데이터 fetching에 많이 쓰이기 때문에 기본 모드는 online
입니다. 그러나 fetchStatus가 추가로 노출됩니다.
online
온라인 모드에서는 쿼리와 뮤테이션이 네트워크 연결이 없으면 작동하지 않습니다. 네트워크 연결이 중단된 경우 기존 상태를 유지합니다.
- fetching : 쿼리함수가 실제로 실행되고 있는 상태
- paused : 쿼리가 실행중이 아님
- idle : 쿼리가 fetching중이 아니고, 멈춘 적도 없음
isFetchingr 과 isPaused가 여기서 기인합니다.
loading 만으로 스피너를 보여주는 건 충분하지 않습니다. Queries가 loading이어도 online모드일 경우 인터넷 연결이 끊어진 상태면 fetchStatus : paused 상태일 수 있기 때문입니다.
always
이 모드에서는 항상 fetch를 합니다. active 한 network가 필요 없는 경우에 사용합니다.
offlineFirst
이 모드에서는 쿼리함수를 한 번 실행하고, retry를 일시정지합니다. offline-first PWA 나 캐시 컨트롤에 유용합니다. 첫번째 fetch는 오프라인 스토리지나 캐시로 부터 가져오기 때문에 성공할 확률이 높지만, 실패하는 경우 온라인 모드처럼 동작합니다. retry를 일시정지합니다.
Manual Parallel Queries
여러 개의 변하지 않는 쿼리를 병렬적으로 실행하고 싶으면 쭉 나열하면 됩니다. 그런데 suspense mode에서는 여러 개의 useQuery를 쓰면 첫번째에서 걸릴 수 있기 때문에, useQueries
를 써야합니다.
function App () { // The following queries will execute in parallel const usersQuery = useQuery(['users'], fetchUsers) const teamsQuery = useQuery(['teams'], fetchTeams) const projectsQuery = useQuery(['projects'], fetchProjects) ... }
여러 개의 쿼리가 렌더링마다 변화되면서 실행된다면, 쿼리를 병렬적으로 나열하면 안되고, useQueries 훅을 써야 합니다.
function App({ users }) { const userQueries = useQueries({ queries: users.map(user => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }) }) }
Dependent Queries
Dependant queries는 이전 쿼리에 의존합니다. 의존하는 항목을 enabled
를 통해서 명시할 수 있습니다.
// Get the user const { data: user } = useQuery(['user', email], getUserByEmail) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects } = useQuery( ['projects', userId], getProjectsByUser, { // The query will not execute until the userId exists enabled: !!userId, } )
위 코드에서 userId를 boolean으로 주게 됩니다. 그러면 projects
쿼리의 상태는 아래와 같은 상태로 시작합니다.
status: 'loading' fetchStatus: 'idle'
user가 준비되면, projects query가 enabled 상태가 되고, 아래 상태로 바뀝니다.
status: 'loading' fetchStatus: 'fetching'
프로젝트 데이터를 가져오면, 아래 상태로 바뀝니다.
status: 'success' fetchStatus: 'idle'
Background Fetching Indicators
status == loading
이라는 상태는 초기 hard-loading 상태를 보여주는 데는 충분하지만, refetching에 대한 상태를 나타내기에는 충분하지 않습니다. 그 문제는 isFetching
을 통해서 해결할 수 있습니다.
function Todos() { const { status, data: todos, error, isFetching } = useQuery( 'todos', fetchTodos ) return status === 'loading' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map(todo => ( <Todo todo={todo} /> ))} </div> </> ) }
전역적으로 fetching중인지를 검사하고 싶다면 useIsFetching을 사용하면 됩니다.
import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null }
Window Focus Refetching
유저가 앱을 나갔다가 오래된 데이터를 돌아오면, 리액트 쿼리는 자동적으로 최신의 데이터를 요청합니다. refetchOnWindowFocus
옵션을 통해서 이를 비활성화할 수 있습니다.
//Disabling Globally const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } //Disabling Per-Query useQuery(['todos'], fetchTodos, { refetchOnWindowFocus: false })
특별한 상황에서, 리액트 쿼리가 재확인하도록 하는 당신은 포커스이벤트를 관리할 수 있습니다. focusManager.setEventListner
를 이용하면 됩니다. 이 API를 호출하면 이전의 핸들러는 제거되고, 새로운 핸들러로 덮어써집니다.
focusManager.setEventListener(handleFocus => { // Listen to visibilitychange and focus if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('visibilitychange', handleFocus, false) window.addEventListener('focus', handleFocus, false) } return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', handleFocus) window.removeEventListener('focus', handleFocus) } })
Disabling/Pausing Queries
쿼리가 자동으로 실행되는 것을 막고 싶으면, enabled=false
옵션을 주면 됩니다. 이 경우,
- 쿼리가 데이터를 캐시했으면 쿼리는
isSuccesss
가 됩니다. - 캐시가 되어있지 않으면
status===loading
이나fetchStatus === idle
이 됩니다. - 마운트 됐을 때 쿼리가 데이터를 자동으로 가져오지 않습니다.
- background에서 자동으로 refetch되지 않습니다.
- useQuery로부터 반환되는 refetch는 query를 fetch하는데 사용할 수 있습니다.
function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery(['todos'], fetchTodoList, { enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> // 여기서 수동으로 refetch {data ? ( <> <ul> {data.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : ( isError ? ( <span>Error: {error.message}</span> ) : ( (isLoading && !isFetching) ? ( <span>Not ready ...</span> ) : ( <span>Loading...</span> ) ) )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) }
Lazy Queries
enabled옵션은 영구적으로 쿼리를 비활성화 할수도 있지만, 나중에 enable/disable할 수도 있습니다. enabled에 걸리는 값을 아래처럼 어떤 시점에 의존적으로 설정하면 딥니다.
function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery( ['todos', filter], () => fetchTodos(filter), { // ⬇️ disabled as long as the filter is empty enabled: !!filter } ) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data}} /> </div> ) } Edit on Github
Query Retries
쿼리가 실패하면 자동적으로 max number of consecutive retries 에 해당하는 값(기본 값 3)만큼 재시도를 합니다. 전역 또는 각 쿼리마다 해당 옵션을 설정할 수 있습니다.
import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery(['todos', 1], fetchTodoListPage, { retry: 10, // Will retry failed requests 10 times before displaying an error })
Retry Delay
실패를 했다고 바로 다시 요청을 보내지는 않습니다. 기본적으로 1초로 세팅되어있고, 바꿀 수 있습니다.
Paginated/Lagged Queries
페이지네이션은 리액트 쿼리로 대표적으로 구현할 수 있는 UI 패턴입니다.
const result = useQuery(['projects', page], fetchProjects)
위 예제에는 문제가 있습니다. 각각의 새 페이지가 완전히 새로운 쿼리처럼 처리되기 때문에 UI가 성공 및 로드 상태를 왔다갔다 합니다. 아래 해결책이 있습니다.
Better Paginated Queries with keepPreviousData
keepPreviousData에 true값을 주게 되면, 쿼리 키가 변경되었더라도, 새로운 데이터 요청 중에 마지막으로 성공적으로 fetching한 데이터에 접근을 할 수 있습니다. 새로운 데이터가 도착하면, 오래된 데이터가 갱신됩니다. isPreviousData
를 통해서 구별할 수 있습니다.
function Todos() { const [page, setPage] = React.useState(0) //페이지 상태 정의 //페이지 값으로 해당 페이지의 프로젝트 데이터 가져오기 const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isLoading, isError, error, data, isFetching, isPreviousData, } = useQuery(['projects', page], () => fetchProjects(page), { keepPreviousData : true }) return ( <div> {isLoading ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map(project => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage(old => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button>{' '} <button onClick={() => { if (!isPreviousData && data.hasMore) { setPage(old => old + 1) } }} // 콜백에 다음페이지가 준비됐는지를 조건으로 걸 수 있다. disabled={isPreviousData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null}{' '} </div> ) }
Infinite Queries
useInfiniteQuery
를 이용하면 무한스크롤을 구현할 수 있습니다. 아래와 같은 구성요소가 있습니다.
- data : infinite query data를 가지고 있는 객체입니다.
- data.pages : 페이지를 fetch한 array입니다.
- data.pageParams : pageParams를 가지고 있는 배열입니다.
fetchNextPage
와fetchPreviousPage
를 통해서 이전/다음 페이지를 가져올 수 있습니다.getNextPageParam
과getPreviousPageParam
을 통해서 더 가져와야할 정보가 있는지 결정할 수 있습니다.hasNextPage
라는 boolean 값을 이용할 수도 있습니다.isFetchingNextPage
와isFetchingPreviousPage
를 이용해서 background의 state를 확인할 수 있습니다.
아래와 같이 Load More UI를 구성할 수 있습니다.
- 기본적으로
useInfiniteQuery
가 첫 데이터그룹을 요청할 때까지 기다립니다. getnextPageParam
으로부터 다음 쿼리에대한 정보를 받습니다.fetchNextPage
함수를 호출합니다.
getNexPageParam
함수에서 반환된pageParam
을 덮어쓰지 않으려면fetchNextPage
arguments와 함께 호출하면 안됩니다!예를 들어,
<button onClick={fetchNextPage}/>
을 주게 되면 인자로 이벤트가 자동으로 들어가는데, 이 때 pageParam이 덮어써지게 됩니다.
function Projects() { const fetchProjects = ({ pageParam = 0 }) => fetch('/api/projects?cursor=' + pageParam) //커서를 param으로 데이터를 가져오는 함수 //param을 args에 넣어서 활용하는 모습 const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery(['projects'], fetchProjects, { getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) //fetchProjects : cursor를 param으로 프로젝트 목록을 로드 //getNextPageParam을 통해 다음 요청에 필요한 param을 반환하는 함수를 작성합니다. return status === 'loading' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.projects.map(project => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} // 다음페이지가 있는지, 데이터를 가져오고있는지에 따라 다른 텍스트 표시 </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) }
infinite query가 refetch 되어야 할 때는?
infinite query는 stale
상태가 되고, 각 그룹이 순차적으로 가져와집니다. 기본 데이터가 변겨오디더라도 오래된 커서를 사용하지 않고 잠재적으로 사본을 얻거나 레코드를 건너 뜁니다. infinite query의 결과가 쿼리 캐시에서도 지워지면, pagination은 처음부터 다시 시작합니다.
refetchPage
subset page를 다시 가져오려면 refetch 함수에 refetchPage 정보가 담긴 함수를 넘깁니다.
query function에 custom information을 넣어주고 싶으면?
fetchNextPage를 이용해서 강제로 갱신할 수 있습니다. (getNextPageParam으로 일반적 방식이 아닌, 수동적 방식으로 param이 달라집니다.)
'Study Log' 카테고리의 다른 글
JS DeepDive 정리 1장 - 20장 (0) | 2022.08.11 |
---|---|
git branch 공부하기 (0) | 2022.02.26 |
Redux Toolkit 알아보기 (0) | 2022.02.10 |
Redux 공부하기 (0) | 2022.01.01 |
브라우저 공부 (0) | 2021.11.20 |
댓글
이 글 공유하기
다른 글
-
JS DeepDive 정리 1장 - 20장
JS DeepDive 정리 1장 - 20장
2022.08.11자바스크립트 딥다이브를 읽으며 중요하거나 모르는 내용 위주로 정리했습니다. 1. 프로그래밍이란? 프로그래밍이란 컴퓨터에게 실행을 요구하는 커뮤니케이션이다. 구문과 의미의 조합이다. 문제 해결을 위해 컴퓨팅 사고력을 요한다. 우리는 프로그래밍 언어를 작성하고, 컴파일러는 그것을 기계어로 번역한다. 문법적으로 틀리지 않아도, 요구사항에 부합하지 않으면 의미가 없다. 결국 프로그래밍은 요구사항을 분석하고, 함수의 집합으로 변환하고, 흐름을 제어하는 일이다. 2. 자바스크립트란? 넷스케이프에서 브라우저에서 동작하는 프로그래밍 언어를 도입하기로 결정 → 모카 → 라이브스크립트 →자바스크립트 JScript 의 등장으로 위기를 맞았으나 표준화 기구 ECMA 인터내셔널에 JS의 표준화를 요청, ECMAScript로 명… -
git branch 공부하기
git branch 공부하기
2022.02.26들어가며 https://learngitbranching.js.org/ 를 통해 깃허브 브랜치를 공부하고 개념들을 정리해본다. Git commit 소개 커밋은 스냅샷 이다. Git은 커밋을 가볍게 유지하고자 하기 때문에, 변경내역(delta)을 저장한다. 정답 코드: git commit git commitGit 브랜치 브랜치는 커밋에 대한 참조이다. 하나의 커밋과 그 부모 커밋들을 포함하는 작업내역이라고 생각하면 된다. 브랜치는 많이 만들어도 메모리나 디스크 공간에 부담이 되지 않는다. git branch명령어는 현재 지점에 새로운 브랜치를 만들어 놓는다. 즉 가장 최근 커밋을 참조하는 다른 브랜치를 생성한다. 생성만 할 뿐, 이동하지는 않는다. 이동 명령어는 git checkout 브랜치명으로 할 수 있… -
Redux Toolkit 알아보기
Redux Toolkit 알아보기
2022.02.10들어가며 출처: 코딩애플 프론트엔드에서 중요한 문제 중 하나가 전역상태를 어떻게 관리할 것인가 라고 생각한다. 특히나 React로 프로젝트를 구성했다면 여러 컴포넌트들 사이에서 props를 주고 받는 것이 프로젝트 규모가 커질수록 부담스러워질 수 있다. 컴포넌트 깊이가 깊어질수록 상위 컴포넌트의 state를 갱신하는 과정이 복잡해지고, 이는 컴포넌트 간의 결합도를 강화시킨다. 컴포넌트 간의 독립성을 추구하기 위해, 그리고 state 갱신에 관한 에러를 방지하기 위해 redux가 등장하였다. Redux에 대한 자세한 설명은 생략하기로 하고, 이 글에서는 Redux Toolkit을 이용해서 프로젝트를 어떻게 구성했는지를 설명해보려고 한다. 그냥 바닐라(?) 리덕스 대신 리덕스 툴킷을 이용한 이유는 보일러플레… -
Redux 공부하기
Redux 공부하기
2022.01.01들어가며 Velopert의 모던 리액트를 읽으면서 알게 된 중요한 내용들을 정리해본다. 1월에 하기로 한 프로젝트에 리덕스를 도입할 것인지 말지를 결정하기 위해 빠르게 공부해보기로 했다. 6장 리덕스 리덕스의 흐름은 아래와 같이 진행된다. createStore에 reducer함수를 넣어서 store를 만드는 것으로 전역 상태를 관리한다. reducer함수는 state와 action을 입력받고, action의 타입에 따라 state를 불변성을 지키며 갱신한다. reducer를 위해 초기 State, action type(문자열), action 생성함수를 정해주어야 한다. action 생성함수는 dispatch를 깔끔하게 하기 위함이고… (store.dispatch(액션생성함수)의 형식으로 쓴다) 기본적으…
댓글을 사용할 수 없습니다.