이번 글에서는 react에서 redux를 사용하는 방법에 대해서 알아보려고 한다.
먼저 CRA를 통해 redux와 react-redux 패키지가 포함된 채로 프로젝트를 시작할 수도 있고, 이미 진행 중인 프로젝트에 redux를 추가할 수도 있다.
npx create-react-app client --template redux-typescript
npm i redux react-redux
# 타입스크립트라면 아래의 패키지를 추가로 설치
npm i -D @tpyes/react-redux
그리고 리덕스를 디버깅하기 쉽게 해주는 크롬 확장프로그램이 있는데 확장프로그램과 연동하기 위해 패키지를 하나 추가로 설치해줘야 한다.
npm i -D redux-devtools
설치가 모두 완료됐으면 이제 redux를 사용할 준비가 되었다!
Redux를 사용할 때 강제되는 폴더구조는 없지만 권장하는 폴더 구조는 존재한다. 우리는 아래의 그림과 같은 폴더 구조를 사용할 것이다.

그럼 Redux를 사용하여 간단한 Counter를 만들어 보겠다.
src/components/Counter.tsx
import React, { FC } from "react";
const Counter: FC = () => {
return (
<>
<div>
<button>-</button>
<span>0</span>
<button>+</button>
</div>
</>
);
};
export default Counter;
src/App.tsx
import React from "react";
import Counter from "./components/Counter";
function App() {
return <Counter />;
}
export default App;
간단하게 Counter 컴포넌트를 만들었다.
여기서 redux를 사용하기 위한 절차를 살펴보자.
src/modules/counter.ts
const INCREMENT_COUNTER: string = "INCREMENT_COUNTER";
const DECREMENT_COUNTER: string = "DECREMENT_COUNTER";
src/modules/counter.ts
interface IncrementAction {
type: typeof INCREMENT_COUNTER;
}
interface DecrementAction {
type: typeof DECREMENT_COUNTER;
}
const INCREMENT_COUNTER: string = "INCREMENT_COUNTER";
const DECREMENT_COUNTER: string = "DECREMENT_COUNTER";
export const increase = (): IncrementAction => ({ type: INCREMENT_COUNTER });
export const decrease = (): DecrementAction => ({ type: DECREMENT_COUNTER });
src/modules/counter.ts
interface InitialState {
number: number;
}
const initialState = {
number : 0
}
Reducer를 만들기 전에 주의해야할 것이 하나 있다. 바로 Reducer는 순수함수여야 한다 이다. 그 이유는 이전 포스팅인 Redux란?을 읽어보길 바란다.
src/modules/counter.ts
type Action = IncrementAction | DecrementAction;
export const counter = (prevState = initialState, action: Action) => {
switch (action.type) {
case INCREMENT_COUNTER:
return { number: prevState.number + 1 };
case DECREMENT_COUNTER:
return { number: prevState.number - 1 };
default:
return prevState;
}
};
현재는 reducer가 counter에 하나밖에 없지만 프로젝트가 커질수록 reducer의 개수는 계속 늘어날 것이다. 하지만 redux store에는 하나의 reducer만 연결되야 하므로 이를 통합 해야한다.
우리는 이를 위해 redux의 combineReducers를 이용할 것이다.
사용법은 간단하다. combineReducers 메소드 안에 객체 형태로 우리가 만든 Reducer를 넣어주면 된다.
src/modules/index.ts
import { combineReducers } from "redux";
import { counter } from "./counter";
const rootReducer = combineReducers({
counter,
});
export default rootReducer;
store도 만들었으니 React 프로젝트에 Redux를 적용시켜보자.
src/App.tsx
import React from "react";
import { createStore } from "redux"; // 추가
import { Provider } from "react-redux"; // 추가
import rootReducer from "./modules/index"; // 추가
import Counter from "./components/Counter";
const store = createStore(rootReducer); // 추가
function App() {
return (
<Provider store={store}> // 추가
<Counter />
</Provider> // 추가
);
}
export default App;
Hooks를 사용해서 간단하게 Redux를 이용할 것이다.
useSelector를 이용하여 상태를 조회하고, useDispatch를 이용하여 Action를 디스패치할 수 있다.
src/components/Counter.tsx
import React, { FC, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { increase, decrease } from "../modules/counter";
const Counter: FC = () => {
const {
counter: { number },
} = useSelector((state) => state as any);
const dispatch = useDispatch();
const onClickMinus = useCallback(() => dispatch(decrease()), [dispatch]);
const onClickPlus = useCallback(() => dispatch(increase()), [dispatch]);
return (
<>
<div>
<button onClick={onClickMinus}>-</button>
<span>{number}</span>
<button onClick={onClickPlus}>+</button>
</div>
</>
);
};
export default Counter;
Hooks를 사용하든 connect 함수를 사용하든 개인의 자유지만 useSelector를 사용하여 Redux의 상태를 조회하는 경우 최적화 작업이 자동으로 이루어지지 않는다.
해결방법
독립 선언
// 최적화 전
// number, diff 중 하나라도 변경되면 리렌더링
const { number, diff } = useSelector(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
// 최적화 후
const number = useSelector(state => state.counter.number);
const diff = useSelector(state => state.counter.diff);
equalityFn
equalityFn?: (prev: any, next: any) => boolean
const { count, prevCount } = useSelector((state: RootState) => ({
count : state.countReducer.count,
prevCount: state.countReducer.prevCount,
}),(prev, next) => {
return prev.count === next.count && prev.prevCount === next.prevCount;
});
shallowEqual