Redux-Toolkit

meroriiDev·2023년 4월 5일
0

최근 한 프로젝트로 redux toolkit과 saga를 함께 사용하여야하는 상황이 생겼다. recoil을 알게된 이후로는 세팅할 수 있는 상황이라면 redux보다는 recoil을 사용해서 redux에 대해 잘 모르고 있었는데 이번 기회로 정리하면서 공부해보려고 한다.

Redux

먼저 redux란 현재 가장 활발하게 사용되고 있는 상태관리 라이브러리이다.
recoil, jotai처럼 편리하게 사용할 수 있는 라이브러리들이 많이 나왔고 신규 프로젝트의 경우 이러한 라이브러리를 사용하는 곳이 많아졌지만 아직도 redux는 가장 활발하게 사용되고 있고, 예전에 작업한 프로젝트의 유지보수를 위해서라면 redux에 대해서도 꼭 알고 있어야 한다.

Redux-Toolkit(RTK)

redux를 보다 편리하게 작성하기 위해 개발된 툴킷으로 공식 홈페이지에서도 RTK(redux-Toolkit)를 사용하는것을 권장한다.

redux를 사용할때는 불편한 것을 해결하기 위해 많은 라이브러리를 함께 사용하게 된다. 매번 액션을 하나하나 만들게 되면 너무 많은 코드가 생성되니 redux-actons을 사용하게 되고, 불변성을 지켜야하는 원칙 때문에 immer를 사용하게되고, store 값을 효율적으로 핸들링하여 불필요 리렌더링을 막기 위해 reselect를 쓰고, 비동기를 수월하게 하기위해 thunk나 saga를 설치하여 redux를 더 효율적으로 사용하게 된다.
즉 redux를 사용해서 상태관리를 하기 위해서는 수많은 라이브러리와 많은 양의 코드가 필요하다는 것이다.

그런데, redux-toolkit은 saga를 제외한 위 기능이 거의 모두 지원한다.

아래와 같은 리덕스의 문제점을 보완해준다.

  • 복잡한 Redux 저장소 구성을 어느 정도 해결해 줄 수 있다.
  • 자체적으로 immer를 내부적으로 도입해서 사용하고 있어 코드의 양이 줄어든다.
  • Redux에서 자주 사용하는 기능들을 모아두어(redux-thunk, redux devtool 등 디폴트로 제공) 패키지 의존성을 줄여준다.

configureStore()

store를 생성할때 사용된다.

기존 Redux에서는 Reducer를 생성하고 combineReducers로 여러 Reducer를 합쳐준 rootReducer를 만들고 그 다음 createStore로 store를 만들어 줘야 했다.

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

...

const store = createStore(rootReducer);

반면 configureStore를 사용하면 reducer를 객체형식으로 전달하여 바로 만들 수 있다.

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    todoList: todoSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(logger).concat(sagaMiddleware),
  devTools: process.env.NODE_ENV !== "production",
});

Store는 reducer를 객체 형식으로 전달하여 만들 수 있다.
createStore와 비슷하지만 {reducer: rootReducer}로 작성해야 한다는 차이점이 있다.
제공하는 모든 Redux 미들웨어를 추가하고, redux-thunk기본적으로 포함하고, Redux DevTools Extension을 사용할 수 있다.

createSlice()

리듀서 함수의 객체, 슬라이스 이름, 초기 상태 값을 받아 해당 액션 생성자와 액션 타입으로 슬라이스 리듀서를 자동으로 생성(name+action name)한다.

const comment = createSlice({
  name: "comment",
  initialState,
  reducers: {
    //일반action
    handlePage: (state, action) => {
      state.data.page = action.payload;
    },
    //비동기작업(saga)
  }
  extraReducers:{
  	//비동기작업(thunk)
  }
});

export const commentActions = comment.actions;
export default comment.reducer;

RTK에서는 comment의 actions를 export해 각각의 action에 접근할 수 있다.

state.data.list = action.payload;

이때 actions 내에서는 immer를 기본적으로 제공하기 때문에 필요한 부분만 수정해주어도 되어 간편하다.

createAsyncThunk

redux-toolkit에서는 react-thunk가 기본적으로 제공이 되는데 createAsyncThunk를 사용하면 된다.

export const getComment = createAsyncThunk(
  'comment/get',
  async (, thunkAPI) => {
    try {
      const response = await axios.get('/comments')
      return response.data
    } catch (error) {
      return thunkAPI.rejectWithValue(error)
    }
  }
)

이렇게 만든 액션은 리듀서에 연결할때는 reducers가 아닌 extraReducers에 연결해주어야 한다.

const comment = createSlice({
  name: "comment",
  initialState,
  reducers: {
  }
  extraReducers:(builder)=>{
  	builder.addCase(getComment.pending,()=>{
    })
    .addCase(getComment.fulfilled,()=>{
    })
    .addCase(getComment.rejected,()=>{
    })
  }
});

extraReducers에서 파라미터인 builder의 addCase를 사용해 각각의 case에 따른 처리를 등록해주어야 한다.
이때 case는 pending(대기), fulfilled(성공), rejected(실패) 3가지가 있다.

redux toolkit 세팅 예시코드

설치

//redux설치
npm install redux react-redux

//toolkit 설치
npm install @reduxjs/toolkit

//로그 확인을 위한 미들웨어 logger설치
npm install logger

store생성
/src/store/store.js

import { configureStore } from "@reduxjs/toolkit";
import { createLogger } from "redux-logger";

import commentReducer from "./reducer";

function createStore() {
  const store = configureStore({
    reducer: {
      comment: commentReducer,
    },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(logger),
    devTools: process.env.NODE_ENV !== "production",
  });
  return store;
}

const store = createStore();
export default store;

reducer
/src/store/reducer.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

const initialState = {
	loading: false,
    error: null,
    data: {
      list: [],
      edit: null,
      limit: 4,
      page: 1,
    }
};

export const getComment = createAsyncThunk(
  'comment/get',
  async (, thunkAPI) => {
    try {
      const response = await axios.get('/comments')
      return response.data
    } catch (error) {
      return thunkAPI.rejectWithValue(error)
    }
  }
);

const comment = createSlice({
  name: "comment",
  initialState,
  reducers: {
    findComment(state, action) {
      const item = state.data.list.find((item) => item.id === action.payload);
      state.data.edit = item;
    }
  },
  extraReducers:(builder)=>{
  	builder.addCase(getComment.pending,(state)=>{
    	state.loading = true;
    })
    .addCase(getComment.fulfilled,(state, action)=>{
        state.loading = false;
        state.data.list = action.payload;
    })
    .addCase(getComment.rejected,(state, action)=>{
        state.loading = false;
        state.data = null;
        state.error = action.payload;
    })
  }
});

export const commentActions = comment.actions;
export default comment.reducer;

0개의 댓글