티스토리 뷰

Redux Toolkit Query (RTK Query)는 웹 어플리케이션에서 데이터를 로딩하는 흔한 케이스를 간단하게 하는 진보된 데이터 패칭, 캐싱 툴이다. RTK Query는 Redux Toolkit core의 위에서 작성되었고, RTK의 API들은 `createSlice`와 `createAsyncThunk`를 확장해서 만들어졌다. 

 

RTK Query는 `@reduxjs/toolkit` 패키지의 추가적인 애드온으로 포함되어져 있다. Redux Toolkit을 사용해도 RTK Query Api를 사용하지 않아도 되지만 RTK Query의 데이터 패칭과 캐싱이 많은 사용자들에게 혜택을 가져다 줄것이라고 예상한다.

즉, RTK Query는 강력한 data fetching, caching 툴이다. 웹 어플리케이션에서 데이터를 가져오는 상황을 간단하게 만들어서 data fetching과 caching 로직을 스스로 작성할 필요가 없도록 편의성 제공


Redux 와 Redux Toolkit 복습 

리덕스는 Flux 아키텍처의 구현체로 대형 MVC 어플리케이션에서 종종 나타나는 데이터 간의 의존성 이슈, 즉 연쇄적인 갱신이 뒤얽혀 데이터의 흐름을 예측할 수 없게 만들었던 문제를 해결하기 위해 고안되었다. 

데이터를 단방향으로 흐르는 방법을 고안하게 되고 그것이 바로 플럭스 아키텍처의 핵심 모델이된다. 그 구현체인 리덕스는 어플리케이션을 위한 상태 컨테이너로써 단방향 데이터 흐름을 활용하여 시스템을 예측 가능하게 만들어서 시스템을 보완하는 역할을 하게 된다. 

 

리덕스를 사용하는 구조에서는 전역 상태를 전부 하나의 저장소(store)안에 있는 객체 트리에 저장하며, 상태를 변경하는 것은 어떤 일이 일어날지를 서술하는 객체인 액션(action)을 내보내는(dispatch) 것이 유일한 방법이다. 그리고 액션이 전체 어플리케이션의 상태를 어떻게 변경할 지 명시하기 위해서는 리듀서(reducer)의 작성이 필요하다.

사용자의 상호작용에 응답하기 위해서 뷰는 액션을 만들어 시스템에 전파한다.

관련된 자세한 내용은 -> `리덕스 툴킷`은 정말 천덕꾸러기 일까` -화해 블로그 를 통해서 확인하시길 바랍니다.

 

[Redux Toolkit 기본기능]

  • configureStore(): createStore를 래핑하여 간단하게 store를 만들어준다.
  • createReducer(): 복잡한 switch 문을 작성하는 대신 간단한 reducer 제공
    • 기존 redux의 경우, reducer를 만들려면 switch를 기반으로 action을 분기하여 만들어야 함
    • 툴킷의 경우 builder를 인자로 받아 좀 더 간단하게 액션을 생성한다. 
  • createAction(): 액션을 쉽게 생성해준다.
    • 기존 redux의 경우, 하나의 function에서 Action Type과 Payload가 정의된다.
    • 툴킷의 경우, createAction으로 액션을 만들고 따로 매개변수를 받는다. 
  • createSlice()
    • 기존 redux의 경우, Reducer를 만들기 위해 액션 정의, 액션 함수 정의, switch를 통해 액션타입별 코드 정의필요
    • 툴킷의 경우, createSlice를 통해 간단하게 reducers 안에 액션을 생성하고 정의한다. 중복되는 변수 사용 줄어듦
    • 내가 툴킷을 썼을 때는 createReducer를 사용하지 않고 createSlice 내에서 reducer와 action을 생성하여 사용했다. 추가 설명을 하자면, createSlice 메서드는 `name`, `initialState`, `reducers` 세 가지 매개변수를 받는다. `name`매개변수는 slice의 이름을 정의하고, `initialState` 매개변수는 slice의 초기상태를 정의한다. `reducers`는 slice에서 사용할 reducer로직과 액션 생성자를 정의한다. createSlice메서드가 반환하는 값은 reducer와 action 객체이다. 
  • createAsyncThunk: 프로미스 기반의 액션들을 dispatch 하는 thunk를 생성한다.
  • createEntityAdapter: 스토어에서 정규화된 데이터를 관리하기 위해 재사용 가능한 리듀서 및 셀렉터 세트를 생성한다.
    • 이건 사용해본 적이 없음

RTK Query 기능

  • 데이터 패칭과 캐싱 로직은 Redux Toolkit의 createSlice와 createAsyncThunk API 위에서 동작한다.
  • Redux Toolkit은 UI 독립적이기 때문에 RTK Query의 기능들은 모든 UI 계층에서 사용가능하다.
  • API 엔드포인트는 인자로부터 쿼리 파라미터를 생성하고 캐싱을 위해 응답을 변환하는 방법을 포함해서 미리 정의된다.
    • 하나의 모듈을 중심으로 createApi를 통해 관련 코드들을 모두 작성할 수 있어 유지보수 편리성 기대, `one API slice per baseURL` 권장
  • RTK Query는 데이터 패칭 프로세스를 캡슐화해서 data와 isLoading 필드를 컴포넌트에게 제공하고 컴포넌트가 mount, unmount시 캐시된 데이터의 라이프타임을 관리하는 React hook을 제공한다. 
    • 개인적으로 react query와 매우 비슷하다고 생각했는데 아니나 다를까 모두 서버의 데이터를 가져와 효율적으로 캐싱하는 것이 주요 기능이라고 한다. 대신 마운트 언마운트시 캐시된 데이터의 라이프타임을 관리할 수 있는 긴으을 제공하는게 매력적이라고 생각한다. 한 곳에서 API와 관련된 모든 작업을 처리하기 때문에 react-query보다 자유도가 낮지만 그 부분이 장점이 될 수도 있다. 하나의 모듈에서 서버와의 통신을 거의 전부 해결할 수 있다는 점이 큰 장점이지만 react query와 비교했을 때 참고 레퍼런스가 적고, 기능이 부족하다는 단점을 가진다.
    • RTK query는 제대로 사용해본적이 없지만 react-query를 사용했을 때, 그렇게 어렵다는 점은 못 느꼈고 다른 http 비동기 통신 라이브러리(e.g. axios)와 결합하여 쓸 수 있어서 좋았다. 하지만, 위에 말했던 단점처럼 자유도가 높고 그로인해 어떻게 로직을 분리해야할 지 고민을 하게 됐다. 또한, react-query에서 무한 스크롤을 구현할 때 컴포넌트와 로직이 뒤엉켜(UI 독립적이진 않은가..?) 코드 분리를 못하는 상황이 발생했다. 이 부분은 내가 미흡해서 분리를 못했을 가능성이 크다. 
    • 비슷한 툴(rtk-query vs react-query vs apollo vs urql)끼리 비교한 표 redux toolkit docs
 

Comparison with Other Tools | Redux Toolkit

RTK Query > Comparison: Compares features and tradeoffs vs other similar tools

redux-toolkit.js.org

 

RTK Query: 스토어와 API 서비스 설정하기 (사용 방법)

 

1. API 서비스 생성하기

예시코드(TS도 지원한다.)

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Define a service using a base URL and expected endpoints
export const pokemonApi = 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 endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
  • RTK Query를 사용할 때, 전체 API를 한 곳에 정의한다. swr 나 react-query같은 라이브러리와 가장 많이 다른 점이다. 
Q. 그렇다면 RTK Query는 react-query처럼 서버 데이터 관리를 주로하는 라이브러리인가?
A. 또 그거는 아닌 거 같다. Redux Toolkit의 일부로서 Redux를 기반으로 클라이언트 측에서 API 호출 및 관리를 쉽게 처리하기 위한 라이브러리라고 한다. React Query는 전역 상태 관리와 데이터 캐싱을 제공하여 UI와 관련된 상태를 관리하는데 중점을 둔다. 반면에 RTK Query는 Redux의 강력한 상태 관리 기능과 API 호출 및 데이터 관리를 통합하여 클라이언트 측에서 API테이터를 관리하는 데 중점을 둔다. 

Redux Toolkit이 Redux를 추상화하고 Redux를 사용하는 방식을 단순화하는 것에 집중하기 때 문에 두 라이브러리의 느낌이 많이 다르다. (기존 redux의 보일러 플레이트 외 지켜야 하는 엄격한 규칙 등이 불편했기 때문에)

+ 2023 05 12 하지만 공식 홈페이지에서 비교군으로 react-query를 둔 것으로 봐서는(다른 강의에서도 react-query와 swr가 비교군이라고 언급하는 것을 들었음) 클라이언트 서버 데이터 상태관리 툴이라고 봐도 될 거 같다.

 

공식 홈페이지 RTK Query TIP

  • 일반적으로, 애플리케이션에 필요한 베이스 URL당 하나의 API 슬라이스를 가져야 합니다. 예시로 만약 사이트에서 /api/posts와 /api/users에서 데이터를 가져와야 한다면 /api를 베이스 URL로 하는 하나의 API 슬라이스를 만들고 posts와 users로 엔드포인트를 나누어야 합니다. 이러면 endpoints와의 관계를 tag로 정의해서 자동 데이터 리패칭 기능을 효과적으로 활용할 수 있습니다.
  • 유지보수적 관점에서, 하나의 API 슬라이스에 엔드포인트들을 포함하면서 엔드포인트들을 여러개의 파일에 나누어 정의하고 싶을 수도 있습니다. 코드 스플리팅에서 어떻게 injectEndpoints 프로퍼티를 사용해서 여러 파일들에서 하나의 API 슬라이스로 API 엔드포인트를 주입할 수 있는지 알아보세요.

 

2. 스토어에 서비스 추가하기

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'

export const store = 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 customization
setupListeners(store.dispatch)

3. 어플리케이션을 Provider로 감싸기

src/index.jsx

import * as React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import store from './app/store'

const rootElement = document.getElementById('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

 

4. 컴포넌트에서 쿼리 사용하기

서비스를 정의하면 hooks를 가져와서 요청을 생성할 수 있다.

src/App.jsx

import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'

export default function App() {
  // Using a query hook automatically fetches data and returns query values
  const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
  // Individual hooks are also accessible under the generated endpoints:
  // const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')

  return (
    <div className="App">
      {error ? (
        <>Oh no, there was an error</>
      ) : isLoading ? (
        <>Loading...</>
      ) : data ? (
        <>
          <h3>{data.species.name}</h3>
          <img src={data.sprites.front_shiny} alt={data.species.name} />
        </>
      ) : null}
    </div>
  )
}

요청을 생성할 때 여러 방법으로 상태를 추적할 수 있다. `data`, `status`, `error`로 알맞는 UI를 렌더링할 수 있다. 또한, useQuery는 유틸리티 불리언 값인 `isLoading`, `isFetching`, `isSuccess`, `isError`로 가장 최근의 요청에 대한 값을 제공한다!!(react-query가 제공하는 기능과 거의 비슷하다)

 

RTK Query는 어떤 컴포넌트든 같은 쿼리를 구독하면 항상 같은 데이터를 사용할 수 있도록 보장한다.

RTK Query는 자동으로 중복 요청을 제거하기 때문에 in-flight 요청과 성능 최적화에 대해 걱정할 필요없다.

4개(2개는 같은 내용 요청, 나머지 2은 각각 다른 endpoint로 다른 내용 요청한다 했을 때)의 구독된 컴포넌트가 있지만 2개의 컴포넌트에서 같은 내용을 요청한다고 했을 때, 2개의 내용은 겹치기 때문에 오직 3개의 요청만을 네트워크 탭에서 확인할 수 있다.  => 예시 코드 링크

 

all ref: https://redux-toolkit.js.org/tutorials/rtk-query

 

RTK Query | Redux Toolkit

 

redux-toolkit.js.org

 

댓글