리액트를 공부하던 중 리덕스를 사용하기 위해 정형적인 프로젝트 속에서 Action
과 State
를 사용하는 법을 정리해 놓으면 좋을 것 같아서 정리하는 글임당.
구체적인 과정이 필요없고 CookBock처럼 사용하려면 핵심 내용만 요약한 글 여기를 참고하면 될 것 같다.
리덕스 사용해 프로젝트를 수정해가는 과정을 차례차례 적을 것인데 따라하면서 연습을 해보고싶다면
$ yarn create react-app react-redux-blog
$ cd react-redux-blog
$ yarn add redux react-redux
를 통해 프로젝트를 생성하자.
그리고 폴더 구조를 아래처럼 만들어 준다.
우선은 전체적인 구조를 알기 위해서 Counter를 먼저 만들어 보자.
components/Counter.js
import React from "react";
const Counter = () => {
return (
<div>
<h1>여기엔 숫자를 담자</h1>
<button>+ 1</button>
<button>- 1</button>
</div>
);
};
export default Counter;
App.js
import React from "react";
import Counter from "./components/Counter";
function App() {
return (
<div>
<Counter />
</div>
);
}
export default App;
그럼 다음과 같은 화면이 보일거다. 이제 우리가 저장하고 싶은 값과 액션을 정의하자!
우리 프로젝트에 리덕스를 사용하기 위한 프로세스는 아래처럼 나눌 수 있다.
1. moduels 폴더에 값, 그리고 액션 정의하기
2. modules/index.js에서 combineReducers
사용해서 rootReducer
만들어주기
3. containers 폴더에서 Container 만들고 connect
함수 사용해서 컴포넌트와 리덕스 연동하기
자 그럼 모듈부터 만들어 보자!
우리가 Module 함수에서 해야하는 일은 크게 아래와 같다.
1. Action Type 정의하기
2. Action Type 반환하는 함수 만들어주기
3. 초기 상태 작성하기
4. Reducer
함수 만들기
우리가 Counter에서 필요한 상태는 카운터의 값인 number
일 것이며 함수는increase
, decrease
일 것이다.
modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
우리가 원하는 함수의 타입 이름을 정의해주면 된다. 액션 타입은 대문자로 작성하고 문자열 내용은 모듈의 이름 / 액션 이름과 같은 형식으로 작성해주면 된다.
이는 나중에 여러 컴포넌트를 다룰 때 타입 이름이 중복되지 않기 위함이다.
modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export
함수를 통해서 해당 타입을 반환하는 함수를 만들어주면 된다.
다만 액션의 종류가 많아진다면 위와 같은 작성이 귀찮아질 수도 있다.
이럴 때는 redux-acitons
라이브러리를 사용하면 된다.
$ yarn add redux-actions
그리고 이를 import
해준다.
import {createAction} from 'redux-actions'
이후 createAction
메소드를 사용해 Action 타입을 반환하는 함수를 만들어준다.
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
훨씬 깔끔하고 간단하게 작성할 수 있다ㅎ
만약 파라미터를 전달해줘야한다고 하면 payload
를 사용하면 된다.
export const changeInput = createAction(CHANGE_INPUT, input => input);
이란 코드를 작성하면
{
type : CHANGE_INPUT,
payload : input
}
이런 식으로 payload
에 값이 들어가게 된다.
이를 사용하는 방법은 아래에서 새로운 기능을 추가할 때를 참고하자
useState
에서 초기값을 지정해주던 것처럼 초기값을 정해주면 된다!
modules/counter.js
(...)
const initialState = {
number : 0
}
이제 우리의 State
와 우리가 정의한 Action Type
을 가지고 Reducer
함수를 만들어주면 된다.
Reducer
함수는 꼭 순수 함수로 만들어야 한다. 이는 아래 4가지 조건을 만족하면 된다.
자 위 내용을 주의하면서 우리의 Reducer
함수를 만들어 보자!
우리는 State
에 number
라는 값을 가지고 있으며 INCREASE
와 DECREASE
를 받는다!
modules/counter.js
(...)
function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return {
number: state.number + 1
};
case DECREASE:
return {
number: state.number - 1
};
default:
return state;
}
}
export default counter;
다음처럼 Action Type
에 따라서 State
를 변형한 새로운 State를 반환해주면 된다.
이 때 절대로 기존 State
를 수정해서는 안된다.
2가지 Action만 해도 위처럼 코드가 매우 길어지는 것을 볼 수 있다.
마찬가지로 redux-actions
라이브러리를 사용하면 switch-case
문으로부터 벗어날 수 있다.
import { createAction, handleActions } from "redux-actions";
handleActions
를 불러온 후에 첫 번째 인자로는 각 액션 타입에 대한 함수가 담긴 객체를, 두 번째 인자로는 초기값을 전해주면 된다.
const counter = handleActions(
{
[INCREASE]: (state, action) => ({
number: state.number + 1
}),
[DECREASE]: (state, action) => ({
number: state.number - 1
})
},
initialState
);
createAction
에서 payload
를 넣어줬다면 아래와 같이 사용하면 된다.
handleActions(
{
[CHANGE_INPUT] : (state, action) => ({
...state,
input : action.payload
})
},
initialState
);
다만 이렇게 사용하면 payload
에 어떤 값을 넣어줬는지 헷갈리므로
handleActions(
{
[CHANGE_INPUT] : (state, { payload : input }) => ({
...state,
input : input
})
},
initialState
);
이런 식으로 작성하면 가독성이 좋아진다.
rootReducer
에 연결하기현재 우리는 counter
라는 Reducer
하나만 가지고 있지만 실제로 modules
폴더에는 수많은 Reducer
가 존재할 것이다. 하지만 리덕스 스토어에는 하나의 Reducer
만 연결되어야 하므로 이를 통합해야 한다.
이를 위해 우리는 redux
의 combineReducers
라는 함수를 사용할 것이다.
modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({
counter
});
export default rootReducer;
사용법은 간단하다. combineReducers
메소드 안에 객체 형태로 우리가 만든 Reducer
를 넣어주면 된다.
예를 들면 아래처럼
const rootReducer = combineReducers({
counter1,
counter2,
counter3
});
Container
를 만들기 전에 만약 우리의 React 프로젝트에 Redux를 적용시키지 않았다면 아래 과정부터 해주자!
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux"; // 추가
import { Provider } from "react-redux"; // 추가
import rootReducer from "./modules"; // 추가
import App from "./App";
const store = createStore(rootReducer); // 추가
ReactDOM.render(
<Provider store={store}> // Provier로 감싸준다
<App />
</Provider>,
document.getElementById("root")
);
만약에 Redux DevTools
와 연동시키고 싶다면
$ yarn add redux-devtools-extension
을 설치한 후에
(...)
import { composeWithDevTools } from "redux-devtools-extension";
(...)
const store = createStore(rootReducer, composeWithDevTools());
로 사용하면 된다.
containers/CounterContainer.js
import React from "react";
import Counter from "../components/Counter";
const CounterContainer = () => {
return <Counter />;
};
export default CounterContainer;
우선 위 코드처럼 CounterContainer
를 만들어 주자.
이 컴포넌트를 Redux
와 연동하기 위해서는 connect
함수를 사용해야 한다.
connect(mapStateToProps, mapDispatchToProps)(연동하고싶은 컴포넌트)
mapStateToProps
는 Store에 담긴 값을 받아서 Props
로 변환시켜주는 역할을, mapDispatchToProps
는 Action을 Props
에 담는 역할을 한다.
우리는 Store
에 담긴 number
를 받아서 Counter
에 counterNum
이라는 props
로 전달해준다 해보자.
const mapStateToProps = state => ({
counterNum: state.counter.number
});
다음과 같은 형태로 state
에서 원하는 값을 가져와 counterNum
이라는 Props
에 넣어줬다.
그럼 이 Props
을 CounterContainer
가 받도록 해보자.
const CounterContainer = ({counterNum}) => {
return <Counter number={counterNum}/>
};
이제 액션을 props
로 전해주는 함수를 만들어 보자.
import { increase, decrease } from "../modules/counter";
우선 우리가 만든 Action을 modules
로부터 불러온 후에
const mapDispatchToProps = dispatch => ({
increase: () => dispatch(increase()),
decrease: () => dispatch(decrease())
});
dispatch
안에 넣어주면 된다
이제 이 두 함수를 connect
함수 안에서 연결해주면 된다.
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
매번 dispatch
안에 넣어주는 것이 귀찮다면 bindActionCreators
를 이용하면 된다.
(...)
import {bindActionCreators} from 'redux'
(...)
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
increase,
decrease
},
dispatch
);
bindActionCreators
안에 객체 형태로 전달해주면 된다.
이제 props
안에 액션을 넣어주자
const CounterContainer = ({ counterNum, increase, decrease }) => {
return (
<Counter number={counterNum} onIncrease={increase} onDecrease={decrease} />
);
};
이제 실제 컴포넌트에서 해당 props
를 받도록 해보자
components/Counter.js
const Counter = ({ number, onIncrease, onDecrease }) => {
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+ 1</button>
<button onClick={onDecrease}>- 1</button>
</div>
);
};
이제 App.js
에서 Container
를 렌더링 해주자
App.js
import CounterContainer from "./containers/CounterContainer";
function App() {
return (
<div>
<CounterContainer />
</div>
);
}
다음처럼 잘 작동하는 것을 확인할 수 있다!
useSelector
를 이용하면 connect
함수 없이 Redux를 조회할 수 있다.
const result = useSelector(상태 선택 함수);
따라서 number
를 아래 코드처럼 간단하게 사용할 수 있다.
import {useSelector} from 'react-redux';
(...)
const CounterContainer = () => {
const counterNum = useSelector(state => state.counter.number)
return (
<Counter number={counterNum}/>
);
};
mapStateToProps
함수를 작성할 필요가 없어진다
const dispatch = useDispatch();
를 이용해서 dispatch
를 얻어준 후에 우리가 modules
에서 얻은 함수를 넣어주면 된다.
import {useSelector, useDispatch} from 'react-redux';
(...)
const CounterContainer = () => {
const counterNum = useSelector(state => state.counter.number)
const dispatch = useDispatch();
return (
<Counter number={counterNum} onIncrease={() => dispatch(increase())} onDecrease={() => dispatch(decrease())} />
);
};
하지만 위처럼 코드를 작성하면 숫자가 바뀔 때마다 컴포넌트가 리렌더링 되어서 매번 함수를 새로 만드므로 useCallback
으로 감싸주는 것이 좋다.
import React, {useCallback} from "react";
(...)
const CounterContainer = () => {
const counterNum = useSelector(state => state.counter.number);
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
return (
<Counter
number={counterNum}
onIncrease={onIncrease}
onDecrease={onDecrease}
/>
);
};
Hooks
를 사용하든 connect
함수를 사용하든 개인의 자유지만 useSelector
를 사용하여 Redux
의 상태를 조회하는 경우 최적화 작업이 자동으로 이루어지지 않으므로 React.memo
를 이용하는 것이 좋다.
export default React.memo(CounterContainer);
다만 React.memo
를 항상 사용하는 것이 능사는 아니다. React.memo
를 사용해야 하는 경우는 다음과 같은 경우다.
1. 순수 함수 컴포넌트
2. 자주 렌더링을 해야하는 함수
3. 같은 props로 자주 리 렌더링 되는 함수
4. 컴포넌트가 props 비교를 필요로 하는 많은 UI 요소를 가지고 있는 경우
만약 props가 자주 변동되는 환경이라면 React.memo
를 사용한다고 해도 컴포넌트를 다시 렌더링 해야하므로 불필요한 비교 과정만 추가하는 꼴이다! 현명하게 사용하자.
자 그럼 우리가 이미 만든 컴포넌트에 새로운 기능을 추가한다고 가정하고 작업해보자.
그냥 연습을 위해 Input
을 하나 만들고 버튼을 누르면 Counter
의 이름을 바꾼다고 생각해보자.
components/Counter.js
import React from "react";
const Counter = ({
name,
number,
inputName,
onIncrease,
onDecrease,
onChange,
onChangeName
}) => {
const onClick = () => {
console.log("Clicked!");
};
const onInputChange = e => onChange(e.target.value);
return (
<div>
<h1>{name}</h1>
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+ 1</button>
<button onClick={onDecrease}>- 1</button>
</div>
<input
onChange={onInputChange}
value={inputName}
placeholder="Counter 이름을 입력하세요"
></input>
<button onClick={onClick}>등록</button>
</div>
);
};
export default Counter;
아래 처럼 간단하게 컴포넌트를 수정해주고 미리 props
를 할당해 주었다.
그럼 아래와 같은 화면이 나올 것이다.
자 우리가 리덕스를 사용하는 프로세스는 위에서 말한 것처럼 아래와 같다.
1. moduels 폴더에 값, 그리고 액션 정의하기
2. modules/index.js에서 combineReducers
사용해서 rootReducer
만들어주기
3. containers 폴더에서 Container 만들고 connect
함수 사용해서 컴포넌트와 리덕스 연동하기
우선 modules
부터 수정해야 한다.
modules
를 수정하는 프로세스는 다음과 같다.
1. Action Type 정의하기
2. Action Type 반환하는 함수 만들어주기
3. 초기 상태 작성하기
4. Reducer
함수 만들기
modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
const INPUT_CHANGE = "counter/INPUT_CHANGE";
const CHANGE_NAME = "counter/CHANGE_NAME";
다음과 같이 액션을 추가해주자!
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
export const inputChange = createAction(INPUT_CHANGE, input => input);
export const changeName = createAction(CHANGE_NAME, name => name);
간단하게 하기 위해서 createAction
메소드를 사용했다.
const initialState = {
number: 0,
name: "평범한 카운터",
inputName: ""
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({
...state,
number: state.number + 1
}),
[DECREASE]: (state, action) => ({
...state,
number: state.number - 1
}),
[INPUT_CHANGE]: (state, { payload: input }) => ({
...state,
inputName: input
}),
[CHANGE_NAME]: (state, { payload: name }) => ({
...state,
name: name
})
},
initialState
);
우리가 새로운 모듈을 추가한 것이 아니므로 수정하지 않아도 된다.
import {
increase,
decrease,
inputChange,
changeName
} from "../modules/counter";
(...)
const { counterNum, name, inputName } = useSelector(({ counter }) => ({
counterNum: counter.number,
name: counter.name,
inputName: counter.inputName
}));
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
const onInputChange = useCallback(input => dispatch(inputChange(input)), [
dispatch
]);
const onChangeName = useCallback(name => dispatch(changeName(name)), [
dispatch
]);
return (
<Counter
name={name}
inputName={inputName}
number={counterNum}
onIncrease={onIncrease}
onDecrease={onDecrease}
onChange={onInputChange}
onChangeName={onChangeName}
/>
);
};
export default React.memo(CounterContainer);
잘 나오는 것을 확인 할 수 있다.
이제 버튼을 누르면 이름이 바뀌도록만 해보자.
components/Counter.js
(...)
const onClick = () => {
onChangeName(inputName);
onChange("");
};
onChangeName
메소드를 통해 Redux
에 담긴 이름을 수정해주고 input
을 비우기 위해서 onChange
를 호출해준다.
생각보다 리덕스를 사용하는 과정이 복잡하고 아직은 낯설다. 핵심 내용만 요약한 글이 여기에 있으니 CookBock처럼 사용하고 싶은 사람은 링크를 참고해보자.
정리 진짜 잘해놓으셨네요ㅠㅠ 잘 읽구 갑니다!!