[React] Redux Toolkit + TypeScript 사용하기

@eunjios·2023년 10월 19일
5

Redux

목록 보기
3/3
post-thumbnail

Redux Toolkit

React에서 TypeScript 와 Redux Toolkit 함께 사용하기

Setting

CRA

// npx
npx create-react-app . --template typescript

// yarn
yarn create react-app . --template typescript

Redux Toolkit 및 react-redux 설치

// npm
npm install @reduxjs/toolkit react-redux

// yarn
yarn add @reduxjs/toolkit react-redux

🤔 CRA를 이미 JavaScript 로 셋팅한 경우

  1. TypeScript 설치
    npm install typescript
    yarn add typescript
  2. TypeScript 관련 모듈 (@types/node, @types/react, @types/react-dom, @types/jest) 설치
    npm install --save @types/node @types/react @types/react-dom @types/jest
    yarn add @types/node @types/react @types/react-dom @types/jest
  3. tsconfig.json 파일 작성
    {
      "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "allowImportingTsExtensions": true,
        "jsx": "react"
      },
      "include": ["src"]
    }

카운터 예제

0. Counter 컴포넌트 만들기

카운터 값과 더하기 빼기 버튼이 있는 컴포넌트를 만들자. 당연히 지금은 아무 동작도 하지 않는다.

components/Counter.tsx

import React from 'react';

const Counter: React.FC = () => {
  const counter = 0;

  return (
    <div>
      <h2>{counter}</h2>
      <div>
        <button>더하기</button>
        <button>빼기</button>
      </div>
    </div>
  );
};

export default Counter;

1. store 만들기

const store = configureStore({
  reducer: {리듀서 map},
});

타입스크립트에서도 store 는 위와 같이 configureStore 를 사용해서 만들 수 있다. 단, 외부 컴포넌트에서 store의 state와 disptach 를 사용하기 위해서는 다음과 같이 타입을 지정해줘야 한다.

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
  • RootState : Redux 스토어의 state를 나타내는 타입
  • AppDispath : Redux 액션을 dispatch하는 함수의 타입

store/index.ts

import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

여기서 reducer는 별도의 슬라이스를 만들어서 구현할 수 있다. 일단은 빈 객체로 두자.


2. 슬라이스 만들기

Redux Toolkit를 사용해서 슬라이스를 만드는 방법은 다음과 같았다.

const slice = createSlice({
  name: slice이름,
  initialState: state초기값,
  reducers: {
    // 리듀서구현
  },
});

위 코드에서 우리는 state와 action의 타입을 지정해야 한다. 우선 타입을 정의하고 리듀서에 타입을 지정해주자.

(1) state, action 의 타입 정의하기

interface StateType {
  // state 타입 정의
}
interface ActionType {
  // action 타입 정의
}

(2) 슬라이스에 정의한 타입 지정해주기

initialStateaction 에 타입을 지정해준다. 단, action 의 타입은 PayloadAcition<액션타입> 으로 지정해야 한다.

const initialState: StateType = 초기값
// 리듀서 함수 내부
someReducer: (state, action: PayloadAction<ActionType>) => {
  // code...
 },

카운터 예제의 코드는 다음과 같이 작성할 수 있다.

store/counter.ts

import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  counter: number;
}

const initialState: CounterState = {
  counter: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    add: (state, action: PayloadAction<number>) => {
      state.counter += action.payload;
    },
    sub: (state, action: PayloadAction<number>) => {
      state.counter -= action.payload;
    },
  },
});

export const counterActions = counterSlice.actions;
export const default counterSlice.reducer;
  • PayloadAction<number> 는 dispatch 해오는 함수의 파라미터 타입을 number로 지정한다.

아까 비워두었던 storereducer 에 방금 counterSlice 에서 정의한 리듀서 함수를 등록하자.

store/index.ts

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counter';

const store = configureStore({
  reducer: {
    counter: counterReducer, // 추가됨
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;
  • store/counter.ts 에서 counterSlice.reducer 를 default로 export 했기 때문에 여기서 counterReducer 는 store/counter.ts 의 counterSlice.reducer 가 된다.

3. 컴포넌트에서 store의 state와 actoin 접근하기

(1) 커스텀 훅 정의하기

JavaScript 를 사용한 Redux의 경우 useDispatchuseSelector 를 사용해서 컴포넌트에서 스토어에 접근했다. 그러나 TypeScript의 경우 기존 훅에 우리가 새롭게 정의한 타입을 적용해야 한다.

useDispatch 는 함수 형태이고, 리턴 타입은 리덕스 스토어의 dispatch 이다. 이는 이미 store/index.ts 에서 AppDispatch 타입으로 정의한 타입이기 때문에 AppDispatch 를 import 해서 사용하면 된다.

export const useAppDispatch: () => AppDispatch = useDispatch;

useSelectoruseSelectorHook 타입이지만, 우리가 정의한 RootState 로 제네릭 타입을 지정해야 한다.

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

(2) slectSlice 정의하기

만약 애플리케이션이 커지면서 슬라이스가 많아진다면 각 슬라이스 파일에서 해당 슬라이스를 선택하는 콜백함수를 export 해주는 것이 유지보수에 더 좋을 것이다. 이는 다음과 같이 구현할 수 있다.

// store/foo.ts

// ...
export selectFoo = state: RootState => state.foo.bar;
// store/SomeComponent.ts

// ...
const bar = useSelector(selectFoo);

위 두 단계를 적용한 카운터 예제 코드는 다음과 같다.

hooks/index.ts

import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from '../store';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

store/counter.ts

import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  counter: number;
}

const initialState: CounterState = {
  counter: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    add: (state, action: PayloadAction<number>) => {
      state.counter += action.payload;
    },
    sub: (state, action: PayloadAction<number>) => {
      state.counter -= action.payload;
    },
  },
});

export const counterActions = counterSlice.actions;
export const selectCount = (state: RootState) => state.counter.counter; // 추가
export const default counterSlice.reducer;
  • 기존 코드와 동일
  • selectCount 만 추가하였다.

components/Counter.tsx

import React from 'react';
import { useAppSelector, useAppDispatch } from '../hooks';
import { selectCount, counterActions } from '../store/counter';

const Counter = () => {
  const counter = useAppSelector(selectCount);
  const dispatch = useAppDispatch();
  
  const addHandler = () => {
    dispatch(counterActions.add(10));
  };
  
  const subHandler = () => {
    dispatch(counterActions.sub(10));
  };

  return (
    <div>
      <h2>{counter}</h2>
      <div>
        <button onClick={addHandler}>더하기</button>
        <button onClick={subHandler}>빼기</button>
      </div>
    </div>
  );
};

export default Counter;
  • useSelector useDispatch 가 아닌, 타입을 지정한 커스텀 훅 useAppSelector useAppDispatch 를 사용한다.
  • store/counter.ts 파일에서 counterActions를 비구조화 할당해서 export 할 수도 있다.
    // store/counter.ts
    export cosnt { add, sub } = counterSlice.actions;
    // components/Counter.tsx
    import { add, sub } from '../store/counter';

4. store 제공하기

<Provider store={store}>
  <App />
</Provider>

타입스크립트를 사용한 Redux Toolkit도 동일하게 최상위 컴포넌트를 Provider 로 감싸서 store를 제공해야 한다. 이렇게 감싸면 내부 컴포넌트는 Redux의 스토어를 전역적으로 사용할 수 있다.


index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

import { Provider } from 'react-redux';
import store from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

References

profile
growth

0개의 댓글