
단순히 전역 상태 관리만 한다면 Context API 를 사용하는 것만으로도 충분합니다.
하지만 리덕스를 사용하면 상태를 더욱 체계젹으로 관리할 수 있기 대문에 프로젝트의 규모가 클 경우에는
리덕스를 사용하는 편이 좋습니다.
리덕스를 사용하면서 접하게 될 키워드의 개념을 간략하게 알아보겠습니다.
액션 객체는 type 필드를 반드시 가지고 있어야 합니다. 이 값을 액션의 이름이라고 생각하면 됩니다.
액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아옵니다.
그리고 두 값을 참고하여 새로운 상태를 만들어 반환해 줍니다.
스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 중요한 내장 함수를 지닙니다.
한 개의 프로젝트는 단 하나의 스토어만 가질 수 있습니다.
스토어의 내장 함수 중 하나로써, 이 함수가 호출되면 스토어는 리듀서 함수를 실행시켜서 새로운 상태를 만들어 줍니다.
// 액션 타입
const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
//액션 생성 함수
const toggle = () => ({type: TOGGLE_SWITCH});
const increase = (difference) => ({type: INCREASE, difference});
const decrease = () => ({type: DECREASE});
//초기값
const initialState = {
toggle: false,
counter: 0
}
//리듀서 함수
function reducer(state = initialState, action) {
switch(action.type) {
case TOGGLE_SWITCH:
return {
...state,
toggle: !state.toggle
};
case INCREASE:
return {
...state,
counter: state.counter + action.difference
};
case DECREASE:
return {
...state,
counter: state.counter - 1
}
default:
return state;
}
}
//스토어
import { createStore } from 'redux';
const store = createStore(reducer);
//구독하기
const listener = () => {
console.log('state update!!');
};
store.subscribe(listener);
//액션 발생시키기
divToggle.onClick = () => {
store.dispatch(toggle());
};
btnIncrease.onClick = () => {
store.dispatch(increase(1));
};
btnDecrease.onClick = () => {
store.dispatch(decrease());
}
1.액션 타입은 항상 대문자로 만들어 주어야 합니다.
2.액션 생성 함수는 객체 형태로써 type 을 명시해 주어야 합니다.
3.리듀서 함수는 함수의 파라미터로 state 와 action 값을 받아 옵니다.
4.스토어는 redux 모듈에서 createStore 로 불러와야 합니다.
단일 스토어
하나의 애플리케이션 안에는 하나의 스토어만 들어가도록 하는게 좋습니다.
여러개의 스토어를 만들 수도 있지만, 상태 관리가 복잡해질 수 있으므로 권장하지 않습니다.
읽기 전용 상태
상태를 업데이트할 때 기존의 객체는 건드리지 않고 새로운 객체를 생성해 주어야 합니다. 리덕스에서 불변성을 유지해야 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교 검사를 하기 때문입니다.
리듀서는 순수한 함수
리듀서에서 순수한 함수는
1) 이전상태와 액션 객체를 파라미터로 받습니다.
2) 파라미터 외의 값에는 의존하면 안 됩니다.
3) 이전 상태는 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환합니다.
4) 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환합니다.
또한 리듀서 함수 내에서는 랜덤 값을 만들거나, 네트워크 요청을 하면 안됩니다.
다른 결과값을 만들수 있기 때문에 이러한 작업은 추후 밑에서 배우는 미들웨어를 통해 관리합니다.
앞서 나온 store 직접 사용하기 보다 react-redux 에서 제공하는 유팅 함수(connect) 와 컴포넌트(Provider) 를 사용하여 리덕스 관련 작업을 처리하는게 더 편합니다.
또한 다른 라이브러리를 사용하여 더 편하고 가독성 좋게 바꾸는 것이 좋습니다.
// reducers.js
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
export const increment = () => ({
type: 'INCREMENT'
});
export const decrement = () => ({
type: 'DECREMENT'
});
const initialState = {
count: 0
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
export default rootReducer;
이렇게 리듀서 까지 만든 모듈을 index.js 로 넘겨줍니다.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './App';
import rootReducer from './reducers';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
위 코드에서는 createStore 함수를 이용하여 store를 생성합니다.
이후, Provider 컴포넌트를 이용하여 store를 전역으로 사용할 수 있게 합니다.
여기서 createStore() 함수에 인자로 넣어준 것은 내가 만든 리듀서 함수입니다.
Provider 은 store 에 내가 만든 store 을 넣어 주어야 하고
자식 컴포넌트에서 redux 를 사용할수 있게 만들어 줍니다.
//App.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
const App = ({ count, increment, decrement }) => (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
const mapStateToProps = state => ({
count: state.count
});
const mapDispatchToProps = dispath => ({
increment: () => dispath(increment()),
decrement: () => dispath(decrement()),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
connect 함수를 사용할 때는 일반적으로 위 코드와 같이
mapStateToProps 와 mapDispatchToProps 를 미리 선언해 놓고 사용합니다.
하지만 connect 함수 내부에 익명 함수형태로 선언해도 문제가 되지 않습니다.
// 기본 코드
const mapStateToProps = state => ({
count: state.count
});
const mapDispatchToProps = dispath => ({
increment: () => dispath(increment()),
decrement: () => dispath(decrement()),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
// 익명 함수 형태
export default connect(
state => ({
count: state.count
}),
dispatch => ({
increase: () => dispatch(increase()),
decrease: () => dispatch(decrease()),
})
)(App);
위에 코드는 컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch 로 감싸는 직업이 조금 번거로울 수 있습니다.
따라서 리덕스에서 제공하는 bindActionCreators 유틸 함수를 사용하면 간편합니다.
하지만 이 유틸 함수도 생략할 수 있기 때문에 두가지 경우를 살펴보겠습니다.
// bindActionCreators
import { bindActionCreators } from 'redux';
export default connect(
state => ({
count: state.count
}),
dispatch =>
bindActionCreators({
increase,
decrease
})
)(App);
// 유틸 함수 생략
export default connect(
state => ({
count: state.count
}),
{
increase,
decrease
}
)(App);
액션 생성함수, 리듀서를 작성할 때 redux-actions 라는 라이브러리와 immer 라이브러리를 활용하면 리덕스를 훨씬 편하게 사용할 수 있습니다.
redux-actions 를 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있습니다.
리듀서를 작성할 때도 switch/case 문이 아닌 handleActions 라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있습니다.
//기존 redux-actions 적용 전 코드
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
export const increment = () => ({
type: 'INCREMENT'
});
export const decrement = () => ({
type: 'DECREMENT'
});
const initialState = {
count: 0
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
// redux-actions 적용 후 코드
import { createAction, handleActions} from 'react-redux'
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const initialState = {
count: 0
};
export const INCREASE = createAction(INCREASE);
export const DECREASE = creaseAction(DECREASE);
const rootReducer = handleActions({
[INCREASE]: (state, action) => ({count: action.count + 1}),
[DECREASE]: (state, action) => ({count: action.count - 1})
}, initialState)
export default rootReducer;
만약 단순히 값만 올리는 것 뿐만 아니라 어떤 값을 추가로 넣어주고 싶다면
밑에 코드처럼 바꾸어 주여야 한다.
import { createAction, handleActions } from 'redux-actions';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const initialState = {
count: 0
};
export const increase = createAction(INCREASE, id => id);
export const decrease = createAction(DECREASE, id => ({ id }));
const rootReducer = handleActions({
[increase]: (state, action) => ({ count: state.count + 1, id: action.payload.id }),
[decrease]: (state, action) => produce(state, draft => {
draft.count -= 1;
draft.id = action.payload.id;
}, initialState);
export default rootReducer;
immer 를 사용한다고 해서 모든 업데이트 함수에 immer 을 적용할 필요는 없습니다.
객체가 너무 복잡하다 생각이 들때 사용하는 것이 좋습니다.
connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks 를 사용할 수도 있습니다.
useSelector
useSelector Hook 을 사용하면 connext 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있습니다.
useDispatch
useDispatch Hook 을 사용하면 스토어의 내장 함수 dispatch 를 사용할 수 있게 해줍니다.
// 기존 코드
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
const App = ({ count, increment, decrement }) => (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
const mapStateToProps = state => ({
count: state.count
});
const mapDispatchToProps = dispath => ({
increment: () => dispath(increment()),
decrement: () => dispath(decrement()),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
//react-redux 사용
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';
const App = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()),[dispatch])
const onDecrease = useCallback(() => dispatch(decrease()),[dispatch])
return (
<div>
<h1>Count: {count}</h1>
<button onClick={onIncrease}>Increase</button>
<button onClick={onDecrease}>Decrease</button>
</div>
);
};
export default App;
useDispatch 를 사용할 때는 이렇게 useCallback과 함께 사용하는 습관을 권장합니다.