티스토리 뷰

🚀 일단 다시 Redux 라는 것을 쓰는 이유를 기억해보자

리덕스는 전역 상태 관리 라이브러리 중 하나입니다. 컴포넌트에서 컴포넌트로 props를 통해 전달을 한다고 하면 깊이가 깊어질 수록 불필요한 props 전달이 생길 수 있습니다. 이러한 구조는 유지보수 또는 props 추적을 힘들게 하는 Props drilling을 야기하게 됩니다.

위 그림은 기존의 props를 통한 상태 관리 방식입니다. 위 그림처럼 상태 변경이 한 번 일어났을 뿐인데 해당 데이터를 변경하기 위해 또 다른 상태 변경이 여러 번 일어나고 있습니다. 이러한 구조는 side effect를 야기할 수 있으며, 발생한 오류를 추척하기도 힘들어지게 됩니다. 대체 어디에서부터 에러가 났는지 계속 타고타고 올라가봐야 알 수 있을 것 입니다.

그러나 리덕스는 Reducer와 Store를 통해 상태 변경 과정을 전역에서 관리하여 기존의 문제를 해결했습니다. 이것은 복잡한 상태 관리를 매우 효율적이고 간편하게 변경하여 오류 발생률을 낮추게 합니다. state를 전역에서 관리하고 있기 때문에 부모 컴포넌트에서 받는 형식이 아니라 Store 라는 하나의 저장소에서 그 값을 가져오기 때문에 에러가 발생하더라도 Store 혹은 Reducer에 문제가 있는지 확인하면 됩니다.

 

🚀 이제는 Redux 말고 Redux-Toolkit을 사용하는 이유

기존 Redux에는 다음과 같은 3가지 문제점이 있습니다.

 

  • Redux 스토어 구성이 너무 복잡하다
  • Redux에서 유용한 작업을 수행하려면 많은 패키지를 추가해야 한다
  • Redux에는 너무 많은 사용구 코드가 필요하다

위 문제점을 Redux-Toolkit을 사용하면 해결할 수 있습니다.

 

🚀 Redux-Toolkit이 제공하는 것

💼 configureStore

간단하게 Store를 만들 수 있도록 도와줍니다. 기존 Redux에서는 createStore에서는 combinReducers로 여러 Reducer를 합쳐준 뒤 rootReducer를 만들어 다시 또 createStore 안에 넣어줘야 했습니다.

하지만 configureStore에서는 rootReducer 없이 reducer라는 키값 안에 reducer들을 넣어주면 됩니다.

 

기존 Redux에서

const rootReducer = combineReducers({
  counter: counterReducer,
  todoList: todoReducer
});

const store = createStore(rootReducer);

 

Redux-Toolkit에서

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    todoList: todoSlice.reducer,
  },
});

🪄 createReducer

switch문을 작성하는 것 대신 간단하게 Reducer를 만들 수 있도록 도와줍니다. 기존 Redux에서는 state와 action을 인자로 받아서 switch-case문으로 action의 type에 따라 나눠줘야 했고, 기존의 state 불변성의 원칙을 반드시 지켜야 했습니다.

하지만 Redux-toolkit에서는 미리 세팅되어 있는 미들웨어 덕분에 콜백함수를 통해 불변성의 원칙을 알아서 지키게 되어있습니다.

💪 createAction

Reducer에 작성한 것들을 기반으로 Action들을 만들어 줍니다. 기존 Redux에서는 해당 기능이 없어서 createAction이라는 함수를 선언해서 타입을 인자로 넣어 사용했었습니다. 하지만 Redux-Toolkit에서는 알아서 제공해줍니다.

🎁 createSlice

reducer의 이름, initailState, reducers 등을 간편하게 만들 수 있도록 도와줍니다. createSlice를 통해 여러 기능을 편리하게 설정이 가능합니다.

🛰️ createAsyncThunk

createAction을 비동기로 만들 수 있도록 도와줍니다. Thunk는 미들웨어입니다. 만약 클라이언트가 reducer에게 데이터를 달라고 get 요청을 보내게 되면 reducer는 비동기 처리를 하지 못하기 때문에 thunk라는 미들웨어에게 서버와 비동기 통신을 하고 답을 달라고 한 뒤 대기 상태가 됩니다. 우선 그 대기 상태라는 것을 클라이언트에게 알려주고, 미들웨어가 서버와 비동기 통신을 하고 성공을 했으면 성공을 했다고, 실패를 했으면 실패를 했다고 reducer에게 다시 알려줍니다. 그러면 이 대답을 다시 또 reducer는 클라이언트에게 알려주는 구조입니다.

🚀 적용해보고 사용해보자

 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

1.  라이브러리 설치

$ npm install @reduxjs/toolkit react-redux

2. Store를 만든다 : store에는 reducer, middleware가 들어간다

이 store는 전역에서 사용하게 됩니다. store에는 reducer, middleware가 들어갑니다.

이때 Store는 createStore()라는 함수를 통해 만듭니다.

import { configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'

export const store = configureStore({
  reducer: {},
  devTools: process.env.NODE_ENV === 'development', // true일 때만 사용
  middleware: defaultMiddleware => { // defaultMiddleware : rtk가 기본적으로 가지고 있는 미들웨어들
    if (process.env.NODE_ENV === 'development') {
      return [...defaultMiddleware(), logger]
    }
    return defaultMiddleware()
  },
})

아직 reducer들을 작성하지 않았기 때문에 reducer 자리는 비워두었습니다.

3. createSlice(name, initailState, reducers, extraReducers)를 작성

이제 store에서 reducer안에 채울 reducer는 createSlice() 함수 안에서 작성됩니다. createSlice에는 name, initialState, extraReducers를 작성합니다.

 

  • name :  해당 state의 이름입니다. 이 state와 관련된 로직들은 이 이름을 접두사로 사용합니다. redux-toolkit의 경우 action을 현재 slice의 name + action name으로 자동 지정해줍니다.
  • initailState : 해당 state의 초기 값입니다.
  • reducers : slice 안에서 사용할 reducer들을 만들 수 있습니다.
  • extraReducers : slice에서 만들어진 reducers에 의한 action, reducer가 아닌 외부에서 만들어진 action(ex. 비동기 통신으로 받은 데이터)를 통해 slice에서 사용하는 initialState에 변경을 가하는 경우 처리받는 reducer입니다.

builder는 미들웨어의 진행 상태이며, 그 진행상태에 따라 reducer를 실행하겠다고 코드를 작성

const initialState = {
	todos: [],
	addTodoState: {
		loading: false,
		done: false,
		err: null,
	},
    // ...
}

export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  extraReducers: builder => {
    builder.addCase(addTodo.pending, state => {
      state.addTodoState.loading = true
    })
    
    builder.addCase(addTodo.fulfilled, (state, action) => {
      state.todos.unshift(action.payload)
      state.addTodoState.loading = false
      state.addTodoState.done = true
      state.addTodoState.err = null
    })
    
    builder.addCase(addTodo.rejected, (state, action) => {
      state.addTodoState.loading = false
      state.addTodoState.done = true
      state.addTodoState.err = action.payload
    })
    
    // ...
  }
})

위 코드에서 addTodo라는 것은 아직 작성하지 않았습니다. 미리 말하자면, 이 addTodo는 thunk로 작성합니다. 즉, 미들웨어를 의미하는데요. 

 

미들웨어의 상태는 총 3가지로 구성됩니다.

 

  1. pending: 대기상태
  2. fulfilled : 성공
  3. rejected : 실패

따라서 위 코드로 다시 돌아가보면, builder에 addCase 한다는 부분은 기존 Redux에서 switch문으로 case를 추가한 부분과 동일하다고 생각하면 되겠습니다. 그리고 builder는 비동기 로직의 진행 상태를 의미하며, 이 진행 상태에 따라서 콜백함수 형태로 되어있는 리듀서를 실행시킬 수 있습니다.

그래서 addTodo라는 미들웨어가 지금 pending 상태이면, 우측과 같은 콜백함수(=리듀서)를 실행하겠다고 코드를 작성한 것입니다.

 

4. 미들웨어를 작성(createAsyncThunk) : 미들웨어는 서버에게 요청을 보내고 받은 값을 리턴한다

위에서 계속 이야기했지만, 미들웨어는 비동기 통신을 하지 못하는 reducer 대신에 서버에게 요청을 보내고 그 진행 상태를 reducer에게 알려줍니다.즉, 지금 미들웨어를 작성한다라고 하는 말은 서버에게 요청을 하고 그 받은 응답을 그대로 리턴하는 로직을 작성하면 됩니다.

미들웨어를 작성할 때는 createAsyncThunk라는 함수를 통해 작성하는데요. 다음과 같은 예시처럼 작성하면 됩니다.

export const addTodo = createAsyncThunk('todo/addTodo', async (todo) => {
  const res = await axios.post('/api/todo', todo)
  return res.data
})

위 코드를 보면서 결론부터 말하면 결국에는 우리는 addTodo라는 함수를 통해서 todos에 todo를 하나 add하고 싶으면 어디에선가 addTdoo(todo) 이렇게 사용하게 될 겁니다. 따라서 'addTodo' 라는 함수명 자체가 기존 redux에서 createAction처럼 액션 타입이 됩니다. createAsyncAction의 첫번째 인자는 type입니다. 우리가 createSlice에서 name을 적었었죠? 그 slice와 관련된 로직임을 나타내기 위해 위와 같은 방법으로 작성하면 됩니다.

5. 이제 reducer가 완성되었으니, slice안에 있는 reducer를 store안에 담아주자

다시 store를 만들었을 때를 생각해보면, store는 configureStore()를 통해 만들었고, 그 안에는 reducer, devTools, middlesare가 담겼었습니다. 처음에는 우리가 만든 reducer가 없어서 빈 객체로 두었었습니다. 이제 좀전에 만들었던 slice안에 있는 reducer를 이 안에 담아주면 끝입니다.

import { configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'

export const store = createStore({
  reducer: {
    todo: todoSlice.reducer
  },
  devTools: process.env.NODE_ENV === 'development', 
  middleware: defaultMiddleware => { 
    if (process.env.NODE_ENV === 'development') {
      return [...defaultMiddleware, logger]
    }
    return defaultMiddleware()
  },
})

6. 사용해보기

지금 todo를 add 하고 싶은 상황이라고 해보겠습니다.

좀전에 미들웨어로 작성한 addTodo(todo) 이런 식으로 미들웨어에 데이터를 보내서 서버에게 요청을 보내고 그 받은 응답을 리턴해서 reducer에게 보낸다고 했습니다.

따라서 사용할 때도 이 미들웨어를 import해서 이 미들웨어에게 보내고 싶은 데이터를 보내면 됩니다.

그리고 그 state 혹은 state의 상태를 store에서 꺼내고 싶다면 기존 redux에서와 동일하게 useSelector를 사용하면 됩니다.

function App() {
  const todoList = useSelector(store => store.todo.todos)
  const state = useSelector(store => store.todo.addTodoState)
  const dispatch = useDispatch()
    
  const onClickSubmit = () => {
    dispatch(addTodo(data))
  }

  useEffect(() => {
    if(state.done && !state.err) {
      alert("데이터 전송을 완료했습니다!")
    }
  })
}

 

728x90
LIST
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함