yarn add redux-action
redux-actions
은 action creator를 간결하게 작성할 수 있게 만들어줍니다.
다음 코드는 지난 실습 때 작성한 액션과 액션 생성 함수 코드 입니다.
const INCREASE = 'reduxCounter/INCREASE';
const DECREASE = 'reduxCounter/DECREASE';
export const increase = () => ({
type: INCREASE,
});
export const decrease = () => ({
type: DECREASE,
});
이 코드에 redux-actions
을 추가하면 객체 생성 과정 없이 간단하게 액션 생성 함수를 만들 수 있습니다.
import {createAction} from 'redux-action'
const INCREASE = 'reduxCounter/INCREASE';
const DECREASE = 'reduxCounter/DECREASE';
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
액션 생성 함수가 엄청나게 간단해지지 않았나요?
또한 redux-actions
에는 리듀서 함수도 더 간단하게 만들 수 있는 함수인 handleActions
함수를 지원합니다. 다음 코드를 보면 redux-actions를 이용하면 더 간결하고 직관적인 코드가 탄생함을 알 수 있습니다.
//redux-action 사용 이전 리듀서 코드
function reduxCounter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return {
number: state.number + 1
};
case DECREASE:
return {
number: state.number - 1
};
default:
return state;
}
}
//redux-action 사용 이후 리듀서 코드
const reduxCounter = handleActions(
{
[INCREASE]: (state, action) => ({number: state.number + 1}),
[DECREASE]: (state, action) => ({number: state.number - 1}),
},
initialState,
);
todos 앱도 redux-actions
를 이용해서 바꿔보겠습니다.
//이전 코드
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
export const changeInput = input => ({
type: CHANGE_INPUT,
input,
});
let id = 3;
export const insert = text => ({
type: INSERT,
todo: {
id: id++,
text,
done: false,
},
});
export const toggle = id => ({
type: TOGGLE,
id,
});
export const remove = id => ({
type: REMOVE,
id,
});
//이후 코드
import {createAction} from 'redux-actions';
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
export const changeInput = createAction(CHANGE_INPUT, input => input);
let id = 3;
export const insert = createAction(INSERT, text => ({
id: id++,
text,
done: false,
}));
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);
createAction
함수에 두번째 파라미터로 함수가 들어갔습니다. 이 함수는 액션에서 필요로 하는 추가 데이터를 넣어줍니다. 이 데이터는 payload
라고 부릅니다. 그래서 데이터를 가공 없이 그대로 넣는다면 changeInput, toggle, remove 처럼 파라미터를 그대로 반환하면 되고, 데이터에 변형이 필요하다면 insert 처럼 파라미터를 가공한 것을 return 해주면 됩니다.
insert 액션 생성 함수는 todo 객체를 액션 객체에 넣어서 이용해야하기에 입력된 text를 파라미터로 해서 todo를 반환하도록 만들었습니다.
이외에 파라미터를 바로 반환하는 payload들은 생략해도 무방하지만, 위 코드처럼 명시하는 경우 각 액션 생성 함수에 파라미터로 어떤 값이 사용되는지 알 수 있게 됩니다.
이번는 handleActions 함수로 리듀서를 고쳐보겠습니다.
//이전 코드
function todos(state = initialState, action) {
switch (action.type) {
case CHANGE_INPUT:
return {
...state,
input: action.input,
};
case INSERT:
return {
...state,
todos: state.todos.concat(action.todo),
};
case TOGGLE:
return {
...state,
todos: state.todos.map(todo => todo.id === action.id ? {...todo, done: !todo.done} : todo),
};
case REMOVE:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id),
};
default:
return state;
}
}
//이후 코드
const todos = handleActions({
[CHANGE_INPUT]: (state, action) => ({...state, input: action.payload}),
[INSERT]: (state, action) => ({...state, todos: state.todos.concat(action.payload)}),
[TOGGLE]: (state, action) => ({
...state, todos: state.todos.map(
todo => todo.id === action.payload ? {...todo, done: !todo.done} : todo)
}),
[REMOVE]: (state, action) => ({
...state, todos: state.todos.filter(
todo => todo.id !== action.payload
)
}),
},
initialState,
);
리듀서도 좀 더 직관적이고 간결하게 바뀌었습니다.
리듀서에서 각 액션 생성 함수는 액션 객체 내부에 값을 넣을 때 action.payload
라는 이름으로 넣게 됩니다.
만약 모든 액션 생성 함수에서 payload라는 중복된 이름으로 인해 가독성이 저하된다고 판단된다면, 다음과 같이 비구조화 할당 문법으로 payload의 이름을 다르게 설정 할 수 있습니다.
const todos = handleActions({
[CHANGE_INPUT]: (state, {payload: input}) => ({...state, input}),
[INSERT]: (state, {payload: todo}) => ({...state, todos: state.todos.concat(todo)}),
[TOGGLE]: (state, {payload: id}) => ({
...state, todos: state.todos.map(
todo => todo.id === id ? {...todo, done: !todo.done} : todo)
}),
[REMOVE]: (state, {payload: id}) => ({
...state, todos: state.todos.filter(
todo => todo.id !== id
)
}),
},
initialState,
);
불변성
은 기존 값은 가만히 두고 새로운 값을 만들어 내는 것을 말합니다. 불변성
을 지키려면 한 객체(배열 등)를 깊은 복사해서 별도의 객체를 만들고 만들어내고 별도의 객체의 값을 수정해야 불변성을 지킬 수 있습니다.
자바스크립트에서 깊은 복사를 하는 방법에는 Object.assign()
이나 ...
전개 연산자를 이용하는 방식이 있습니다. 하지만 두 방식 모두 겉의 값만 깊은 복사를 하고 내부 값에 대해선 얕은 복사를 하기 때문에 immer
라는 라이브러리를 이용하게 됩니다.
//Object.assign()이나 전개 연산자는
//객체 내부의 객체인 inside에 대해서는 얕은 복사가 이루어집니다.
let obj = {
outside: 0,
inside: {
x: 100,
},
};
//내부까지 복사하기 위해서는 다음과 같이 작성해야합니다.
let copiedObj = {
...obj,
inside: {
...obj.inside,
},
};
객체 내부 구조가 이것보다 더 복잡하다면 이렇게 구현해버리는 것도 한계가 있습니다. 물론 객체가 너무 깊어지지 않도록 설계하는 것이 좋겠지만, 불가피한 상황은 오기 마련입니다. 그래서 immer
라이브러리를 이용하면 복잡한 객체나 객체 배열에서 불변성을 쉽게 유지할 수 있게 해줍니다.
그럼 immer
를 설치하고 객체를 이용하는 todos 컴포넌트의 TOGGLE 액션 함수에 적용시켜 보겠습니다.
yarn add immer
[TOGGLE]: (state, {payload: id}) => produce(state, draft => {
const todo = draft.todos.find(todo => todo.id === id);
todo.done = !todo.done;
}),
immer
의 produce
함수는 다음과 같이 사용합니다.
produce(상태, 업데이트 함수 => {});
함수의 첫번째 파라미터인 상태는 변경하고 싶은 상태를 넣습니다. TOGGLE 액션 생성 함수에서는 파라미터로 가져온 state를 변화시키고 싶으므로 state를 그대로 적었습니다. 그리고 두번째 업데이트 함수는 상태를 어떻게 업데이트 할 지 정의하는 함수입니다. 함수 몸통에 상태 업데이트에 대한 내용을 적으면 불변성 유지를 하면서 상태를 변화시켜 줍니다.
react-redux
에서 제공하는 Hooks로 리덕스를 연동할 때 사용했던 connect 함수를 대체 할 수 있습니다.
한 가지 주의점은 어디까지나 대체일 뿐이고 어느 것이 좋다라고는 할 수 없습니다. connect
함수는 컨테이너의 부모 컴포넌트가 리렌더링 될 때 컨테이너 컴포넌트의 props 변화가 없다면 리렌더링을 방지해 성능을 최적화 해 줍니다. 반면 useSelector
Hook은 코드가 간결해지는 대신 성능 최적화를 해주지 않으므로 React.memo()
를 이용해 성능 최적화를 해야합니다. React.memo 성능 최적화도 간단한 코드이기 때문에 뭐가 더 편하고 불편하다의 차이는 없는 것 같습니다. 그러므로 사용하기 전에 부모 컴포넌트의 리렌더링 여부 등을 따져보고 상황에 알맞는 방식이나 더 편한 방식을 이용하면 됩니다.
useSelector
는 connect 함수 대신 리덕스 상태를 조회하는 Hook입니다.
다음 코드는 기존 카운터 컨테이너 컴포넌트 코드와 useSelector
를 이용한 방식입니다.
//기존 코드
import React from 'react';
import {connect} from 'react-redux';
import ReduxCounter from '../ReduxCounter';
import {decrease, increase} from '../ducks-modules/reduxCounter';
const ReduxCounterContainer = ({number, increase, decrease}) => {
return <ReduxCounter number={number} onIncrease={increase} onDecrease={decrease}/>;
};
export default connect(
state => ({number: state.reduxCounter.number}),
{
increase,
decrease,
},
)(ReduxCounterContainer);
//useSelector Hook 사용 코드
import React from 'react';
import {useSelector} from 'react-redux';
import ReduxCounter from '../ReduxCounter';
import {increase, decrease} from '../ducks-modules/reduxCounter';
const ReduxCounterContainer = () => {
const number = useSelector(state => state.reduxCounter.number);
return <ReduxCounter number={number}/>;
};
export default ReduxCounterContainer;
useDispatch
Hook을 이용하면 컴포넌트 내부에서 dispatch()함수를 이용할 수 있게 됩니다.
import React from 'react';
import {useDispatch, useSelector} from 'react-redux';
import ReduxCounter from '../ReduxCounter';
import {increase, decrease} from '../ducks-modules/reduxCounter';
const ReduxCounterContainer = () => {
const number = useSelector(state => state.reduxCounter.number);
const dispatch = useDispatch();
return (
<ReduxCounter
number={number}
onIncrease={()=>dispatch(increase())} onDecrease={()=>dispatch(decrease())}
/>
);
};
export default ReduxCounterContainer;
증감 버튼 클릭시 마다 함수가 새로 생성되어 동작하는 것을 방지하기 위해 useCallback
Hook와 함께 사용합니다.
import React, {useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import ReduxCounter from '../ReduxCounter';
import {increase, decrease} from '../ducks-modules/reduxCounter';
const ReduxCounterContainer = () => {
const number = useSelector(state => state.reduxCounter.number);
const dispatch = useDispatch();
const onIncrease = useCallback(()=> dispatch(increase()), [dispatch]);
const onDecrease = useCallback(()=> dispatch(decrease()), [dispatch]);
return <ReduxCounter number={number} onIncrease={onIncrease()} onDecrease={onDecrease()}/>;
};
export default ReduxCounterContainer;
이 방식 외에도 useStore라는 Hook을 이용하면 Store 객체에 직접 접근할 수도 있습니다.