처음 만난 리덕스 (Redux) 문서


14.4 immer 사용 패턴

지금부터는 immer를 사용해서 Immutable Update를 하는 여러가지 패턴에 대해서 자세히 살펴보도록 하겠습니다.

14.4.1 State 수정 또는 반환

첫 번째 immer 사용 패턴은 State 수정 또는 반환입니다.
이 패턴은 State 일부를 수정하거나 또는 새로운 State를 반환함으로써 State를 업데이트 할 때 사용합니다.

import { createSlice } from '@reduxjs/toolkit';

const todoSlice = createSlice({
    name: 'todo',
    initialState: [],
    reducers: {
        add: (state, action) => {
            // 기존 state의 값을 직접 수정
            state.push(action.payload);
        },
        remove: (state, action) => {
            const itemId = action.payload;
            // 기존 state로부터 새로운 state를 생성해서 리턴
            return state.filter(todo => todo.id !== itemId);
        }
    }
});

위 코드는 immer를 사용한 State 수정 또는 반환 예시 코드입니다.
여기 Reducer를 작성하는 부분에서 각기 다른 형태로 State를 업데이트 하고 있습니다.

먼저 add Reducer는 배열의 push() 함수를 사용해서 기존 state의 값을 직접 수정하는 형태로 State를 업데이트 합니다.
그리고 remove Reducer에서는 배열의 filter() 함수를 사용해서 기존 state로부터 새로운 state를 생성해서 리턴하는 식으로 State를 업데이트 하고 있습니다.

이처럼 Redux Toolkit의 Reducer에서는 기존 state를 직접 수정하거나, 새로운 state를 만들어서 반환하는 형태로 State를 업데이트 할 수 있습니다.

14.4.2 State 교체 또는 초기화

다음으로 두 번째 immer 사용 패턴은 State 교체 또는 초기화입니다.
이 패턴은 State를 전체를 새로운 값으로 교체하거나 초기화 시킬 때 사용합니다.

import { createSlice } from '@reduxjs/toolkit';

const todoSlice = createSlice({
    name: 'todo',
    initialState: [],
    reducers: {
        replace: (state, action) => {
            // ❌ 에러: 값을 변경하거나 새로운 state를 리턴하지 않음!
            state = action.payload;

            // ✅ 정상: 새로운 state를 리턴하여 기존 state를 교체
            return action.payload;
        },
        reset: (state, action) => {
            // ✅ 정상: 초기 state를 리턴하여 기존 state를 초기화
            return initialState;
        },
    }
});

위 코드는 State 교체 또는 초기화 예시 코드를 나타낸 것입니다.

먼저 State를 교체하기 위한 replace Reducer에서는 위쪽에서 state에 직접 값을 대입하고 있는데, 이렇게 하면 아무런 변화도 생기지 않습니다.
그 이유는 Reducer 내에서의 state는 실제 State가 아닌 local 변수이기 때문입니다.
그래서 전체 State를 교체하려면 아래쪽에 있는 코드처럼 새로운 state를 리턴해야 합니다.

이렇게 state에 직접 값을 대입하는 형태는 Redux Toolkit을 처음 사용하는 개발자들이 가장 많이 하는 실수 중 하나입니다.
그렇기 때문에 이런 형태로 사용하지 않도록 꼭 유의하시기 바랍니다.

그리고 다음으로 State를 초기화 하려면 reset Reducer에 나와 있는 것처럼 initialState를 리턴하면 됩니다.

14.4.3 디버깅 및 Drafted State 검사

세 번째 immer 사용 패턴은 디버깅 및 Drafted State 검사입니다.
이 패턴은 Reducer 내에서 State를 디버깅 할 때 사용합니다.

import { createSlice, current } from '@reduxjs/toolkit';

const todoSlice = createSlice({
    name: 'todo',
    initialState: [],
    reducers: {
        add: (state, action) => {
            state.push(action.payload);

            // ❌ 에러: Proxy instance가 출력됨
            console.log(state);

            // ✅ 정상: 현재 시점의 state 값이 출력됨
            console.log(current(state));
        },
    }
});

위 코드는 디버깅 및 Drafted State 검사 예시 코드를 나타낸 것입니다.
add Reducer 내부에서 현재 state를 콘솔 로그로 확인하기 위해서 출력하고 있는 것을 볼 수 있습니다.

먼저 위쪽에 있는 코드는 state를 곧바로 콘솔로 출력하고 있는데, 이렇게 하면 실제 state의 값이 아닌 Proxy instance가 출력 됩니다.
그래서 앞에서 배운 것처럼 current() 함수를 사용해서 출력해야 현재 시점의 state 값이 정상적으로 출력됩니다.
이 부분을 잘 기억해두기 바랍니다.

14.4.4 중첩된 데이터 업데이트

마지막 네 번째 immer 사용 패턴은 중첩된 데이터 업데이트입니다.
이 패턴은 State내의 중첩된 데이터를 업데이트 할 때 사용합니다.

const todoSlice = createSlice({
    name: 'todo',
    initialState: [],
    reducers: {
        toggleDone: (state, action) => {
            const itemId = action.payload;
            const todo = state.find((todo) => todo.id === itemId);

            if (todo) {
                // ❌ 에러: Immer는 primitive value의 업데이트를 트래킹 할 수 없음!
                let { done } = todo;
                done = !done;

                // ✅ 정상: todo 객체가 Proxy로 감싸져 있기 때문에 수정할 수 있음
                todo.done = !todo.done;
            }
        },
    },
});

위 코드는 중첩된 데이터 업데이트 예시 코드를 나타낸 것입니다.

toggleDone이라는 Reducer는 할 일의 완료여부를 변경해주는 역할을 합니다.
Reducer 내에서는 먼저 itemId를 가지고 todo 배열에서 해당하는 todo 아이템을 찾습니다.
그리고 todo 객체가 존재할 경우, todo 객체 내에서 작업 완료 여부를 나타내는 done이라는 boolean 값을 변경합니다.

이 때 위쪽 방식처럼 todo 객체로부터 done 값을 꺼내서 새로운 변수로 선언하고,
이 변수의 값을 변경하면 제대로 업데이트가 되지 않습니다.
왜냐하면 Immer에서는 string, number, boolean 등의 primitive value의 업데이트를 트래킹 할 수 없기 때문입니다.

그래서 중첩된 데이터를 정상적으로 업데이트 하려면, 아래쪽 방법처럼 Proxy로 감싸져 있는 상태의 todo객체의 done값을 직접 변경해야 합니다.

이번에는 다른 형태의 중첩된 데이터 업데이트 예시 코드를 한 번 보도록 하겠습니다.

import { createSlice } from '@reduxjs/toolkit';

const todoSlice = createSlice({
    name: 'todo',
    initialState: { home: [], work: [] },
    reducers: {
        add: (state, action) => {
            const { category, item } = action.payload;

            // ❌ 에러: category에 해당하는 배열이 존재하지 않을 경우 오류 발생!
            state[category].push(item);

            // ✅ 정상: category에 해당하는 배열이 존재하는지 먼저 확인한 이후에 아이템 추가
            if (!state[category]) {
                state[category] = [];
            }
            state[category].push(item);
        },
    },
});

여기 add Reducer 내부에서는 두 가지 다른 방식으로 state를 업데이트 시도하고 있습니다.
먼저 위에 나와 있는 방식은 category에 해당하는 배열에 곧바로 아이템을 추가하는 형태입니다.
이렇게 하면 category에 해당하는 배열이 존재하지 않을 경우 오류가 발생하게 됩니다.
여기에서는 지금 category가 특정 값으로 정해져 있지 않기 때문이죠.

그래서 아래에 있는 방식처럼 category에 해당하는 배열이 존재하는지 먼저 확인하고, 만약 존재하지 않는다면 새로운 배열을 만들어 준 이후에 아이템을 추가해야 합니다.
이처럼 Immer는 State내에 중첩된 객체나 배열을 자동으로 생성해주지 않기 때문에 개발자가 직접 생성해야 한다는 점을 기억하기 바랍니다.


마지막 업데이트: 2023년 07월 14일 00시 00분

이 문서의 저작권은 이인제(소플)에 있습니다. 무단 전재와 무단 복제를 금합니다.