Posts [redux] redux-toolkit으로 리덕스 구성하기(액션, 리듀서, 스토어)
Post
Cancel

[redux] redux-toolkit으로 리덕스 구성하기(액션, 리듀서, 스토어)

이전 포스트(리덕스(Redux)란 무엇일까?)에서 이어집니다.

redux-toolkit을 사용하지 않고 기존 방식으로 리덕스 구성하기

이번 포스팅에서는, 우선 리덕스로 액션이나 리듀서를 어떤 식으로 정의하는지 간단하게 살펴보려고 합니다.

그리고 redux-toolkit의 createSlice 와 작성법을 비교해 보겠습니다.

(스토어 코드 등과 합쳐서 자세한 프로젝트 코드 작성은 별도의 포스팅에서 다룰 예정입니다.)

처음 시작할 때에는 공식 사이트의 Getting Started를 참고하는 것이 가장 빠른 지름길이라고 생각하는데요!

링크 : https://redux.js.org/introduction/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 이라는 이름을 가지고 있었습니다.

참고 : Idiomatic Redux: Redux Toolkit 1.0

그리고 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

JavaScript의 구조 분해 할당(Destructuring Assignment) 활용하기

[운영체제] 한번에 모아보는 면접대비 운영체제 질문 목록들