[Redux + TypeScript] Redux-toolkit 사용해보기

김방울·2023년 4월 3일
0

TypeScript

목록 보기
10/12

코딩애플님의 '빠르게 마스터하는 타입스크립트' 강의를 수강한 뒤 정리를 위해 작성한 글입니다.

Redux-tookit은 보다 간단한 문법으로 리덕스를 사용해 줄 수 있게 해 주는 (무려) 공식 라이브러리입니다. 🥳

라이브러리 설치

npm install @reduxjs/toolkit react-redux // or yarn add @reduxjs/toolkit react-redux

store, reducer 정의하기

store 생성

// src/redux/UI/store.ts
import UIStoreType from './type';

const UIStore: UIStoreType = {
  modal: false,
};

export default UIStore;

state의 초기값을 정의해 주는 store.ts 파일을 만들어 줍니다.
간단하게 모달을 열고 닫을 용도로 쓸 거라서 객체 안에 modal이라는 속성을 만들어주었습니다.

// src/redux/UI/type.ts
import { ReactNode } from 'react';

interface UIStoreType {
  modal: boolean | ReactNode;
}

export default UIStoreType;

만들어 준 store 객체의 타입을 정의해주기 위해, 같은 폴더에 type.ts를 만들어줍니다.
상태가false 값이면 모달이 보이지 않고, false 값이 아니면 state로 지정해 준 컨텐츠가 표시되는 모달을 만들기 위해 boolean | ReactNode 로 타입을 지정해주었습니다.

reducer 생성

// src/redux/UI/action.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ReactNode } from 'react';

import UIStore from './store';

const UISlice = createSlice({
  name: 'UI',
  initialState: UIStore,
  reducers: {
    setModalContent(state, action: PayloadAction<ReactNode>) { // 모달 컨텐츠 지정 및 모달 열기
      state.modal = action.payload;
    },
    setModalClose(state) { // 모달 닫기
      state.modal = false;
    },
  },
});

export const { setModalClose, setModalContent } = UISlice.actions;
export default UISlice;

action.ts 파일을 만들고 상태를 조작하는 방법을 정의합니다.
리덕스 툴킷은 createSlice 함수로 슬라이스를 만들어 그 안에서 리듀서를 정의해 줄 수 있는 특징이 있습니다🤔

슬라이스를 생성하려면

  • 슬라이스를 식별하기 위한 문자열 이름 (name 속성)
  • 초기 상태 값 (위에서 만들었던 store.ts 파일!)
  • 상태를 조작할 리듀서! (reducers)
    를 정의해 주면 됩니다.

원래 Redux는 데이터 복사본을 만들고 복사본을 업데이트해서 상태를 불변적으로 업데이트해야만 하는데, redux-toolkit의 createSlice API를 사용하면 Mobx처럼 편하게 상태를 업데이트 할 수 있다고 합니다. 👩‍🚀

reduxer의 첫 파라미터에는 state가, 두 번째 파라미터에는 action이 자동으로 부여됩니다.

reducers: {
    setModalContent(state, action: PayloadAction<ReactNode>) { 
      state.modal = action.payload;
    },
    ...

action에 payload(dispatch 시 같이 보낼 데이터) 가 있으면 자료의 타입을 PayloadAction 의 제네릭에 넣어서 타입을 지정해 주면 된다고 합니다.

configureStore에 등록

// src/redux/index.ts
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import UISlice, { setModalClose, setModalContent } from './UI/action';
import ContentSlice, { setTodayFeel, setTodayFeelDesc, setReset } from './Content/action';

const indexStore = configureStore({
  reducer: {
    ui: UISlice.reducer,
    content: ContentSlice.reducer,
  },
  middleware: getDefaultMiddleware({
    serializableCheck: false,
  }),
});

// 리듀서들을 하나의 객체로 묶어서 내보내기
export const indexAction = { setModalClose, setModalContent, setTodayFeel, setTodayFeelDesc, setReset };

// state 타입을 export
export type RootStateType = ReturnType<typeof indexStore.getState>;
export default indexStore;

만들어 준 리듀서는 configureStore 라는 함수를 import해와서 등록해 줍니다.
저는 상태로 컴포넌트를 넘겨 주었기 때문에 콘솔창에서 직렬화 관련 경고가 계속 발생해서 추가로 serializableCheck: false 설정을 해 주었는데, store의 일관성 유지, 미들웨어 사용시 오류 발생 가능성 높음, 디버깅 오류 등이 발생할 수 있기에 권장되는 설정은 아니라고 합니다 😭 (https://ymc-crow.github.io/basic/redux%EC%99%80-serializable/)

ProviderStore에 등록

//_app.tsx
//...
import ModalView from '@/components/common/modal/ModalView';
import { Provider } from 'react-redux';
import indexStore from '@/redux';
//...

function App({ Component, pageProps }: AppProps) {
  return (
    <>
      //...
      <Provider store={indexStore}>
          <Component {...pageProps} />
          <ModalView />
      </Provider>
    </>
  );
}

export default App;

최상위 컴포넌트에(next.js의 경우 _app.tsx) redux의 Provider 컴포넌트를 불러오고,
configureStore 로 등록해 준 리듀서와 스토어 파일을 가져와서 store 속성으로 설정해줍니다.

리듀서 사용하기

state 꺼내오기

// ModalView.tsx
import { RootStateType } from '@/redux';
import { useSelector, useDispatch } from 'react-redux';
import { indexAction } from '@/redux';
import Modal from './ModalViewStyle';

const ModalView: React.FC = () => {
  const rootState = useSelector((state: RootStateType) => state);
  const dispatch = useDispatch();

  /** FUNCTION 기록하기 버튼 클릭 시 실행 */
  const setModalClose = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (e.target !== e.currentTarget) return;
    dispatch(indexAction.setModalClose());
  };

  return (
    <>
      {rootState.ui.modal !== false && (
        <Modal.Section className='Modal' onClick={setModalClose}>
          <Modal.Container className='Modal__container'>{rootState.ui.modal}</Modal.Container>
        </Modal.Section>
      )}
    </>
  );
};

export default ModalView;
import { RootStateType } from '@/redux';
import { useSelector } from 'react-redux';

리덕스 state를 사용할 곳에 redux에서 제공하는useSelector hook과 configureStore 로 리듀서를 등록하면서 export 해 줬던 state 타입을 불러옵니다.

...
const ModalView: React.FC = () => {
  const rootState = useSelector((state: RootStateType) => state);
...

useSelector 안 콜백함수의 첫 파라미터는 항상 state가 됩니다. state에 타입 지정을 하고 그대로 반환해 주면 configureStore에서 등록해 줬던 store state에 접근할 수 있습니다.

return (
    <>
      {rootState.ui.modal !== false && (
        <Modal.Section className='Modal' onClick={setModalClose}>
          <Modal.Container className='Modal__container'>{rootState.ui.modal}</Modal.Container>
        </Modal.Section>
      )}
    </>
  );

configureStore 에서 정의해 줬던 이름대로 접근이 가능합니다.

상태 조작하기

import { useDispatch } from 'react-redux';
import { indexAction } from '@/redux';

configureStore 정의할 때 묶어서 export해준 action과, redux에서 제공하는 useDispatch 함수를 가져와 줍니다.

const ModalView: React.FC = () => {
  const dispatch = useDispatch();
  ...
  // 모달 닫아 주는 함수
   const setModalClose = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (e.target !== e.currentTarget) return;
    dispatch(indexAction.setModalClose());
  };
 ...
 
 <Modal.Section className='Modal' onClick={setModalClose}>
   ...
dispatch(indexAction.setModalClose());

dispatch 함수 파라미터로 실행하고 싶은 action을 넣어 주면 끝! 👻

확실히 toolkit 라이브러리 없이 redux를 사용하는 것보다 한결 사용하기 쉬워진 느낌이긴 한데, Mobx를 주로 사용하다 보니 이것저것 너무 설정해주어야 할 게 많아서 번잡스럽다는 느낌은 들긴 합니다...🤖 (Mobx 짱!)

참고자료

profile
코딩하는 고양이🐱 / UI Developer, Front-end Developer

0개의 댓글