React에서 TypeScript 와 Redux Toolkit 함께 사용하기
// npx
npx create-react-app . --template typescript
// yarn
yarn create react-app . --template typescript
// npm
npm install @reduxjs/toolkit react-redux
// yarn
yarn add @reduxjs/toolkit react-redux
npm install typescript
yarn add typescript
npm install --save @types/node @types/react @types/react-dom @types/jest
yarn add @types/node @types/react @types/react-dom @types/jest
{
"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"]
}
카운터 값과 더하기 빼기 버튼이 있는 컴포넌트를 만들자. 당연히 지금은 아무 동작도 하지 않는다.
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;
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는 별도의 슬라이스를 만들어서 구현할 수 있다. 일단은 빈 객체로 두자.
Redux Toolkit를 사용해서 슬라이스를 만드는 방법은 다음과 같았다.
const slice = createSlice({
name: slice이름,
initialState: state초기값,
reducers: {
// 리듀서구현
},
});
위 코드에서 우리는 state와 action의 타입을 지정해야 한다. 우선 타입을 정의하고 리듀서에 타입을 지정해주자.
(1) state, action 의 타입 정의하기
interface StateType {
// state 타입 정의
}
interface ActionType {
// action 타입 정의
}
(2) 슬라이스에 정의한 타입 지정해주기
initialState
와 action
에 타입을 지정해준다. 단, 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로 지정한다.아까 비워두었던 store
의 reducer
에 방금 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;
counterSlice.reducer
를 default로 export 했기 때문에 여기서 counterReducer
는 store/counter.ts 의 counterSlice.reducer
가 된다.JavaScript 를 사용한 Redux의 경우 useDispatch
와 useSelector
를 사용해서 컴포넌트에서 스토어에 접근했다. 그러나 TypeScript의 경우 기존 훅에 우리가 새롭게 정의한 타입을 적용해야 한다.
useDispatch
는 함수 형태이고, 리턴 타입은 리덕스 스토어의 dispatch 이다. 이는 이미 store/index.ts 에서 AppDispatch
타입으로 정의한 타입이기 때문에 AppDispatch
를 import 해서 사용하면 된다.
export const useAppDispatch: () => AppDispatch = useDispatch;
useSelector
는 useSelectorHook 타입이지만, 우리가 정의한 RootState 로 제네릭 타입을 지정해야 한다.
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
만약 애플리케이션이 커지면서 슬라이스가 많아진다면 각 슬라이스 파일에서 해당 슬라이스를 선택하는 콜백함수를 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
export cosnt { add, sub } = counterSlice.actions;
// components/Counter.tsx
import { add, sub } from '../store/counter';
<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>
);