이번 포스팅에서는 redux-actions 와 immer를 사용해 Redux에서 action들을 통한 상태 관리를 보다 쉽게 하는 방법에 대해 알아보고자 한다. 우선 VSCode 터미널에서 redux-actions 와 immer 패키지를 설치하도록 하자.
npm install redux-actions
npm install immer
< 목차 >
- redux-actions
- immer
1. redux-actions
redux-actions 패키지에서 제공되는 createAction 과 handleActions 함수를 이용하면 redux에서 보다 쉽게 action들을 관리할 수 있다.
👉 createAction
creataAction 함수를 이용하면 action 생성을 자동화 할 수 있다. 이전 포스팅에서 redux-saga를 이용해 만들었던 카운터 예제 코드의 action을 createAction을 사용해 만들어보도록 하자.
2022.05.10 - [React] - Redux - redux-saga
아래는 reducers 디렉토리 안에 있는 counter.js 파일의 코드이다.
const initialState = {
number: 0,
loading: false,
error: null
}
const UP = 'COUNTER/UP_REQUEST'
const DOWN = 'COUNTER/DOWN_REQUEST'
export const up = (payload) => ({type: UP, payload})
export const down = (payload) => ({type: DOWN, payload})
const counter = (state = initialState, action) => {
switch (action.type) {
case 'COUNTER/UP_REQUEST' :
return {
...state,
loading: true,
error: null
}
case 'COUNTER/UP_SUCCESS' :
return {
...state,
loading: false,
number: state.number + 1
}
case 'COUNTER/UP_FAILURE' :
return {
...state,
loading: false,
error: '에러 발생'
}
case 'COUNTER/DOWN_REQUEST' :
return {
...state,
loading: true,
error: null
}
case 'COUNTER/DOWN_SUCCESS' :
return {
...state,
loading: false,
number: state.number - 1
}
case 'COUNTER/DOWN_FAILURE' :
return {
...state,
loading: false,
error: '에러 발생'
}
default :
return state
}
}
export default counter;
코드를 살펴보면 action 생성 함수를 하나하나 전부 만들어주면서 처리하고 있는 것을 확인할 수 있다. 하지만 createAction 함수를 사용하면 다음과 같이 action 생성을 자동화 할 수 있다.
// createAction() 사용 전
const UP = 'COUNTER/UP_REQUEST'
const DOWN = 'COUNTER/DOWN_REQUEST'
export const up = (payload) => ({type: UP, payload})
export const down = (payload) => ({type: DOWN, payload})
// createAction() 사용 후
const UP = 'COUNTER/UP_REQUEST'
const DOWN = 'COUNTER/DOWN_REQUEST'
export const up = createAction(UP)
export const down = createAction(DOWN)
createAction 함수의 인자값으로 type 내용을 전달해주면 전달받은 type을 가진 action 객체를 자동으로 생성해준다. 또한 createAction 함수로 만든 action 생성 함수는 파라미터로 전달받은 값을 action의 payload 값으로 설정해준다. 위의 예시 코드에서는 const up = createAction( UP ) 으로 action 생성 함수 up을 만들어주었다. up( ) 함수를 호출할 때 인자값을 넣어주면 해당 인자값이 action 객체의 payload로 들어가게 된다.
console.log( up('actionPayload') )
// output
// {type: 'COUNTER/UP_REQUEST', payload: 'actionPayload'}
👉 handleActions
reducer에서 action의 type에 따라 다른 작업을 하기 위해서 switch문을 사용했었다. 하지만 switch문을 사용하는 방식은 scope가 reducer 함수로 설정된다는 단점이 있다. 그렇기 때문에 서로 다른 case에서 let 이나 const 를 통해 변수를 선언하려고 하면 같은 이름이 중첩될 시에 에러가 발생하게 된다. handleActions 함수를 사용하면 이러한 문제를 해결할 수 있다.
import { createAction, handleActions } from 'redux-actions'
const initialState = {
number: 0,
loading: false,
error: null
}
const UP = 'COUNTER/UP_REQUEST'
const UP_SUCCESS = 'COUNTER/UP_SUCCESS'
const UP_FAILURE = 'COUNTER/UP_FAILURE'
const DOWN = 'COUNTER/DOWN_REQUEST'
const DOWN_SUCCESS = 'COUNTER/DOWN_SUCCESS'
const DOWN_FAILURE = 'COUNTER/DOWN_FAILURE'
export const up = createAction(UP)
export const down = createAction(DOWN)
const counter = handleActions(
{
[UP] : (state, action) => ({
...state,
loading: true,
error: null
}),
[UP_SUCCESS] : (state, action) => ({
...state,
loading: false,
number: state.number + 1
}),
[UP_FAILURE] : (state, action) => ({
...state,
loading: false,
error: '에러 발생'
}),
[DOWN] : (state, action) => ({
...state,
loading: true,
error: null
}),
[DOWN_SUCCESS] : (state, action) => ({
...state,
loading: false,
number: state.number - 1
}),
[DOWN_FAILURE] : (state, action) => ({
...state,
loading: false,
error: '에러 발생'
})
},
initialState
)
export default counter;
handleActions( ) 함수의 첫번째 인자로는 action에 따라 실행할 함수들을 가지고 있는 객체를, 두번째 인자로는 상태의 기본 값 (initialState) 을 넣어주면 된다.
2. immer
immer는 React에서 불변성을 유지하는 코드를 쉽게 작석할 수 있도록 도와주는 라이브러리이다. React 컴포넌트에서 상태를 업데이트 할 때 불변성을 지키는 것은 매우 중요한 일이다. 불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못하기 때문이다. 여기서 "불변성을 지킨다"라는 것은 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 말한다.
React에서는 기본적으로 부모로부터 내려받는 props나 내부 상태인 state가 변경되었을 때 컴포넌트를 다시 랜더링 하는 리랜더링 과정이 일어난다. React는 이 props와 state의 변경을 불변성을 이용해서 감지한다. 객체의 참조를 복사한다는 점을 이용해 단순히 참조만 비교하는 얕은 비교를 통해서 변경이 일어났는지 확인한다. 하지만 JavaScript에서 참조 타입의 데이터인 객체의 경우 메모리 힙 영역에 저장되어 내부 속성을 변경해도 같은 참조를 갖고 있다. 따라서 객체의 특정 속성만 변경하는 작업을 수행하면 React에서는 변경이 일어나지 않았다고 인식하여 리랜더링이 일어나지 않는다. 리랜더링을 일으키기 위해서는 React에 이전의 참조와 다른 참조로 변경되었음을 알려야만 한다.
위의 코드를 살펴보면 handleActions( ) 함수를 사용해서 상태값을 업데이트 할 때 spread 연산자를 사용해 { ...state, } 와 같이 기존의 상태값을 복사해서 새로운 객체 안에 넣어주는 방식으로 불변성을 지키면서 상태값을 바꿔주는 것을 확인할 수 있다. 이는 JavaScript에서 깊은복사를 하는 방법과 유사하다. 하지만 객체의 구조가 좀 더 복잡해진다면 불변성을 유지하면서 변경 내용을 업데이트 하는 것이 까다로워진다. immer를 사용하면 복잡한 구조일 경우에도 쉽고 편하게 불변성을 유지하면서 코드를 작성할 수 있다.
immer 패키지에서 주로 사용하게 될 함수는 바로 produce 함수이다. produce( ) 함수를 사용해서 위에서 작성된 코드를 보다 간결하게 수정해보도록 하자.
import { createAction, handleActions } from 'redux-actions'
import { produce } from 'immer'
const initialState = {
number: 0,
loading: false,
error: null
}
const UP = 'COUNTER/UP_REQUEST'
const UP_SUCCESS = 'COUNTER/UP_SUCCESS'
const UP_FAILURE = 'COUNTER/UP_FAILURE'
const DOWN = 'COUNTER/DOWN_REQUEST'
const DOWN_SUCCESS = 'COUNTER/DOWN_SUCCESS'
const DOWN_FAILURE = 'COUNTER/DOWN_FAILURE'
export const up = createAction(UP)
export const down = createAction(DOWN)
const counter = handleActions(
{
[UP] : (state, action) => produce(state, (draft) => {
draft.loading = true
}),
[UP_SUCCESS] : (state, action) => produce(state, (draft) => {
draft.number = state.number + 1
}),
[UP_FAILURE] : (state, action) => produce(state, (draft) => {
draft.error = '에러 발생'
}),
[DOWN] : (state, action) => produce(state, (draft) => {
draft.loading = true
}),
[DOWN_SUCCESS] : (state, action) => produce(state, (draft) => {
draft.number = state.number - 1
}),
[DOWN_FAILURE] : (state, action) => produce(state, (draft) => {
draft.error = '에러 발생'
})
},
initialState
)
export default counter;
produce( ) 함수는 기본적으로 두개의 인자값을 받는다. 첫번째 인자값으로는 업데이트 하고자 하는 현재의 상태값을 받고 두번째 인자값으로는 상태를 어떻게 업데이트 할 지 정의하는 함수를 받는다. 위의 코드를 살펴보면 ( draft ) => { } 함수가 produce( ) 함수의 두번째 인자값으로 들어가 있는 것을 확인할 수 있다. 여기서 draft는 현재의 상태값이 복사된 새로운 state라고 생각하면 된다. 그렇게 두번째 인자값으로 전달된 함수 내부에서 원하는 값을 변경하면 produce 함수가 불변성을 유지해 주면서 새로운 상태를 생성해준다.
immer 라이브러리의 핵심은 불변성을 신경쓰지 않는 것처럼 코드를 작성하되 불변성이 지켜진다는 점에 있다. 따라서 produce( ) 함수를 사용해서 상태값을 업데이트 해줄 때는 객체 안에 있는 값을 직접 수정하거나 배열에 직접적인 변화를 일으키는 push( ) , splice( ) 등의 함수를 사용할 수 있게 된다.
'React' 카테고리의 다른 글
Redux - redux-persist 세팅 (0) | 2022.05.10 |
---|---|
Redux - redux-saga (1) | 2022.05.10 |
Redux - redux middleware (0) | 2022.05.07 |
React - react-redux (0) | 2022.05.03 |
React - react-router-dom (0) | 2022.05.02 |