React _ RTK Query 기본 이해하기

kyle kwon·2022년 11월 30일
2

React

목록 보기
13/15
post-thumbnail

Prologue

이번에는 RTK(Redux Toolkit) Query에 대해서 이해해보겠습니다. RTK Query 공식문서를 번역하면서 정리한 내용입니다.

RTK Query는 Redux Toolkit을 설치하고 나면, 내장되어 있어 따로 설치할 필요가 없습니다. 선택적으로 사용 가능합니다.



RTK Query를 사용하는 이유

우리가 서버와 데이터를 주고 받는 과정에서, 클라이언트 사이드(프론트엔드)에서는 웹 어플리케이션에 데이터를 로드하는 경우를 API에 맞게 CRUD 작업과 캐싱 처리를 하는 로직을 직접 설계하는 경우들이 다반사입니다.
하지만, RTK Query를 사용하게 되면, 공식문서에서 언급하길, 강력한 데이터 가져오기, 캐싱 도구로 우리가 직접 설계할 필요 없이 쉽게 데이터 처리 및 캐싱을 할 수 있도록 도와준다고 합니다.

일반적인 데이터 처리 로직은 화면에 데이터를 표시하기 위해서 서버에서 데이터를 가져오고, 클라이언트 사이드에서 사용자의 조작에 의해 데이터가 업데이트 되고, 업데이트한 데이터를 서버로 보내고, 클라이언트 사이드에 캐시된 데이터를 서버의 데이터와 동기화 상태로 유지해야 합니다. 이런 과정이 현대의 웹 어플리케이션 구현에서는 더욱 복잡해집니다.

예를 들어,

  1. UI 스피너를 표시하기 위한 로드 상태 추적
  2. 동일한 데이터에 대한 중복 요청 방지
  3. 사용자가 UI와 상호작용 시 캐시 수명 관리
  4. UI가 빠르게 느껴지도록 낙관적 업데이트

제가 생각하는 RTK의 강력한 장점은 동일한 데이터에 대한 중복 요청을 제어하는 코드를 우리가 직접 구현할 필요가 없으며, 별도의 패키지 설치 없이 ReduxToolkit을 설치하면 Add-on 형태로 사용가능하다는 점인 것 같습니다.
또, 현재 TypeScript를 도입한 상태에서, RTK Query 자체가 TypeScript로 구현되었기에 TS 경험에 도움을 줄 것이라고 생각합니다.



RTK Query Essentials

RTK Query Essential 1 : createAPI()

RTK 쿼리의 핵심 기능입니다. 내부적으로 다음과 같은 내용들을 정의할 수 있습니다.

  1. 데이터를 가져오고 변환하는 방법에 대한 정의
  2. 서버와 직접적으로 통신하게 되는 지점(서버의 기본 URL)인 endPoints(끝점)에 대한 정의
  3. reduxToolkit의 createSlice()와 같이 unique한 이름값인 reducerPath를 정의

코드를 살펴보면 다음과 같습니다.

// src/api/pokemonApi.ts
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'

export interface PokemonType {
  id: number;
  name: string;
  order: number;
  species: {
    name: string;
    url: string;
  };
  sprites: {
    front_default: string;
    front_shiny: string;
  };
  stats: {
    base_stat: number;
    effort: number;
    stat: {
      name: string;
      url: string;
    };
  }[];
  weight: number;
}


export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({baseUrl : 'https://pokeapi.co/api/v2/'}),
  endPoints: (builder) => ({
    getPokemonByName : builder.query<PokemonType, string>({
      query: (name) => `pokemon/${name}`,
    }),
  },
})

export const { useGetPokemonByNameQuery } = pokemonApi;

위의 코드에서 fetchBaseQuery()는 데이터 요청 헤더와 응답을 axios와 같은 라이브러리와 유사한 방식으로 자동으로 파싱을 해주는 가벼운 fetch wrapper로 별도로 axios와 같은 라이브러리를 설치하고 사용할 필요가 없습니다.
다만, fetch를 사용하는 경우에는 폴리필이 필요할 수 있다고 공식문서에서 언급하고 있습니다.

  • reducerPath는 redux Toolkit에서 고유한 slice 이름을 설정해야 하는 createSlice와 같이 사용하는 고유한 이름과 같습니다.

  • endPoints의 경우 서버와 직접적으로 맞닿는 지점(서버의 기본 URL - API)으로 서버에 대대응해서 행동하려는 action의 모음이라고 생각하면 좋습니다. 빌더 구문을 사용하여 객체로 정의합니다. 기본 유형으로는 builder.query 그리고 builder.mutation이 있습니다.

    • 마치 createSlice에서 원하는 해당 actiondispatch 될 때, 실행될 reducer 함수를 reducers에 정의해주는 것처럼, 해당 createApi의 특정 행동(action)의 이름을 정의해주는 곳이라고 생각하면 될 것 같습니다.
  • 코드 제일 하단의 useGetPokemonByNameQuerygetPokemonByName을 실행하는 hook 함수라고 생각하면 쉽습니다.builder.query가 실행되어 baseUrl 뒤에 name이라는 string을 덧붙여 주어, 예를 들어, https://pokeapi.co/api/v2/pokemon/bulbasaur가 되면 해당 API(url)로 데이터를 요청에서 가져오는, 즉 서버와 통신이 이루어지는 접점이라고 생각하면 됩니다.



RTK Query Essential 2 : configureStore에 해당 API slice와 미들웨어 추가

코드를 살펴봅시다.


// src/store.ts

import {configureStore} from '@reduxjs/toolkit';
import {setUpListeners} from '@reduxjs/toolkit/query';
import {pokemonApi} from './api/pokemonApi';

export const store = configureStore({
  	reducer :{
      [pokemonApi.reducerPath] : pokemonApi.reducer;
    },
  middleware: (getDefaultMiddleware) => {
    	getDefaultMiddleware().concat(pokemonApi.middleware)
  }
})

setupListeners(store.dispatch);

위의 코드에서, createSlice를 이용해서 만든 slice로부터 해당 reducer들을 저장하는 configureStore를 같이 사용하고 있는 것을 볼 수 있습니다.

[pokemonApi.reducerPath]는 마치, createSlice에서 지정한 유니크한 name프로퍼티를 지정할 때와 유사하고, 해당 프로퍼티의 값으로 reducer을 할당해준 것과 같은 모습입니다.

middleware 프로퍼티의 경우 선택적으로 캐싱, 무효화등을 도와주는 기능을 활용할 수 있도록 합니다.

setUpListners(store.dispatch)는 선택적으로 같은 특정 이벤트에 대한 데이터를 다시 가져올 수 있도록 하는 옵션이라고 생각하면 됩니다.



RTK Query Essential 3 : Provider로 App 컴포넌트 감싸기

전역 상태 관리를 위해 Provider 컴포넌트로 전역 상태를 사용할 모든 컴포넌트들을 감싸서 사용했었던 것처럼, RTK Query를 사용하기 위해서도 현재 store 파일을 만들고, 감싸준 상태라면 그대로 사용하면 됩니다.

// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { store } from './store/store';
import { Provider } from 'react-redux';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
);

만약, 전역 상태 관리 대상인 store를 생성하지 않은 상태라면, APIProvider를 사용하면 됩니다. 다만, 기존의 Provider와 같이 사용할 수 없다는 점은 유의해야 합니다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ApiProvider } from '@reduxjs/toolkit/query/react';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <ApiProvider api={api}>
      <App />
    </ApiProvider>
  </React.StrictMode>,
);


리액트 컴포넌트에서 RTK Query 사용하기

// App.tsx
import React, { useState } from 'react';
import Pokemon from './components/Pokemon';

const pokemon = ['bulbasaur', 'pikachu', 'ditto', 'bulbasaur'];

function App() {
  const [changeInterval, setChangeInterval] = useState<number>(0);
  const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setChangeInterval(Number(e.target.value));
  };
  return (
    <div>
      <select onChange={handleSelectChange}>
        <option value={0}>Off</option>
        <option value={1000}>1s</option>
        <option value={5000}>5s</option>
      </select>
      <div>
        {pokemon.map((poke, index) => (
          <Pokemon key={`${poke}_${index}`} name={poke} pollingInterval={changeInterval} />
        ))}
      </div>
    </div>
  );
}

export default App;
// src/components/Pokemon.tsx
import { useGetPokemonByNameQuery } from '../api/pokemonApi';

interface PokemonProps {
  name: string;
  pollingInterval: number
}

const Pokemon = ({ name, pollingInterval }: PokemonProps) => {
  const { data, error, isLoading } = useGetPokemonByNameQuery(name, { pollingInterval });

  if (error) {
    return <>Oh, there was an error</>;
  }

  if (isLoading) {
    return <>Loading ...</>;
  }
  return (
    <div>
      {data ? (
        <>
          <h3>
            {data.id} : {data.species.name}
          </h3>
          <img src={data.sprites.front_shiny} alt={data.species.name} style={{ border: '1px solid #e5e5e5' }} />
        </>
      ) : null}
      <div>base-stat: {data?.stats[0].base_stat}</div>
      <div>weight: {data?.weight}</div>
    </div>
  );
};

export default Pokemon;

위의 코드에서 중점적으로 볼 사항은 const { data, error, isLoading } = useGetPokemonByNameQuery(name, { pollingInterval }); 입니다.

useGetPokemonByNameQueryendpoints에서 builder.query로 객체로 정의했던 pokemonByNameQuery를 담은 endpoint hook으로 2개의 파라미터(queryArg, queryOptions)를 필요로 합니다.

  • 첫 번째 파라미터는 builder.query로 객체로 정의햇던 pokemonByNameQuery에서 기본 baseUrl에 덧붙여질 string 값으로 사용되던 인자(위의 코드에서는 name)를 전달하는 곳입니다.

  • 두 번재 파라미터는 옵션값으로, 위의 코드에서는 pollingInterval이라는 값으로 활용되고 있습니다. 쿼리가 밀리초 단위로 지정된 간격으로 자동으로 다시 가져오도록 하는 옵션으로 기본값은 0이지만, 위의 select를 통해 조절하고 해볼 수 있습니다.


해당 옵션은 다음과 같이 더 있습니다.

  1. 건너뛰기 - 쿼리가 해당 렌더링에 대해 실행 중인 '건너뛰기'를 허용합니다. 기본값은 false
  2. pollingInterval - 쿼리가 밀리초 단위로 지정된 제공된 간격으로 자동으로 다시 가져올 수 있습니다. 기본값은 0
  3. selectFromResult - 후크의 반환된 값을 변경하여 반환된 하위 집합에 대해 렌더링 최적화된 결과의 하위 집합을 얻을 수 있습니다.
  4. refetchOnMountOrArgChange - 쿼리가 마운트 시 항상 다시 가져오도록 허용합니다( true제공된 경우). 동일한 캐시에 대한 마지막 쿼리(제공된 경우) 이후 충분한 시간(초)이 경과한 경우 쿼리를 강제로 다시 가져올 수 있습니다. 기본값은 false
  5. refetchOnFocus - 브라우저 창이 다시 포커스를 받으면 쿼리를 강제로 다시 가져올 수 있습니다. 기본값은 false
  6. refetchOnReconnect - 네트워크 연결을 다시 얻을 때 강제로 쿼리를 다시 가져올 수 있습니다. 기본값은 false

위의 해당 useGetPokemonByNameQuery hook이 반환하는 값은 다음과 같습니다.

  1. data
  2. currentData
  3. error
  4. isLoading
  5. isFetching
  6. isSuccess
  7. isError
  8. refetch

위의 코드에서 사용한 반환값은 data, error, isLoading이 있습니다. 주로 우리가 axios를 활용할 때, 데이터 처리에 대한 결과를 다음과 같이 3가지 정도로 분류합니다. 데이터를 받아오는 동안 loading 상태일 때 보여줄 요소, 그리고 화면에 렌더링할 data, 실패했을 때 보여줄 에러 메세지등으로 정의합니다.

따라서, 이 3가지를 활용하면 좋을 것 같습니다:)



Conclusion

이렇게 RTK Query를 사용하면 좋은 점은 무엇인지, 어떻게 활용할 수 있는지 기본적인 개념들을 공식문서를 바탕으로 정리하고, 간단하게 화면에 어떻게 보여지는 지 테스트 해보았습니다.
생각보다 쉽지 않은 주제라 어렵게 느껴지지만, 로그인/로그아웃/회원가입과 같은 기능을 현재 진행하고 있는 프로젝트에서 구현하고 있는 상황에서 RTK Query를 도입하기로 했는데 매우 도움이 될 것 같습니다.

참조문서

https://redux-toolkit.js.org/rtk-query/overview

profile
FrontEnd Developer - 현재 블로그를 kyledot.netlify.app으로 이전하였습니다.

0개의 댓글