불변 데이터
이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
Redux FAQ: 불변 데이터
불변성의 장점은 무엇인가요?
불변성은 앱 성능 향상을 가져올 수 있으며, 변경이 자유로운 데이터보다 전혀 변경되지 않는 데이터가 추론하기 쉽기 때문에 프로그래밍과 디버깅이 단순해집니다.
특히 웹 앱에서 불변성은 정교한 변경 감지 기법을 간단하고 저렴하게 구현할 수 있게 하여, DOM 업데이트라는 계산 비용이 큰 프로세스가 반드시 필요한 경우에만 수행되도록 보장합니다(이는 React가 다른 라이브러리 대비 성능 개선을 이루는 핵심 요소입니다).
추가 정보
아티클
Redux에서 불변성이 필요한 이유는 무엇인가요?
-
Redux와 React-Redux 모두 얕은 비교(shallow equality checking)를 사용합니다. 구체적으로:
- Redux의
combineReducers유틸리티는 호출한 리듀서로 인한 참조 변경을 얕은 비교로 확인합니다. - React-Redux의
connect메서드는 루트 상태에 대한 참조 변경을 얕은 비교하고,mapStateToProps함수의 반환값을 확인해 래핑된 컴포넌트의 재렌더링 필요 여부를 판단합니다. 이러한 얕은 비교는 정상 작동을 위해 불변성이 필요합니다.
- Redux의
-
불변 데이터 관리는 최종적으로 데이터 처리의 안전성을 높입니다.
-
시간 여행 디버깅은 리듀서가 부수 효과(side effect) 없는 순수 함수여야 서로 다른 상태 간 정확한 이동이 가능합니다.
추가 정보
문서
토론
Redux의 얕은 비교 사용이 불변성을 요구하는 이유는 무엇인가요?
연결된 컴포넌트가 올바르게 업데이트되려면 Redux의 얕은 비교 사용 시 불변성이 필요합니다. 그 이유를 이해하려면 JavaScript에서 얕은 비교와 깊은 비교의 차이를 알아야 합니다.
얕은 비교와 깊은 비교는 어떻게 다르나요?
얕은 비교(shallow equality checking, 또는 참조 동등성)는 단순히 두 _변수_가 동일한 객체를 참조하는지 확인합니다. 반면 깊은 비교(deep equality checking, 또는 값 동등성)는 두 객체의 모든 프로퍼티 _값_을 일일이 확인해야 합니다.
따라서 얕은 비교는 a === b만큼 간단하고(빠르며), 깊은 비교는 두 객체의 프로퍼티를 재귀적으로 순회하며 각 단계에서 프로퍼티 값을 비교합니다.
Redux가 얕은 비교를 사용하는 이유는 바로 이런 성능 향상 때문입니다.
추가 정보
아티클
Redux는 어떻게 얕은 동등성 검사를 사용하나요?
Redux는 combineReducers 함수에서 얕은 동등성 검사를 사용하여 루트 상태 객체의 변경된 새 복사본을 반환하거나, 변경 사항이 없을 경우 현재 루트 상태 객체를 반환합니다.
추가 정보
문서
combineReducers는 어떻게 얕은 동등성 검사를 사용하나요?
Redux 스토어의 권장 구조는 상태 객체를 키별로 여러 "슬라이스" 또는 "도메인"으로 분할하고, 각 개별 데이터 슬라이스를 관리하는 별도의 리듀서 함수를 제공하는 것입니다.
combineReducers는 키/값 쌍으로 구성된 해시 테이블 형태의 reducers 인수를 받아 작업합니다. 여기서 각 키는 상태 슬라이스의 이름이고, 해당 값은 이를 처리할 리듀서 함수입니다.
예를 들어 상태 형태가 { todos, counter }인 경우, combineReducers 호출은 다음과 같습니다:
combineReducers({ todos: myTodosReducer, counter: myCounterReducer })
여기서:
-
키
todos와counter는 각각 별도의 상태 슬라이스를 가리킵니다; -
값
myTodosReducer와myCounterReducer는 각 키에 해당하는 상태 슬라이스를 처리하는 리듀서 함수입니다.
combineReducers는 각 키/값 쌍을 순회하며 다음을 수행합니다:
-
각 키가 참조하는 현재 상태 슬라이스에 대한 참조를 생성합니다;
-
해당 리듀서를 호출하고 슬라이스를 전달합니다;
-
리듀서가 반환한 변경 가능성이 있는 상태 슬라이스에 대한 참조를 생성합니다.
순회 과정에서 combineReducers는 각 리듀서가 반환한 상태 슬라이스로 구성된 새 상태 객체를 만듭니다. 이 새 객체는 현재 상태 객체와 달라질 수도 있고 그대로일 수도 있습니다. 여기서 combineReducers는 얕은 동등성 검사를 통해 상태 변경 여부를 판단합니다.
구체적으로, 각 순회 단계에서 combineReducers는 현재 상태 슬라이스와 리듀서가 반환한 상태 슬라이스에 대해 얕은 동등성 검사를 수행합니다. 리듀서가 새 객체를 반환하면 얕은 동등성 검사가 실패하고, combineReducers는 hasChanged 플래그를 true로 설정합니다.
순회 완료 후 combineReducers는 hasChanged 플래그 상태를 확인합니다. true이면 새로 구성된 상태 객체를 반환하고, false이면 현재 상태 객체를 반환합니다.
강조할 점: 모든 리듀서가 전달받은 state 객체를 그대로 반환하면 combineReducers는 업데이트된 새 객체가 아닌 현재 루트 상태 객체를 반환합니다.
추가 정보
문서
영상
React-Redux는 어떻게 얕은 동등성 검사를 사용하나요?
React-Redux는 래핑된 컴포넌트의 재렌더링 여부를 판단하기 위해 얕은 동등성 검사를 사용합니다.
이를 위해 래핑된 컴포넌트가 순수하다고 가정합니다. 즉, 동일한 props와 state가 주어지면 컴포넌트가 동일한 결과를 생성할 것이라는 의미입니다.
래핑된 컴포넌트가 순수하다고 가정하므로, 루트 state 객체나 mapStateToProps에서 반환된 값이 변경되었는지만 확인하면 됩니다. 변경되지 않았다면 래핑된 컴포넌트는 리렌더링할 필요가 없습니다.
변경 사항은 루트 state 객체에 대한 참조와 mapStateToProps 함수에서 반환된 props 객체의 각 값에 대한 참조를 유지함으로써 감지합니다.
그런 다음 저장된 루트 state 객체 참조와 전달받은 state 객체에 대해 얕은 비교(shallow equality check)를 수행하고, props 객체 값들에 대한 개별 참조와 mapStateToProps를 다시 실행하여 반환된 값들에 대해 별도의 얕은 비교를 수행합니다.
추가 정보
문서
아티클
왜 React-Redux는 mapStateToProp에서 반환된 props 객체 내 각 값을 얕게 비교하나요?
React-Redux는 props 객체 자체가 아닌 props 객체 내 각 값에 대해 얕은 비교를 수행합니다.
이렇게 하는 이유는 props 객체가 실제로는 다음과 같이 프로퍼티 이름과 값(또는 값을 검색하거나 생성하는 셀렉터 함수)으로 구성된 해시이기 때문입니다:
function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}
export default connect(mapStateToProps)(TodoApp)
따라서 mapStateToProps를 반복 호출할 때마다 새로운 객체가 반환되므로, props 객체에 대한 얕은 비교는 항상 실패하게 됩니다.
그래서 React-Redux는 반환된 props 객체의 각 값에 대한 개별 참조를 별도로 유지합니다.
추가 정보
아티클
React-Redux는 어떻게 얕은 비교를 사용해 컴포넌트 리렌더링 필요성을 판단하나요?
React-Redux의 connect 함수가 호출될 때마다, 저장된 루트 state 객체 참조와 스토어에서 전달받은 현재 루트 state 객체에 대해 얕은 비교를 수행합니다. 비교가 성공하면 루트 state 객체가 업데이트되지 않았으므로 컴포넌트를 리렌더링하거나 mapStateToProps를 호출할 필요가 없습니다.
하지만 비교가 실패하면 루트 state 객체가 업데이트된 것이므로, connect는 래핑된 컴포넌트의 props가 업데이트되었는지 확인하기 위해 mapStateToProps를 호출합니다.
이는 객체 내 각 값에 대해 개별적으로 얕은 비교를 수행하여 이루어지며, 이 비교 중 하나라도 실패할 때만 리렌더링이 트리거됩니다.
아래 예제에서 state.todos와 getVisibleTodos() 반환값이 connect 연속 호출 시 변경되지 않으면 컴포넌트는 리렌더링되지 않습니다.
function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}
export default connect(mapStateToProps)(TodoApp)
반대로 다음 예제(아래)에서는 todos 값이 항상 새로운 객체이므로, 값이 변경되었는지 여부와 관계없이 컴포넌트가 항상 리렌더링됩니다:
// AVOID - will always cause a re-render
function mapStateToProps(state) {
return {
// todos always references a newly-created object
todos: {
all: state.todos,
visibleTodos: getVisibleTodos(state)
}
}
}
export default connect(mapStateToProps)(TodoApp)
mapStateToProps에서 반환된 새 값과 React-Redux가 참조로 유지한 이전 값 간의 얕은 비교가 실패하면 컴포넌트 리렌더링이 트리거됩니다.
추가 정보
아티클
토론
왜 가변 객체에서는 얕은 비교가 동작하지 않을까요?
객체가 가변적일 때, 함수가 전달받은 객체를 변형했는지 얕은 비교로는 감지할 수 없습니다.
왜냐하면 동일한 객체를 참조하는 두 변수는 객체의 값이 변경되더라도 항상 동일하기 때문입니다. 따라서 다음은 항상 true를 반환합니다:
function mutateObj(obj) {
obj.key = 'newValue'
return obj
}
const param = { key: 'originalValue' }
const returnVal = mutateObj(param)
param === returnVal
//> true
param과 returnValue의 얕은 비교는 단순히 두 변수가 동일한 객체를 참조하는지만 확인하며, 실제로 동일합니다. mutateObj()가 obj의 변형된 버전을 반환하더라도, 전달된 동일한 객체입니다. mutateObj 내부에서 값이 변경되었다는 사실은 얕은 비교에 전혀 영향을 미치지 않습니다.
추가 정보
아티클
가변 객체에 대한 얕은 비교가 Redux에 문제를 일으킬까요?
가변 객체에 대한 얕은 비교 자체는 Redux에 문제를 일으키지 않지만, React-Redux와 같은 저장소에 의존하는 라이브러리에는 문제가 발생합니다.
구체적으로, combineReducers가 리듀서에 전달한 상태 조각이 가변 객체인 경우 리듀서는 이를 직접 수정하고 반환할 수 있습니다.
이렇게 하면 combineReducers가 수행하는 얕은 비교는 항상 통과합니다. 리듀서가 반환한 상태 조각의 값은 변했을 수 있지만 객체 자체는 변하지 않았기 때문입니다. 여전히 리듀서에 전달된 동일한 객체입니다.
결과적으로 combineReducers는 상태가 변경되었음에도 hasChanged 플래그를 설정하지 않습니다. 다른 리듀서들도 새로운 업데이트된 상태 조각을 반환하지 않으면 hasChanged 플래그는 false로 유지되며, combineReducers는 기존 루트 상태 객체를 반환합니다.
저장소는 새로운 값으로 업데이트되지만, 루트 상태 객체 자체가 동일한 객체이기 때문에 React-Redux와 같은 Redux 바인딩 라이브러리는 상태 변화를 인지하지 못해 래핑된 컴포넌트의 리렌더링을 트리거하지 않습니다.
추가 정보
문서
상태를 변형하는 리듀서가 React-Redux의 래핑 컴포넌트 리렌더링을 방해하는 이유는 무엇인가요?
Redux 리듀서가 전달받은 상태 객체를 직접 변형하고 반환하면, 루트 상태 객체의 값은 변경되지만 객체 자체는 동일하게 유지됩니다.
React-Redux는 감싼 컴포넌트의 리렌더링 필요 여부를 판단하기 위해 루트 상태 객체에 얕은 검사를 수행하므로, 상태 변이를 감지하지 못해 리렌더링을 트리거하지 않습니다.
추가 정보
문서
선택자가 변경된 지속성 객체를 mapStateToProps에 반환하면 왜 React-Redux가 감싼 컴포넌트 리렌더링을 방해하나요?
mapStateToProps에서 반환된 props 객체의 값 중 하나가 connect 호출 간에 지속되는 객체(루트 상태 객체 등)인 동시에 선택자 함수에 의해 직접 변이되어 반환되면, React-Redux는 해당 변이를 감지하지 못해 감싼 컴포넌트의 리렌더링을 트리거하지 않습니다.
선택자 함수가 반환한 변경 가능 객체의 값은 변경되었을 수 있으나, 객체 자체는 동일하며 얕은 동등성 검사는 객체의 값이 아닌 객체 자체만 비교하기 때문입니다.
예를 들어 다음 mapStateToProps 함수는 절대 리렌더링을 트리거하지 않습니다:
// State object held in the Redux store
const state = {
user: {
accessCount: 0,
name: 'keith'
}
}
// Selector function
const getUser = state => {
++state.user.accessCount // mutate the state object
return state
}
// mapStateToProps
const mapStateToProps = state => ({
// The object returned from getUser() is always
// the same object, so this wrapped
// component will never re-render, even though it's been
// mutated
userRecord: getUser(state)
})
const a = mapStateToProps(state)
const b = mapStateToProps(state)
a.userRecord === b.userRecord
//> true
반대로 불변 객체를 사용할 경우 불필요한 리렌더링이 발생할 수 있습니다.
추가 정보
아티클
토론
불변성이 얕은 검사로 객체 변이를 감지하게 하는 원리는 무엇인가요?
객체가 불변일 경우 함수 내에서 변경 작업은 반드시 객체의 복사본에 수행해야 합니다.
이렇게 변이된 복사본은 원본 객체와 별개의 객체이므로 반환 시 얕은 검사에서 입력값과 다른 객체로 식별되어 검사가 실패합니다.
추가 정보
아티클
리듀서의 불변성이 컴포넌트 불필요 리렌더링을 유발하는 경우
불변 객체는 변이할 수 없으며, 대신 원본을 그대로 둔 채 복사본을 변이해야 합니다.
복사본 변이는 문제없지만, 리듀서 컨텍스트에서 변이되지 않은 복사본을 반환하면 Redux의 combineReducers 함수가 상태 업데이트가 필요하다고 판단합니다. 전달된 상태 슬라이스 객체와 완전히 다른 객체를 반환하기 때문입니다.
이후 combineReducers는 새 루트 상태 객체를 스토어에 반환합니다. 새 객체는 현재 루트 상태와 동일한 값을 가지지만 다른 객체이므로 스토어 업데이트가 발생하고, 결과적으로 모든 연결된 컴포넌트가 불필요하게 리렌더링됩니다.
이를 방지하려면 리듀서가 상태를 변이하지 않을 때 반드시 전달받은 상태 슬라이스 객체를 그대로 반환해야 합니다.
추가 정보
아티클
mapStateToProps에서 불변성이 컴포넌트를 불필요하게 렌더링하게 하는 이유는?
배열 필터링과 같은 특정 불변 연산은 값 자체가 변경되지 않았더라도 항상 새로운 객체를 반환합니다.
이러한 연산이 mapStateToProps에서 선택자 함수로 사용되면, React-Redux가 반환된 props 객체의 각 값에 대해 수행하는 얕은 동등성 검사가 항상 실패합니다. 선택자가 매번 새로운 객체를 반환하기 때문입니다.
따라서 새 객체의 값이 변경되지 않았음에도 래핑된 컴포넌트는 항상 재렌더링됩니다.
예를 들어 다음 코드는 항상 재렌더링을 트리거합니다:
// A JavaScript array's 'filter' method treats the array as immutable,
// and returns a filtered copy of the array.
const getVisibleTodos = todos => todos.filter(t => !t.completed)
const state = {
todos: [
{
text: 'do todo 1',
completed: false
},
{
text: 'do todo 2',
completed: true
}
]
}
const mapStateToProps = state => ({
// getVisibleTodos() always returns a new array, and so the
// 'visibleToDos' prop will always reference a different array,
// causing the wrapped component to re-render, even if the array's
// values haven't changed
visibleToDos: getVisibleTodos(state.todos)
})
const a = mapStateToProps(state)
// Call mapStateToProps(state) again with exactly the same arguments
const b = mapStateToProps(state)
a.visibleToDos
//> { "completed": false, "text": "do todo 1" }
b.visibleToDos
//> { "completed": false, "text": "do todo 1" }
a.visibleToDos === b.visibleToDos
//> false
반대로, props 객체의 값이 변경 가능한 객체를 참조하는 경우 컴포넌트가 렌더링되어야 할 때 렌더링되지 않을 수 있습니다.
추가 정보
아티클
데이터 불변성을 처리하는 방법은 무엇인가요? Immer를 꼭 사용해야 하나요?
Redux에서 Immer를 사용할 필요는 없습니다. 올바르게 작성한다면 일반 JavaScript만으로도 불변성을 완벽하게 제공할 수 있습니다.
그러나 JavaScript로 불변성을 보장하는 것은 어렵고, 실수로 객체를 변경하여 앱에서 찾기 매우 어려운 버그를 발생시키기 쉽습니다. 따라서 Immer와 같은 불변성 유틸리티 라이브러리를 사용하면 앱의 신뢰성을 크게 높이고 개발을 훨씬 수월하게 할 수 있습니다.
추가 정보
토론
일반 JavaScript로 불변성 작업을 할 때 발생하는 문제점은 무엇인가요?
JavaScript는 원래 불변 작업을 보장하도록 설계되지 않았습니다. 따라서 Redux 앱에서 불변성 작업에 JavaScript를 사용하기로 선택한다면 주의해야 할 몇 가지 문제점이 있습니다.
의도치 않은 객체 변이
JavaScript에서는 Redux 상태 트리와 같은 객체를 쉽게 의도치 않게 변이시킬 수 있습니다. 예를 들어 깊게 중첩된 속성 업데이트, 새로운 객체 대신 객체에 대한 새 참조 생성, 깊은 복사 대신 얕은 복사 수행 등이 모두 의도치 않은 객체 변이로 이어질 수 있으며, 경험 많은 JavaScript 개발자도 실수할 수 있습니다.
이러한 문제를 피하려면 권장되는 불변성 업데이트 패턴을 따르세요.
장황한 코드
복잡한 중첩 상태 트리를 업데이트하면 장황한 코드가 만들어질 수 있으며, 작성이 지루하고 디버깅이 어려워집니다.
낮은 성능
JavaScript 객체와 배열을 불변 방식으로 작업하는 것은 속도가 느릴 수 있으며, 특히 상태 트리가 커질수록 더욱 그렇습니다.
기억하세요, 불변 객체를 변경하려면 해당 객체의 복사본 을 변경해야 합니다. 큰 객체를 복사할 때는 모든 속성을 복사해야 하므로 속도가 느려질 수 있습니다.
반면, Immer와 같은 불변성 라이브러리는 구조적 공유를 활용할 수 있습니다. 이는 기존 객체의 상당 부분을 재사용하면서 새로운 객체를 효과적으로 반환하는 방식입니다.
추가 정보
문서
아티클