이 글은 super tokens 블로그의 "Why is redux state immutable?" 이라는 글을 한글로 번역하여 작성하였습니다. 원문 링크는 글 하단에 기재하였습니다.
Redux가 올바르게 동작하기 위해서 state는 불변(immutable) 객체여야 합니다. 이것은 우리가 redux state를 업데이트할 때마다, 전체 state의 복제본을 생성하고 변경하기 원하는 필드를 변경해야 한다는 것을 의미합니다. 코드로 보면 아래와 같습니다.
let newState = {
...oldState,
field1: {
...oldState.field1,
field2: 'some new value'
},
}
위 코드에서, 우리는 oldState의 field2 값을 새로운 state를 생성하고 field2에 새로운 값을 넣는 방식으로 수정했습니다. oldState의 값과 레퍼런스는 변경되지 않습니다.
왜 이러한 방식으로 redux state를 변경해야하는지 알아보기 전에, 우리는 값과 레퍼런스의 차이에 대해 알아야 합니다.
값과 레퍼런스의 차이
변수의 값이란 해당 변수가 갖고 있는 "의미론적" 의미입니다. 예를 들어, 아래 코드의 예시에서 var1과 var2가 갖고 있는 "의미론적" 값은 동일합니다. 그러므로 우리는 두 변수의 값이 "같다" 라고 할 수 있습니다. 하지만 var3의 값은 갖고있는 의미가 다르기 때문에 값이 다릅니다.
let var1 = { name: 'John', age: 20 }
let var2 = { name: 'John', age: 20 }
let var3 = { name: 'May', age: 30 }
레퍼런스에 대해 이야기 해보자면, 레퍼런스는 어떤 객체가 저장되어있는 메모리 주소에 대한 참조를 의미합니다. 위의 예시에서, var1이 참조하고 있는 메모리 주소는 var2가 참조하고 있는 메모리 주소와 다릅니다. 다른 말로, var1가 가리키고 있는 메모리 주소는 var2와 다릅니다. 그러므로 두 변수의 값은 같더라도, 레퍼런스는 다릅니다.
두 개의 변수가 같은 레퍼런스를 가질 수 있는 유일한 방법은 두 변수가 같은 메모리 주소를 가리키는 것 뿐입니다. 따라서 아래 코드에서 var4와 var5는 같은 레퍼런스를 갖습니다.
let var4 = { name: 'Jeremy', age: 50 }
let var5 = var4
만약에 var5.name = 'Mary' 와 같이 코딩하면, var4.name 의 값도 'Mary' 가 됩니다.
이러한 이해를 바탕으로 아래와 같이 결론을 내릴 수 있습니다.
- 만약 두 변수의 값이 같으면, 두 변수의 레퍼런스는 같을 수도 있고 다를 수도 있습니다.
- 만약 두 변수의 값이 다르면, 두 변수의 레퍼런스는 다를 수 밖에 없습니다.
- 만약 두 변수의 레퍼런스가 같으면, 두 변수의 값은 같을 수 밖에 없습니다.
- 만약 두 변수의 레퍼런스가 다르면, 두 변수의 값은 같을 수도 있고 다를 수도 있습니다.
React 컴포넌트의 리렌더링
React와 Redux로 돌아와서, react는 props나 state의 값이 변경된 경우에만 리렌더링되기를 원할 것입니다. props나 state의 값이 변경되었는지 확인하려면, 우리는 "깊은 비교"를 해야만 합니다. 즉, state나 props 내부의 모든 필드를 재귀적으로 확인해서 변경된 값이 있는 지 확인해야만 하는 것입니다.
규모가 있는 어플리케이션에는 일반적으로 redux를 사용할 때 매우 깊은 state 구조를 갖습니다. 꽤 큰 중첩 레벨(100~1000개 가량) 갖습니다. 이런 구조에서 매초당 여러 번 깊은 비교를 수행하게 되면, UI가 굉장히 느려질 것입니다. 반면에, "얕은 비교(첫 번째 레벨의 값이 변경되었는지만 확인하는 것)"를 하게 되면 훨씬 빨라지겠지만, 깊은 레벨의 값이 업데이트된 것을 놓치게 될 수 있고, 이렇게 되면 어플리케이션 로직이 깨지게 됩니다. 우리가 얕은 비교를 사용하였을 때 어떻게 업데이트를 놓치게 되는 지에 대한 예시를 아래의 코드로 보여 드리겠습니다.
let oldState = {
name: 'John',
age: 20,
profession: {
title: 'software enginner',
organisation: 'supertokens.io'
}
}
let newState = oldState
newState.profession.title = 'senior software enginner'
// 첫 번째 레벨만 얕은 비교
if (newState !== oldState || oldState.name !== newState.name || oldState.age !== newState.age || oldState.profession !== newState.profession) {
// UI 업데이트!
}
위 예시의 if 조건문에서 상태를 변경했고 UI 업데이트가 되길 바랬지만 업데이트가 되지 않습니다. newState.profession의 레퍼런스가 oldState.profession 의 레퍼런스와 같기 때문입니다.
또한 위의 조건문에서 oldState.profession !== newState.profession이나 newState !== oldState는 실제로 값이 아닌 레퍼런스가 동일한지 체크하고 있다는 것이 중요합니다(둘 다 객체이기 때문입니다).
불변성(immutable) 룰을 통해 최적화하기
만약 우리가 값이 업데이트되는 것을 놓치지 않고 얕은 비교를 할 수 있다면 위의 리렌더링의 문제를 해결할 수 있습니다. 그렇게 되면 어플리케이션의 성능이 향상 될 것이고 어플리케이션 로직도 깨지지 않을 것입니다.
우리는 지난 섹션을 통해서 "만약 두 변수(여기에서는 old state와 new state)가 다르다면, 두 변수의 값은 같을 수도 있고 다를 수도 있다"라는 것을 알고 있습니다. 그런데 우리가 만약 "만약 두 변수의 참조값이 다르다면, 우리는 두 변수의 값도 다르다고 가정합시다" 라고 바꾼다면 어떻게 될까요?
만약 위에서 바꾼 것처럼 룰을 정하고 시행한다면, state의 값이 변경 되었는지 알기 위해, 우리는 단지 oldState === newState와 같이 레퍼런스만 체크하면 됩니다(이 조건문이 false라면 레퍼런스가 변경된 것). 만약 레퍼런스가 변경되었다면, 우리는 값도 무조건 변경되었다고 가정하고 리렌더링을 하면 됩니다. 그리고 레퍼런스가 변경되지 않았다면 리렌더링을 하지 않으면 됩니다.
이 가정을 시행하려면, 우리는 oldState 내의 필드를 직접적으로 변경하면 안됩니다. 대신 우리는 이 글의 맨 위의 예시에서 보여준 것처럼 반드시 oldState의 복제본(newState)을 생성하고 newState에서 수정을 해야 합니다. newState는 새로운 객체이므로, newState의 레퍼런스는 언제나 oldState의 레퍼런스와 다릅니다. 바로 이것이 상태의 불변성을 강제하는 것으로 알려져 있는 것입니다.(정확히는 react와 redux가 사용자에게 강제하는 것입니다.)
결론
react와 redux state의 불변성은 state의 변화를 효율적으로 감지하게 해주기 때문에 꼭 필요합니다. 이것은 우리가 상태를 업데이트하고자 할 때, 새로운 복제본을 생성하고 그 복제본에서 수정을 해야한다는 것을 의미합니다. - 그러면 그것은 새로운 상태가 됩니다.
react와 redux의 상태를 사용하면서 왜 immutable하게 상태를 변경해야 하는지 모르고 사용하고 있었다면, 이 글이 도움이 되었을 것이라고 생각합니다. 우리가 사용하는 툴에서 강제하는 방식이 있다면 왜 그러한 방식이 채택되었는지 이해하는 것은 중요합니다. 이 글이 많은 도움이 되었으면 좋겠습니다. 읽어주셔서 감사합니다.
원문 URL : https://supertokens.com/blog/why-is-redux-state-immutable