이전 포스트(리덕스(Redux)란 무엇일까?)에서 이어집니다.
redux-toolkit을 사용하지 않고 기존 방식으로 리덕스 구성하기
이번 포스팅에서는, 우선 리덕스로 액션이나 리듀서를 어떤 식으로 정의하는지 간단하게 살펴보려고 합니다.
그리고 redux-toolkit의 createSlice 와 작성법을 비교해 보겠습니다.
(스토어 코드 등과 합쳐서 자세한 프로젝트 코드 작성은 별도의 포스팅에서 다룰 예정입니다.)
처음 시작할 때에는 공식 사이트의 Getting Started
를 참고하는 것이 가장 빠른 지름길이라고 생각하는데요!
공식 스타트 문서에서는 리덕스 구성 시, Redux Toolkit
으로 시작하는 것을 추천하고 있습니다.
Redux Toolkit is our official recommended approach for writing Redux logic. It wraps around the Redux core, and contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
리덕스는 스토어 연결, 필요한 액션과 액션 타입을 정의하고, 그에 따른 리듀서도 별도 파일에 정의하는 등 만들어야 하는 파일들이 다소 많다고 느껴질 수 있습니다. 그것을 Redux Toolkit
이 소위 올인원의 느낌으로 한 파일 안에서 손쉽게 정의할 수 있도록 해 줍니다.
기존에 Redux Toolkit
을 사용하지 않고 액션, 액션 타입, 리듀서, 스토어를 각각 다른 파일에 정의하던 방식을 먼저 살펴보겠습니다.
Action Type과 Action, reducer 정의
특정한 액션
이 유발(dispatch)되면, 이에 대응하는 리듀서
가 변화한 상태(state)값을 반환하고, 이것은 전역 스토어
에 반영되는 것이 리덕스의 기본 흐름이라고 할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*** actionType.js ***/
export const SET_NAME = "USERS/SET_NAME";
export const SET_AGE = "USERS/SET_AGE";
/*** actions.js ***/
// 아래 함수들은 액션 생산자(Action Creator) 입니다.
export function setName(name) {
return {
type: SET_NAME,
name,
};
}
export function setAge(age) {
return {
type: SET_NAME,
age,
};
}
/*** reducers.js ***/
const initialState = {
name: "John",
age: 20,
};
export default function users(state = initialState, action) {
switch (action.type) {
case SET_NAME:
return {
...state,
name: action.name,
};
case SET_AGE:
return {
...state,
age: action.age,
};
default:
return state;
}
}
위에서 액션 타입, 액션(액션 생성자), 리듀서 모두 각각 다른 파일에 정의하고 있습니다.
특히 리듀서에서는 액션 case별로 반환하는 객체를 다르게 하고 있는데, 여기서 상태 변화 시 새로운 객체를 반환하고 있음을 알 수 있습니다. (참고 : JavaScript의 spread operator 포스팅의
Object Spread Properties
부분을 확인해 주세요!)
위 코드 중 리듀서의
SET_NAME
액션 케이스를 예로 들면,SET_NAME
발생 시 기존 state 객체 내부의 값을...state
로 복사해 오고, 그 중 name 프로퍼티의 값만 변화시켜서 새로운 객체로 반환하고 있습니다. 이는 리덕스가 state 변화 여부를, 이전 객체와 바뀐 객체가 서로 다른지로 판단하기 때문입니다. 따라서 state 에 값을 직접 대입하는 등의 변경은 허용하지 않습니다.
이처럼 리덕스 구성에 필요한 것들을 전부 다른 파일에 각각 작성하다 보니, 새로운 액션과 리듀서를 정의할 때마다 코드에 추가해 줘야 하는 번거로움이 생길 수 있습니다.
그래서 리덕스에서는 redux-toolkit
을 제안하고 있습니다.
redux-toolkit을 사용하여 리덕스 구성하기
redux-toolkit은 예전에는 Redux Starter Kit
이라는 이름을 가지고 있었습니다.
그리고 redux-toolkit이 생긴 이유를 아래와 같이 소개하고 있습니다.
- Make it easier to get started with Redux
- Simplify common Redux tasks and code
- Use opinionated defaults guiding towards “best practices”
- Provide solutions to reduce or eliminate the “boilerplate” concerns with using Redux
궁극적으로, Redux를 좀 더 손쉽고 단순하게 구성할 수 있게 하기 위해 탄생한 것입니다.
redux-toolkit을 리액트에서 사용하려면 아래 모듈을 설치해야 합니다.
1
npm install @reduxjs/toolkit
이후 redux-toolkit에 포함된 createSlice
로 액션 타입, 액션 생산자 함수, 리듀서 등 모든 기능을 하나의 파일 안에서 만들고자 합니다. (위의 redux-toolkit을 사용하지 않을 때와 비교해 보세요.)
참고로 createSlice
에서 reducer를 정의할 때에는 개발자가 불변성
을 고려하면서 작성하지 않아도, 내부적으로 immer
라이브러리를 사용해서 안전하게 불변성을 유지하면서 변화한 상태 객체를 반환시킵니다.
즉, createSlice
안에서는 그냥 이전 상태값에 새로운 값을 대입하듯이 코드를 작성해도 괜찮다는 의미입니다.
(Modern Redux with Redux Toolkit 의 createSlice 부분을 참고해 주세요)
redux-toolkit의 createSlice 활용하여 액션, 리듀서 정의하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*** userSlice.js ***/
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
name: 'John'
age: 20
};
const userSlice = createSlice({
name: 'USERS',
initialState,
reducers: {
setName(state, action) {
const name = action.payload;
state.name = name;
},
setAge(state, action) {
const age = action.payload;
state.age = age;
}
}
})
export const { setName, setAge } = userSlice.actions;
export default userSlice.reducer;
위처럼 하나의 파일 안에서 createSlice
로 모두 해결이 가능합니다.
별도의 액션 파일에서 만들었던 액션 생산자 함수들은 createSlice
에서 reducers: 내부에 정의하고 있습니다.
action.payload
는 해당 액션과 같이 전달하는 값을 의미합니다. 예를 들어,setName(value)
와 같이 액션을 호출하는 코드를 작성했다면 action.payload 값에는value
가 담기게 됩니다.- 불변성을 고려하여 Object spread properties 를 사용하지 않아도
state.name = name;
와 같이 작성하면 됩니다.
이처럼 정의한 뒤에는 USERS/setName
혹은 USERS/setAge
와 같이 액션 타입도 자동으로 가지게 됩니다.
또한 별도의 Action Creator 함수를 만들지 않고, 하단에서 export한 setName, setAge
를 사용하면 됩니다.
state값 내부의 프로퍼티 값만 변경하는 것이 아니라, state 값 전체를 통째로 바꿔야 하거나, state가 배열 형태였을 경우에는 reducers 내부에 적용하는 방식이 조금 다를 수 있습니다. 아래와 같은 상황인데요,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const initialState = [];
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// ❌ ERROR: mutates state, but also returns "new array size"!
brokenReducer: (state, action) => state.push(action.payload),
// ✅ SAFE: the `void` keyword prevents a return value
fixedReducer1: (state, action) => void state.push(action.payload),
// ✅ SAFE: curly braces make this a function body and no return
fixedReducer2: (state, action) => {
state.push(action.payload)
},
// ✅ SAFE - filter 메서드는 새로운 배열을 반환하기 때문
fixedReducer3: (state, action) => state.filter((item) => item > 10);
// state 값을 initialState 값으로 적용
resetReducer: () => {
return initialState;
}
},
})
예를 들어 위에서와 같이 배열 형태인 state에 어떤 값을 추가하려면 push
를 사용할 텐데, 이것을 return 문에 작성하면 안 됩니다. 왜냐하면 Array.prototype.push()
메서드는 배열의 새로운 ‘길이값’을 반환하기 때문입니다. 반대로 filter
메서드를 사용할 경우에는 ‘새로운 배열’을 반환하기 때문에 return문과 같이 작성합니다.
(() => {return someValue}
와 () => someValue
는 같은 의미입니다.)
state에 대한 각종 CRUD 함수를 제공하여 손쉽게 다루도록 해 주는 createEntityAdapter
에 대해서도 다음에 다룰 예정입니다.
참고 - https://redux-toolkit.js.org/tutorials/quick-start, https://redux-toolkit.js.org/usage/immer-reducers