Redux Toolkit Query 튜토리얼에 오신걸 환영합니다! 이 튜토리얼은 Redux Toolkit의 "RTK Query" 데이터 패칭 기능을 소개하고 올바르게 사용하는 방법에 대해서 간단하게 소개합니다.
RTK Query는 웹 애플리케이션에서 데이터를 로딩하는 흔한 케이스를 간단하게하는 진보된 데이터 패칭, 캐싱 툴입니다. RTK Query는 Redux Toolkit core의 위에서 작성되었고, RTK의 API들은 createSlice와 createAsyncThunk를 확장해서 만들어졌습니다.
RTK Query는 @reduxjs/toolkit 패키지의 추가적인 애드온으로 포함되어져있습니다. Redux Toolkit을 사용해도 RTK Query Api를 사용하지 않아도 되지만 우리는 RTK Query의 데이터 패칭과 캐싱이 많은 사용자들에게 택을 가져다 줄것이라고 생각합니다
How to Read This Tutorial
이 튜토리얼에서 우리는 React와 Redux Toolkit을 사용하는 걸 가정하지만, 다른 UI layers들과도 사용할 수 있습니다. 예시들은 애플리케이션 코드가 src 폴더에 있는 전형적인 Create-React-App 폴더 구조를 기반으로 하지만, 패턴들은 대부분의 사용하는 프로젝트나 폴더 구조에 적용할 수 있습니다.
스토어와 API 서비스 설정하기
RTK Query가 어떻게 작동하는지 보기위해, 기본적인 사용 예시를 만들어 보겠습니다. 이 예시에서는 React를 사용하고 RTK Query에서 자동 생성된 리액트 hooks를 사용하는 것을 가정하겠습니다.
// createApi를 import하기위해 React 엔트리 포인트 사용import { createApi, fetchBaseQuery } from'@reduxjs/toolkit/query/react'import { Pokemon } from'./types'// base URL과 엔드포인트들로 서비스 정의exportconstpokemonApi=createApi({ reducerPath:'pokemonApi', baseQuery:fetchBaseQuery({ baseUrl:'https://pokeapi.co/api/v2/' }),endpoints: (builder) => ({ getPokemonByName:builder.query<Pokemon,string>({query: (name) =>`pokemon/${name}`, }), }),})// 정의된 엔드포인트에서 자동으로 생성된 훅을 함수형 컴포넌트에서 사용하기 위해 exportexportconst { useGetPokemonByNameQuery } = pokemonApi
src/services/pokemon.js
// Need to use the React-specific entry point to import createApiimport { createApi, fetchBaseQuery } from'@reduxjs/toolkit/query/react'// Define a service using a base URL and expected endpointsexportconstpokemonApi=createApi({ reducerPath:'pokemonApi', baseQuery:fetchBaseQuery({ baseUrl:'https://pokeapi.co/api/v2/' }),endpoints: (builder) => ({ getPokemonByName:builder.query({query: (name) =>`pokemon/${name}`, }), }),})// Export hooks for usage in functional components, which are// auto-generated based on the defined endpointsexportconst { useGetPokemonByNameQuery } = pokemonApi
RTK Query를 사용할때, 전체 API를 보통 한곳에 정의합니다. 이 점은 swr이나 react-query같은 라이브러리들과 가장 많이 다른 점일 것인데, 여기에는 여러가지 이유들이 있습니다. 저희의 관점에서는 여러개의 커스텀 hooks들이 다른 파일들에 있는 것 보다 한곳에 위치하는게 요청, 캐시 무효화, 공통 앱 설정을 관리하기가 더욱 쉽다고 생각합니다.
팁
일반적으로, 애플리케이션에 필요한 베이스 URL당 하나의 API 슬라이스를 가져야 합니다. 예시로 만약 사이트에서 /api/posts와 /api/users에서 데이터를 가져와야 한다면 /api를 베이스 URL로 하는 하나의 API 슬라이스를 만들고 posts와 users로 엔드포인트를 나누어야 합니다. 이러면 endpoints와의 관계를 tag로 정의해서 자동 데이터 리패칭 기능을 효과적으로 활용할 수 있습니다.
유지보수적 관점에서, 하나의 API 슬라이스에 엔드포인트들을 포함하면서 엔드포인트들을 여러개의 파일에 나누어 정의하고 싶을 수도 있습니다. 코드 스플리팅에서 어떻게 injectEndpoints 프로퍼티를 사용해서 여러 파일들에서 하나의 API 슬라이스로 API 엔드포인트를 주입할 수 있는지 알아보세요.
스토어에 서비스 추가하기
RTK Query는 리덕스 루트 리듀서에 추가해야하는 "슬라이스 리듀서"와 데이터 패칭을 위한 커스텀 미들웨어를 생성합니다. 둘 다 리덕스 스토어에 추가해야 합니다.
src/store.ts
import { configureStore } from'@reduxjs/toolkit'// Or from '@reduxjs/toolkit/query/react'import { setupListeners } from'@reduxjs/toolkit/query'import { pokemonApi } from'./services/pokemon'exportconststore=configureStore({ reducer: {// 특정 top-level slice에서 생성된 리듀서를 추가 [pokemonApi.reducerPath]:pokemonApi.reducer, },// 캐싱, 요청 취소, 폴링 등등 유용한 rtk-query의 기능들을 위한 api 미들웨어 추가middleware: (getDefaultMiddleware) =>getDefaultMiddleware().concat(pokemonApi.middleware),})// 옵셔널, refetchOnFocus/refetchOnReconnect 기능을 위해 필요함// setupListeners 문서를 참고 - 커스텀을 위한 옵셔널 콜백을 2번째 인자로 받음setupListeners(store.dispatch)
src/store.js
import { configureStore } from'@reduxjs/toolkit'// Or from '@reduxjs/toolkit/query/react'import { setupListeners } from'@reduxjs/toolkit/query'import { pokemonApi } from'./services/pokemon'exportconststore=configureStore({ reducer: {// Add the generated reducer as a specific top-level slice [pokemonApi.reducerPath]:pokemonApi.reducer, },// Adding the api middleware enables caching, invalidation, polling,// and other useful features of `rtk-query`.middleware: (getDefaultMiddleware) =>getDefaultMiddleware().concat(pokemonApi.middleware),})// optional, but required for refetchOnFocus/refetchOnReconnect behaviors// see `setupListeners` docs - takes an optional callback as the 2nd arg for customizationsetupListeners(store.dispatch)
애플리케이션을 Provider로 감싸기
만약 애플리케이션을 Provider로 감싸지 않았다면, 리액트 애플리케이션 컴퍼넌트를 감싸는 리덕스 스토어의 표준 패턴을 사용하세요:
import*as React from'react'import { useGetPokemonByNameQuery } from'./services/pokemon'exportdefaultfunctionApp() {// 자동으로 데이터를 패치하고 쿼리 값을 가져오는 쿼리 hook을 사용const { data,error,isLoading } =useGetPokemonByNameQuery('bulbasaur')// 각각의 hooks은 생성된 엔드포인트에서도 접근 가능함// const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')return ( <divclassName="App"> {error ? ( <>Oh no, there was an error</> ) : isLoading ? ( <>Loading...</> ) : data ? ( <> <h3>{data.species.name}</h3> <imgsrc={data.sprites.front_shiny} alt={data.species.name} /> </> ) :null} </div> )}
src/App.jsx
import*as React from'react'import { useGetPokemonByNameQuery } from'./services/pokemon'exportdefaultfunctionApp() {// Using a query hook automatically fetches data and returns query valuesconst { data,error,isLoading } =useGetPokemonByNameQuery('bulbasaur')// Individual hooks are also accessible under the generated endpoints:// const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')return ( <divclassName="App"> {error ? ( <>Oh no, there was an error</> ) : isLoading ? ( <>Loading...</> ) : data ? ( <> <h3>{data.species.name}</h3> <imgsrc={data.sprites.front_shiny} alt={data.species.name} /> </> ) :null} </div> )}
요청을 생성할 때 여러 방법으로 상태를 추적할 수 있습니다. data, status, error로 알맞는 UI를 렌더링할 수 있습니다. 또한 useQuery는 유틸리티 불리언 값인 isLoading, isFetching, isSuccess, isError 로 가장 최근의 요청에 대한 값을 제공합니다.
기본 예시
흥미롭네요... 하지만 만약 여러개의 포켓몬을 한번에 보여주고 싶거나 여러개의 컴포넌트들이 같은 포켓몬을 불러오면 어쩌죠?
고급 예시
RTK Query는 어떤 컴포넌트든 같은 쿼리를 구독하면 항상 같은 데이터를 사용할 수 있도록 보장합니다. RTK Query는 자동으로 중복 요청을 제거하기 때문에 in-flight 요청과 성능 최적화에 대해서 걱정할 필요가 없습니다. 아래의 샌드박스를 시작해보죠. 브라우저 데브툴의 네트워크 탭을 확인해보세요. 4개의 구독된 컴포넌트가 있음에도 3개의 요청을 볼 수 있습니다. bulbasur는 오직 하나만 요청하고 두개의 컴포넌트의 로딩 상태는 동기화되어있습니다. 재미를 위해, 드롭다운의 값을 Off에서 1s로 바꿔서 쿼리가 리렌더링할때 행동을 봅시다.