[React] Redux Toolkit 과 RTK Query 정리

Nowod_K·2022년 9월 6일
7

출처 : https://redux-toolkit.js.org/introduction/getting-started

Redux Toolkit?

리덕스 팀에서 만든 공식적인 Redux Tool이다. 홈페이지를 가보면 아래와 같이 설명이 되어있는데 자신감을 엿볼 수 있다.

The official, opinionated, batteries-included toolset for efficient Redux development
'Redux 개발을 위한 공식적인, 독단적인, 배터리포함 ToolSet'

1. 공식적인

  • Redux 팀에서 만든 공식적인 라이브러리.

2. 독단적인

  • 스토어 설정을 위한 default를 제공하며 가장 일반적으로 사용되는 Redux 기능들을 포함. (Redux의 Default를 제공하는 자신감!!?)

3. 배터리포함!! (관용구적 표현임)

  • Redux Toolkit만 사용해도 아무 문제 없다!

Why Redux Toolkit?

기존 Redux의 문제점

  1. "Redux Store 설정이 너무 복잡하다"
  2. "Redux를 효율적으로 쓰려면 많은 패키지가 필요하다."
  3. "Redux에는 너무 많은 boilerplate code가 있다."

-> Redux Toolkit을 사용하면 이러한 문제점을 해결하고, 유지보수가 쉽다!

Redux Toolkit 제공 기능

기본기능

configureStore(): createStore를 랩핑하여 간단하게 store를 만들어줍니다.

createReducer(): 복잡하게 switch 문을 작성하는 대신 간단하게 reducer를 만들어줍니다.

createAction(): 간단하게 액션들을 만들어줍니다.

createSlice(): 리듀서 함수의 객체, 슬라이스 이름, 초기 상태 값을 받아 해당 액션 생성자와 액션 유형을 가진 슬라이스 리듀서를 자동으로 생성합니다.

createAsyncThunk: 프로미스 기반의 액션들을 dispatch 하는 thunk를 생성합니다.

createEntityAdapter: 스토어에서 정규화된 데이터를 관리하기 위해 재사용 가능한 리듀서 및 셀렉터 세트를 생성합니다.

RTK Query 기능

출처 : https://junsangyu.gitbook.io/rtk-query/overview

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

  • 데이터 패칭과 캐싱 로직은 Redux Toolkit의 createSlice와 createAsyncThunk API 위에서 동작합니다.

  • Redux Toolkit은 UI 독립적이기 때문에 RTK Query의 기능들은 모든 UI 계층에서 사용할 수 있습니다.

  • API 엔드포인트는 인자로부터 쿼리 파리미터를 생성하고 캐싱을 위해 응답을 변환하는 방법을 포함해서 미리 정의됩니다.

  • RTK Query는 데이터 패칭 프로세스를 캡슐화해서 data와 isLoading필드를 컴포넌트에게 제공하고, 컴포넌트가 mount, unmount시 캐시 된 데이터의 라이프타임을 관리하는 React hook을 제공합니다.

Redux와 Redux Toolkit 비교

Reducer 생성

Redux : switch 기반으로 Action을 분기하여 Reducer를 만든다.

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO': {
      return state.concat(action.payload)
    }
    case 'TOGGLE_TODO': {
      const { index } = action.payload
      return state.map((todo, i) => {
        if (i !== index) return todo

        return {
          ...todo,
          completed: !todo.completed,
        }
      })
    }
    case 'REMOVE_TODO': {
      return state.filter((todo, i) => i !== action.payload.index)
    }
    default:
      return state
  }
}

Redux Toolkit : builder를 통해 좀더 간단하게 액션을 생성한다.

const todosReducer = createReducer([], (builder) => {
  builder
    .addCase('ADD_TODO', (state, action) => {
      // "mutate" the array by calling push()
      state.push(action.payload)
    })
    .addCase('TOGGLE_TODO', (state, action) => {
      const todo = state[action.payload.index]
      // "mutate" the object by overwriting a field
      todo.completed = !todo.completed
    })
    .addCase('REMOVE_TODO', (state, action) => {
      // Can still return an immutably-updated value if we want to
      return state.filter((todo, i) => i !== action.payload.index)
    })
})

Action 생성

Redux : 하나의 function에서 Action Type과 Payload가 정의된다.

function addTodo(text) {
  return {
    type: 'ADD_TODO',
    payload: { text },
  }
}

Redux Toolkit : createAction으로 액션을 만들고 따로 매개변수를 받는다.

const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })

/* Reducer와 조합 */
const actionCreator = createAction('SOME_ACTION_TYPE')

const reducer = createReducer({}, (builder) => {  
  builder.addCase(actionCreator, (state, action) => {})
})

createSlice

Redux : Reducer를 만들기 위해 액션 정의, 액션 함수 정의, switch를 통해 액션타입 별 코드 정의를 진행한다.

// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'

// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'

export function addPost(id, title) {
  return {
    type: CREATE_POST,
    payload: { id, title },
  }
}

// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'

const initialState = []

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    case CREATE_POST: {
      // omit implementation
    }
    default:
      return state
  }
}

Redux Toolkit : createSlice를 통해 간단하게 reducers안에 액션을 생성하고 정의한다. 중복되는 변수 사용이 줄어든다.

const postsSlice = createSlice({
  name: 'posts',
  initialState: [],
  reducers: {
    createPost(state, action) {},
    updatePost(state, action) {},
    deletePost(state, action) {},
  },
})

console.log(postsSlice)
/*
{
    name: 'posts',
    actions : {
        createPost,
        updatePost,
        deletePost,
    },
    reducer
}
*/

const { createPost } = postsSlice.actions

console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
})

RTK Query

위의 Redux Toolkit의 간단하고 파워풀한 Reducer 생성을 토대로, 거기에 데이터를 효과적으로 가져오도록 한다.

reducer 생성

  1. createApi를 이용하여 reducer를 만듭니다.
    -> reducerPath는 이 reducer의 이름입니다. configureStore에서 사용합니다.
    -> endpoints는 API 수행 단위입니다. BaseUrl 기반 API를 분리하는 목적입니다.
  2. reducer가 생성되면 자동으로 "use + endpoints 단위 + Query" 훅이 생성됩니다.
  3. configureStore에 reducer를 정의합니다.
  4. 앱 최상단에 Provider를 생성하여 Store를 추가합니다.
  5. 필요한 컴포넌트에서 자동생성된 훅을 사용하여 data를 가져옵니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
//createApi(): RTK Query 기능의 코어입니다. 데이터를 패치하고 변환하는 설정을 포함해서 엔드포인트들에서 어떻게 데이터를 패치하는지 정의할 수 있습니다.

//fetchBaseQuery(): 간단한 요청을 위한 fetch의 래퍼입니다. 대부분의 사용자에게 createApi의 baseQuery로 권장합니다.

const baseUrl = 'api_test.com'

// 이렇게 createRequest를 만들어서 사용하지 말것!
// 이미 baseQuery는 아래와 같은 형태를 지원하기에 이런식으로 만들필요가 없다.
//개발자 권고에 따라 삭제 : const createRequest = (url) => ({url})

export const someApi = createApi({
    reducerPath: 'someApi',
    baseQuery: fetchBaseQuery({ 
      baseUrl,
//    either you can just set `headers` here:
//    headers: { "Accept": "application/vnd.api+json" }

//    or you use `prepareHeaders` where you can do some calulations and have access to stuff like `getState` or the endpoint name
      prepareHeaders: (headers, { getState, endpoint, type, forced }) => {
         headers.set("Accept", "application/vnd.api+json")
         return headers
      }}),
    endpoints: (builder) => ({
        getSome: builder.query({
            query: (count) => {url : `/some?limit=${count}`}
        }),
        getSomes: builder.query({
            query: ({ coinUuid, timePeriod }) => {url :`/somes`}
        }),       
    })
})

export const { useGetSomeQuery, useGetSomesQuery } = someApi;
//store 생성
export default configureStore({
    reducer: {
        [someApi.reducerPath]: someApi.reducer,
    },
});
//root에 정의
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);
// 컴포넌트에서 활용
const someComponents = ({count}) => {
  const { data, isFetching } = useGetSomeQuery({count});

  if(isFetching) return 'Loading...'
  
  return (
  	<>
    	<p>Good!</p>
    </>
  )
  
}

RTK Query 사용 후기

개인적으로 느낀 가장 큰 장점은 state 변경에 따라 자동으로 다시 데이터를 불러오는 점이다!

아래와 같이 컴포넌트에서 state의 값을 RTK-Query의 훅에 전달하는 경우가 있는데, 이때 RTK-Query에서 해당 state의 변경을 자동으로 읽어서 다시 데이터를 가져온다. 굳이 useEffect를 사용하지 않아도, 필요한 부분을 다시 랜더링해오는 기능은 정말 편하고 유용했다.

물론 데이터 캐싱도 자동으로 되는 것도 정말 좋았지만, 여러모로 사용자 관점에서 편하고 효율적으로 사용할 수 있다는 생각이 들었다.

// 컴포넌트에서 활용
const someComponents = ({count}) => {
  const [count, setcount] = useState(0);
  const { data, isFetching } = useGetSomeQuery({count});
profile
개발을 좋아하는 마음과 다양한 경험을 토대로 좋은 개발자가 되고자 노력합니다.

2개의 댓글

comment-user-thumbnail
2022년 10월 14일

Hi,
I'm the author of RTK Query.

Please do not use that kind of createRequest helper function.

That is taken from a tutorial that grossly misunderstands what baseQuery is made for (and spreading to other tutorials now).

Essentially, baseQuery is already a createRequest function like this - the return value of an endpoint's query function will be passed as first argument into baseQuery, which will in the case of fetchBaseQuery then call fetch.

So please use fetchBaseQuery correctly instead here:

export const api = createApi({
    baseQuery: fetchBaseQuery({ 
      baseUrl,
//    either you can just set `headers` here:
//    headers: { "Accept": "application/vnd.api+json" }

//    or you use `prepareHeaders` where you can do some calulations and have access to stuff like `getState` or the endpoint name
      prepareHeaders: (headers, { getState, endpoint, type, forced }) => {
         headers.set("Accept", "application/vnd.api+json")
         return headers
      }
     }),
    endpoints: (builder) => ({
        getSomeStuff: builder.query({
          query: () => { url: `/someStuff` }
// or the short notation: if you only have an `url` just pass a string
//        query: () => `/someStuff`
        }),
    })
});

I would greatly appreciate it if you could change that up in this tutorial as this is essentially a bad practice and we do not want it to spread.

1개의 답글