본문으로 바로가기
 

Redux Toolkit | Redux Toolkit

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

redux-toolkit.js.org

 

회사에서 툴킷을 써서 부랴부랴 배워보기로 했다.

 

0. 설치

속이 다 시원하네

// immer, redux, redux-devtools-extension 자체 내장되어 있으므로 설치 ㄴㄴ
yarn add @reduxjs/toolkit react-redux

 

 

1. createStore너무 길다 => configureStore로 간편하게

 

기존의 redux의 createStore를 대체합니다. 

https://redux-toolkit.js.org/api/configureStore

 

기존의 createStore를 이용하여, 미들웨어, enhancer, store를 구성한 코드입니다. 꽤나 코드양이 많죠.

import rootReducer from "./_reducers";
const { createStore, applyMiddleware, compose } = require("redux");
const { asyncLogin } = require("./_actions/user");
const { createLogger } = require("redux-logger");

// middleware
const logger = createLogger();
const customLogger = (store) => (next) => (action) => {
  console.log("before action", store.getState());
  next(action); // 액션을 넘겨주지 않으면 리듀서로 액션을 진행하지 못합니다.
  console.log("after action", store.getState());
};
const thunkMiddleware = (store) => (next) => (action) => {
  // action이 객체가 아닌 함수인 경우 비동기라 가정하고 처리하자.
  if (typeof action === "function") {
    // action 함수에 next, getState 함수를 넘겨서 활용할 수 있도록 하자
    return action(next, store.getState);
  }
  next(action);
};

const enhancer = compose(
  applyMiddleware(thunkMiddleware, logger),
  window.__REDUX_DEVTOOLS_EXTENSION__
    ? window.__REDUX_DEVTOOLS_EXTENSION__()
    : (args) => args
);

// store
const initialState = {
  user: null,
  posts: [],
};

const store = createStore(rootReducer, initialState, enhancer);

// store 꺼낸 후 react와의 연결
export default store;

// dispatch! 사용해보자
// store.dispatch(asyncLogin({ id: 1, name: "darren", admin: true }));
// store.dispatch(addPost({ userId: 1, id: 1, content: "my first post" }));
// store.dispatch(addPost({ userId: 1, id: 2, content: "second post" }));
// store.dispatch(logOut());

 

위 코드를 configureStore를 통해서 간략하게 줄여보겠습니다. 솔직히 configureStore의 인자만 잘 맞춰서 딱딱 넣어주면 됩니다.

import rootReducer from "./_reducers";
const { createStore, applyMiddleware, compose } = require("redux");
const { asyncLogin } = require("./_actions/user");
const { createLogger } = require("redux-logger");

// middleware
const logger = createLogger();

// store
const initialState = {
  user: null,
  posts: [],
};

// enhancers에 대해선, 공식 문서에서 applyMiddleware나 devtools를 넣지 말라고 함. 이미 내장되어 있음.
// preloadedState는 initialState
const store = configureStore({
  reducer: rootReducer,
  middleware: [logger, ...getDefaultMiddleware()],
  devTools: process.env.NODE_ENV !== "production",
  preloadedState: initialState, // 만약 SSR로 넘어온 정보가 있다면 여기에 담아주면 됨.
});

// store 꺼낸 후 react와의 연결
export default store;

 

 

2. action, reducer 대신 slice를 사용하자 => createSlice

 

(1) 개념적인 이해

https://redux-toolkit.js.org/api/createSlice

문서에 따르면, action, reducer각 합친 개념입니다. 지금까지는 별도로 관리하고, 타입도 맞춰줘야 해서 왔다갔다 불편했죠.

그래서 slice라는 개념이 나왔습니다. 내부적으로도 it(slice) uses createAction and createReducer랍니다.

createAction, createReducer를 각각 사용하는 방법도 있긴 한데, 굳이...

 

공식 문서의 Slice 예시를 가져와보았습니다. 가장 기본적인 예시입니다. 

다른 필드들은 직관적으로 이해 되는데, reducers와 extraReducers가 무슨 차이인지가 조금 궁금해집니다.

reducers는 동기적인 반면 extraReducers는 비동기적인 처리를 위해 사용합니다.

It's particularly useful for working with actions produced by createAction and createAsyncThunk.

 

import { createSlice } from '@reduxjs/toolkit'

const initialState = { value: 0 }

const counterSlice = createSlice({
  name: 'counter', // A name, used in action types
  initialState,  // The initial state for the reducer
  reducers: {},
  extraReducers: {}
})

export const { ... reducers 객체의 키값들 } = counterSlice.actions
export default counterSlice.reducer // reducer들

 

그리고 createSlice가 반환하는 값을 분리하여 action도 추출하고, reducer도 추출하는 등의 작업을 볼 수 있는데, 

createSlice의 반환값은 아래와 같습니다.

{
    name : string,
    reducer : ReducerFunction,
    actions : Record<string, ActionCreator>,
    caseReducers: Record<string, CaseReducer>
}

 

 

(2) 실제로 slice를 만들어보자.

아래는 과거 action과 reducer를 그대로 사용한 코드입니다.

// action
const addPost = (data) => {
  return {
    type: ADD_POST,
    data,
  };
};

// reducer
const postReducer = (prevState = initialState, action) => {
  switch (action.type) {
    case ADD_POST:
      return [...prevState, action.data];
    default:
      return prevState;
  }
};

 

slice로 위 코드를 개선했습니다. 아주 클린해졌습니다.

const { createSlice } = require("@reduxjs/toolkit");

const initialState = [];

const postSlice = createSlice({
  name: "post",
  initialState,
  reducers: {
    addPost(state, action) {
      state.push(action.data);
    },
  },
});

 

 

 

(3) slice를 이용하여 action을  dispatch하고, reducer를 작동시키자.

 

이렇게 작성한 slice를 이용하여 action을 dispatch하는 방법도 달라집니다.

크게 달라진 건 아니고 slice에서 빼다 쓰면 됩니다.

// react-redux의 dispatch는 여전히 필요함
const dispatch = useDispatch();

// before. action을 가져와서 사용해야 함
const LogoutClick = useCallback(() => {
    dispatch(logOut());
}, []);

// after. slice에서 action 추출 후 사용
const LogoutClick = useCallback(() => {
    dispatch(userSlice.actions.logOut());
}, []);

 

(4) combineReducers

최종적으로, 이렇게 만들어진 slice에서 reducer만 따로 빼서 rootReducer를 만들어 주는 작업도 아래와 같이 바뀝니다.

// before
const rootReducer = combineReducers({
  user: userReducer,
  posts: postReducer,
});

// after
const rootReducer = combineReducers({
  user: userSlice.reducer,
  posts: postSlice.reducer,
});

module.exports = rootReducer;

 

3. 비동기 액션 처리 => createAsyncThunk

https://redux-toolkit.js.org/api/createAsyncThunk

 

아래는 간단한 비동기를 처리하기 위한 액션과 리듀서입니다. (immer도 곁들이긴 했네요) 여튼 진짜 길어요.

길어진 이유는, 대개 비동기 처리는 네트워크 요청일텐데, REQUEST, SUCCESS, FAILURE 세 경우 모두를 처리할 액션과 리듀서를 만들어둬야하기 때문이었죠.

const asyncLogin = (data) => {
  return (next, getState) => {
    // 요청 보내기
    next(loginRequest(data)); // 패스워드, 인증 등 정보 넘어가겠죠? 여기선 일단 생략

    try {
      // 로그인 과정 2초
      setTimeout(() => {
        next(loginSuccess({ id: 1, name: "darren", admin: true }));
      }, 2000);
    } catch (error) {
      next(loginFailure(error));
    }
  };
};

const loginRequest = (data) => {
  return {
    type: LOG_IN_REQUEST,
    data,
  };
};

const loginSuccess = (data) => {
  return {
    type: LOG_IN_SUCCESS,
    data,
  };
};

const loginFailure = (error) => {
  return {
    type: LOG_IN_FAILURE,
    error,
  };
};

const userReducer = (prevState = initialState, action) => { // 새로운 state 만들어주기
  return produce(prevState, (draft) => {
    switch (action.type) {
      case 'LOG_IN_REQUEST':
        draft.data = null;
        draft.isLoggingIn = true;
        break;
      case 'LOG_IN_SUCCESS':
        draft.data = action.data;
        draft.isLoggingIn = false;
        break;
      case 'LOG_IN_FAILURE':
        draft.data = null;
        draft.isLoggingIn = false;
        break;
      default:
        break;
    }
  });
};

 

다음과 같이 간단하게 비동기 처리 함수를 만든 후, slice에서 후속처리 해주면 됩니다.

주의할 점으로는, createAsyncThunk 내부에서  try/catch를 하지말고, slice에서 에러를 핸들링할 수 있도록 만들어야 한다는 것입니다.

// try/catch 하지 마세요. slice에서 처리하게 두어야 합니다.
const asyncLogin = createAsyncThunk(LOG_IN, async (data, thunkAPI) => {
  console.log("user input", data);
  return await sleep(2000, {
    userId: 1,
    name: "darren",
    admin: true,
  });
});

 

해당 비동기 thunk는 extraReducers에 다음과 같이 처리하면 됩니다.

pending, fulfilled, rejected라는 3가지 상태는 toolkit에 의해 고정된 상태이므로 그대로 따라야 합니다.

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {},
  extraReducers: {
    [asyncLogin.pending](state, action) {
      state.data = null;
      state.isLoggedIn = false;
    },
    [asyncLogin.fulfilled](state, action) {
      // toolkit에 의해 action에 넘겨진 데이터는 전부 payload라는 이름으로 고정됨
      state.data = action.payload;
      state.isLoggedIn = false;
    },
    [asyncLogin.rejected](state, action) {
      state.data = null;
      state.isLoggedIn = false;
    },
  },
});

 

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체