• 리액트 리덕스 툴킷으로 상태관리하기 (React + Redux Toolkit + TypeScript) - with Next.js

    2021. 3. 4.

    by. kimyang-Sun

    리덕스 (Redux)

     

    리액트 프로젝트를 진행하며 상태관리시에 프로젝트 규모가 작으면 Context API를 이용하고

    그 외에 경우에는 Redux 또는 Mobx를 많이 사용하는것 같습니다.

    리덕스는 액션 타입, 액션 실행함수, 리듀서 이렇게 만들어주고 관리하는 특성이 있어서 코드량이 많아진다는 단점이 있습니다.

    그렇지만 Redux DevTools 등을 이용하여 상태변화를 하나하나 다 체크하고 확인할 수 있기 때문에 안정성은 높다는 평가가 많아요.

    저는 리덕스의 Redux Toolkit이 리덕스의 코드를 확 줄여줘서 정말 편하다고 하길래 찾아봤습니다.

    직접 테스트를 조금 해보니까 좀 많이 편한거 같아서 앞으로 자주 이용할 생각입니다.

     

    리덕스 등 다른 필수 패키지가 다 설치되어있다는 가정하에 리덕스 툴킷을 설치해줍니다.

     

    yarn add @reduxjs/toolkit // Redux Toolkit 설치

     

    저는 리덕스 관련 파일들을 store 디렉토리를 만들어 관리합니다.

     

     

    store 디렉토리입니다. (해당 프로젝트는 next.js 와 타입스크립트를 사용하며 진행하고 있습니다.)

    store 디렉토리와 화면을 그려주는 components 디렉토리.

    그 컴포넌트들을 리덕스와 연결하며 감싸주는 컴포넌트들을 보관하는 container 디렉토리.

    이런식으로 관리하는 방식에서 redux-toolkit을 사용하면서 감싸주는 컴포넌트들의 디렉토리인 container를 아예 없애버렸습니다.

    redux-toolkit에서는 reducer들마다 컴포넌트에 사용될 커스텀 훅(Custom Hook)들을 따로 만들어서 container 디렉토리 없이 사용했습니다.

    테스트 코드는 간단하게 로그인과 로그아웃만 isLoggedIn과 userData로 관리해 주었습니다.

     

    // store/modules/user.ts
    
    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    
    // 초기 상태 타입
    export type UserState = {
      isLoggedIn: boolean;
      userData: any;
    };
    
    // 액션 Payload 타입
    export type LoginPayload = {
      userId: string;
      password: string;
    };
    
    // 초기 상태
    const initialState: UserState = {
      isLoggedIn: false,
      userData: null,
    };
    
    // 리듀서 슬라이스
    const userSlice = createSlice({
      name: 'user',
      initialState,
      reducers: {
        loginAction(state: UserState, action: PayloadAction<LoginPayload>) {
          state.isLoggedIn = true;
          state.userData = action.payload;
        },
        logoutAction(state: UserState) {
          state.isLoggedIn = false;
          state.userData = null;
        },
      },
    });
    
    // 리듀서 & 액션 리턴
    const { reducer, actions } = userSlice;
    export const { loginAction, logoutAction } = actions;
    export default reducer;

     

    createSlice로 기존의 리덕스에서 액션 타입, 액션 생성함수, 리듀서를 다 따로 만들어주던걸 한 번에 만들어줄 수 있습니다.

    userSlice를 저런식으로 만들어주면 userSlice.reducer, userSlice.actions 이런식으로 리듀서와 액션타입을 사용할 수 있습니다.

    userData의 타입은 테스트 코드에서는 아직 정해진게 없기 때문에 any로 해주었습니다.

    이제 내보내준 리듀서와 액션타입들을 container가 따로 없으니 userHooks라는 Custom Hook을 만들어줍니다.

     

    // store/modules/userHook.ts
    
    import { useCallback } from 'react';
    import { useDispatch, useSelector } from 'react-redux';
    import { RootState } from '.';
    import { loginAction, LoginPayload, logoutAction } from './user';
    
    // 커스텀 훅
    export default function useUser() {
      const { isLoggedIn } = useSelector((state: RootState) => state.user);
      const dispatch = useDispatch();
      
      const login = useCallback((data: any) => {
        dispatch(loginAction(data));
      }, []);
      
      const logout = useCallback(() => {
        dispatch(logoutAction());
      }, []);
    
      return { isLoggedIn, login, logout };
    }
    

     

    커스텀 훅을 만들어줬습니다.

    useSelector, useDispatch를 따로 userHook 파일 안에 한꺼번에 만들어주고 login, logout 함수도 만들어줬습니다.

    이제 내보내서 적용시키는 단계입니다.

     

    // store/modules/index.ts
    
    import { combineReducers } from 'redux';
    import user from './user';
    
    // 루트 리듀서
    const rootReducer = combineReducers({ user });
    
    export default rootReducer;
    
    // 루트 리듀서의 반환값를 유추해줍니다
    // 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줍니다.
    export type RootState = ReturnType<typeof rootReducer>;

     

    index.ts 입니다. 이곳에서 combineReducers 안에 만들어준 user를 넣어줍니다.

    rootReducer를 내보내주고 RootState 타입은 ReturnType을 이용하여 rootReducer의 state타입을 넣어줍니다.

    이렇게 만들어주면 추후에 combineReducers에 다른 리듀서를 추가만 해주시면 알아서 RootState의 타입에도 추가됩니다.

     

    // store/configureStore.ts
    
    import { configureStore } from '@reduxjs/toolkit';
    import { createWrapper } from 'next-redux-wrapper';
    import rootReducer from 'store/modules';
    
    const store = () => {
      const store = configureStore({ reducer: rootReducer });
      return store;
    };
    
    const wrapper = createWrapper(store, {
      // 이 부분이 true면 디버그때 자세한 설명이 나옵니다. (개발할때는 true로)
      debug: process.env.NODE_ENV === 'development',
    });
    
    export default wrapper;

    configureStore 파일에서는 store를 cofigureStore()를 이용하여 reducer를 불러와줬는데

    "기존의 createStore랑 뭐가다르냐?" 하실 수 있습니다.

    configureStore는 toolkit에서 리덕스 createStore를 베이스로하여 새롭게 만들어준 기능입니다.

    createStore를 사용하면 devTools를 이용할때는 composeWithDevTools를 넣어줘야하고,

    미들웨어를 사용할때도 applyMiddleware도 넣어줘야 합니다.

     

    configureStore를 이용한다면

     

    const store = () => {
      const store = configureStore({ reducer: rootReducer, middleware: [...getDefaultMiddleware()] });
      return store;
    };

     

    이렇게 간단하게 리듀서와 미들웨어를 쉽게 넣어줄 수 있습니다.

    cofigureStore는 Redux DevTools와 리덕스 미들웨어를 포함하고 있어서 devtools는 따로 넣어주지 않아도 알아서 들어가고

    미들웨어는 따로 무언가를 불러올 필요없이 저렇게 뒤에 추가만 해주시면 됩니다.

     

    export default wrapper.withRedux(App);

     

    _app.tsx입니다. 저는 next.js를 사용하고 있어서 이렇게 적용시켜 줬습니다.

    (일반 리액트 프로젝트라면 index.tsx 파일에서 store를 불러와서 Provider에 App을 감싸고 store 프롭스에 store를 넣어주었을 것입니다.)

     

    const { isLoggedIn, login, logout } = useUser();

     

    이제 사용할 컴포넌트에서 이런식으로 내보내준 isLoggedIn과 login 함수를 가져와서 사용하면 됩니다.

     

    댓글