이번 글에서는 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