본문으로 건너뛰기
비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

불변성 업데이트 패턴

필수 개념#불변 데이터 관리에 나열된 문서들은 객체 내 필드 업데이트나 배열 끝에 항목 추가하기 같은 기본적인 불변성 업데이트 연산 수행 방법에 대한 훌륭한 예시들을 제공합니다. 그러나 리듀서는 종종 더 복잡한 작업을 수행하기 위해 이러한 기본 연산들을 조합해야 합니다. 여기서는 구현해야 할 수 있는 일반적인 작업들에 대한 몇 가지 예시를 소개합니다.

중첩 객체 업데이트

중첩 데이터 업데이트의 핵심은 모든 중첩 수준을 적절히 복사하고 업데이트해야 한다는 것입니다. 이는 Redux를 배우는 사람들에게 종종 어려운 개념이며, 중첩 객체를 업데이트하려 할 때 자주 발생하는 특정 문제들이 있습니다. 이러한 문제들은 의도치 않은 직접 변이(mutation)로 이어지므로 피해야 합니다.

올바른 접근법: 모든 중첩 데이터 수준 복사하기

안타깝게도 깊게 중첩된 상태에 불변성 업데이트를 올바르게 적용하는 과정은 쉽게 장황해지고 가독성이 떨어질 수 있습니다. 다음은 state.first.second[someId].fourth를 업데이트하는 예시 모습입니다:

function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

명백하게 각 중첩 계층은 코드 읽기를 더 어렵게 만들고 실수할 기회를 더 많이 제공합니다. 이는 상태를 평평하게 유지하고 가능한 한 리듀서를 구성하도록 권장하는 여러 이유 중 하나입니다.

흔한 실수 #1: 동일한 객체를 가리키는 새 변수

새 변수를 정의한다고 해서 실제로 새로운 객체가 생성되는 것은 아닙니다 - 이는 동일한 객체에 대한 또 다른 참조(reference)만을 생성할 뿐입니다. 이 오류의 예시는 다음과 같습니다:

function updateNestedState(state, action) {
let nestedState = state.nestedState
// ERROR: this directly modifies the existing object reference - don't do this!
nestedState.nestedField = action.data

return {
...state,
nestedState
}
}

이 함수는 최상위 상태 객체의 얕은 복사(shallow copy)를 올바르게 반환하지만, nestedState 변수가 여전히 기존 객체를 가리키고 있었기 때문에 상태가 직접 변이되었습니다.

흔한 실수 #2: 한 수준만 얕은 복사하기

이 오류의 또 다른 일반적인 형태는 다음과 같습니다:

function updateNestedState(state, action) {
// Problem: this only does a shallow copy!
let newState = { ...state }

// ERROR: nestedState is still the same object!
newState.nestedState.nestedField = action.data

return newState
}

최상위 수준만 얕은 복사하는 것은 충분하지 않습니다 - nestedState 객체도 함께 복사되어야 합니다.

배열에서 항목 추가 및 제거

일반적으로 자바스크립트 배열의 내용은 push, unshift, splice 같은 변이 함수(mutative functions)를 사용해 수정됩니다. 리듀서에서 상태를 직접 변이시키지 않으려면 이러한 함수들은 일반적으로 피해야 합니다. 이 때문에 "추가"나 "제거" 동작이 다음과 같이 작성된 것을 볼 수 있습니다:

function insertItem(array, action) {
return [
...array.slice(0, action.index),
action.item,
...array.slice(action.index)
]
}

function removeItem(array, action) {
return [...array.slice(0, action.index), ...array.slice(action.index + 1)]
}

그러나 핵심은 원본 메모리 참조 가 수정되지 않아야 한다는 점을 기억하세요. 먼저 복사본을 만들기만 하면, 그 복사본을 안전하게 변이시킬 수 있습니다. 이 규칙은 배열과 객체 모두에 적용되지만, 중첩된 값은 여전히 동일한 규칙으로 업데이트해야 합니다.

이는 추가 및 제거 함수를 다음과 같이 작성할 수도 있음을 의미합니다:

function insertItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 0, action.item)
return newArray
}

function removeItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 1)
return newArray
}

제거 함수는 다음과 같이 구현할 수도 있습니다:

function removeItem(array, action) {
return array.filter((item, index) => index !== action.index)
}

배열 내 항목 업데이트

배열 내 한 항목을 업데이트하려면 Array.map을 사용하여 업데이트하려는 항목에 대해서는 새 값을 반환하고, 다른 모든 항목에 대해서는 기존 값을 반환하면 됩니다:

function updateObjectInArray(array, action) {
return array.map((item, index) => {
if (index !== action.index) {
// This isn't the item we care about - keep it as-is
return item
}

// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
}
})
}

불변성 업데이트 유틸리티 라이브러리

불변성 업데이트 코드 작성이 지루해질 수 있기 때문에, 이 과정을 추상화하려는 여러 유틸리티 라이브러리가 존재합니다. 이러한 라이브러리들은 API와 사용법이 다양하지만, 모두 업데이트 작성법을 더 짧고 간결하게 제공하려고 시도합니다. 예를 들어 Immer는 불변성 업데이트를 단순한 함수와 일반 자바스크립트 객체로 만듭니다:

var usersState = [{ name: 'John Doe', address: { city: 'London' } }]
var newState = immer.produce(usersState, draftState => {
draftState[0].name = 'Jon Doe'
draftState[0].address.city = 'Paris'
//nested update similar to mutable way
})

일부 라이브러리(예: dot-prop-immutable)는 명령에 문자열 경로를 사용합니다:

state = dotProp.set(state, `todos.${index}.complete`, true)

다른 라이브러리들, 예를 들어 immutability-helper (현재 폐기된 React Immutability Helpers 애드온의 포크)는 중첩된 값과 헬퍼 함수를 사용합니다:

var collection = [1, 2, { a: [12, 17, 15] }]
var newCollection = update(collection, {
2: { a: { $splice: [[1, 1, 13, 14]] } }
})

이들은 수동으로 불변 업데이트 로직을 작성하는 데 유용한 대안을 제공할 수 있습니다.

다양한 불변 업데이트 유틸리티 목록은 Redux 애드온 카탈로그불변 데이터#불변 업데이트 유틸리티 섹션에서 확인할 수 있습니다.

Redux Toolkit으로 불변 업데이트 간소화하기

우리의 Redux Toolkit 패키지는 내부적으로 Immer를 사용하는 createReducer 유틸리티를 포함합니다.

이를 통해 불변 업데이트 로직을 훨씬 간결하게 작성할 수 있습니다. 다음은 createReducer를 사용한 중첩 데이터 예제 모습입니다:

import { createReducer } from '@reduxjs/toolkit'

const initialState = {
first: {
second: {
id1: { fourth: 'a' },
id2: { fourth: 'b' }
}
}
}

const reducer = createReducer(initialState, {
UPDATE_ITEM: (state, action) => {
state.first.second[action.someId].fourth = action.someValue
}
})

이는 확실히 훨씬 짧고 읽기 쉽습니다. 그러나 이 기능은 리듀서를 Immer의 produce 함수로 감싸는 Redux Toolkit의 "마법 같은" createReducer 함수를 사용할 때만 올바르게 작동합니다. Immer 없이 이 리듀서를 사용하면 실제로 상태를 변형시킵니다! 코드만 보고 이 함수가 안전하게 불변 업데이트를 수행한다는 것을 바로 알기 어렵습니다. 불변 업데이트 개념을 완전히 이해했는지 반드시 확인하세요. 이를 사용할 경우 리듀서가 Redux Toolkit과 Immer를 사용한다는 설명을 코드에 주석으로 추가하는 것이 도움이 될 수 있습니다.

추가로 Redux Toolkit의 createSlice 유틸리티는 제공한 리듀서 함수를 기반으로 액션 생성자와 액션 타입을 자동 생성하며, 내부적으로 동일한 Immer 기반 업데이트 기능을 제공합니다.

추가 정보