: 어떤 값을 직접 변경하지 않고, 새로운 값을 만들어내는 것이다... 이게 무슨 뜻일까? 어쨌든, 리액트를 사용할 때는 이러한 불변성을 지켜줘야
한다는건 모두가 대략적으로는 안다. 예를 들어,
const [someState, setSomeState] = useState("");
// 위와 같은 state를
const changeStateValue = (value) => {
someState = value;
}
위에와 같은 함수를 써서 혹은 로직을 써서 직접 값을 변경하면 안되고, setSomeState를 써야한다는 걸 안다. 그리고 리액트는 이렇게 직접 값을 변경하면 안되고, 새로운 값을 만들어내는 불변성을 지켜야(따라야)한다.
: Javascript Engine은 세개로 구성돼있다고 할 수 있는데, code area, call stack, heap memory 이다. 이 때, 불변성의 원리랑 이 엔진의 구성요소랑 무슨 관련이 있을까?
: 알다시피 자바스크립트의 데이터 타입은 두 종류로 분류 가능하다.
: 이 때, 자바스크립트 엔진이 각 타입의 데이터를 처리하는 방식을 통해 불변성 원리가 왜 리액트에 필요한지를 알아보자.
let stringTypeValue = "0715yk";
stringTypeValue = "yk0715";
위와 같은 코드가 있을 때 JS 엔진은 어떻게 처리를 할까?
처음엔 위와 같은 형태로 call stack의 변수값에 저장을 하게 된다. 그렇다면 두번째 줄을 실행하면? 즉, 변수에 값을 재할당하게 되면 어떻게 될까?
위와 같이 바뀌게 된다. 이 때, 봐야할건 우리가 흔히 얕은 복사 & 깊은 복사 혹은 얕은 비교 & 깊은 비교를 할 때의 얘기처럼 변수에 값을 재할당하니까 call stack에서는 해당 변수에 아예 다른 주소와 값을 저장했다. 그러면 이전에 있던 주소와 값은 어떻게 될까?. 이는 가비지 컬렉터의 판단에 의해 적절한 시점에서 메모리 상에서 삭제된다. 이런식으로 원시타입의 데이터는 콜스택에 저장되고, 재할당될 때, 즉, 값을 직접 수정하게 될 때 주소값 자체가 바뀌어서 저장된다. 이에 따라 이전에 변수에 있던 값(주소 값)과 재할당 이후에 변수에 있는 값(역시 주소 값)은 얕은 비교를 해도, 깊은 비교를 해도 다르다. 얕은 비교를 해도 다른건 주소값이 다르기 때문이고, 깊은 비교를 해도 다른건 0715yk 와 yk0715가 다르기 때문이다.
결론적으로 원시타입은 이런식으로 콜스택에서 저장되고, 변경된다고 생각하면 된다.
: 그럼 이제 참조 타입으로 넘어와보자. 참조 타입은 원시타입과 다르게 저장되는데, 일단 참조타입은 주소값은 원시타입처럼 콜스택에 저장되지만, 실제 값은 힙 메모리(Heap Memory)에 저장된다. 다른분의 블로그에서 그걸 잘 표현한 이미지를 가져와봤다.
(참조 : https://narup.tistory.com/268)
위와 같이 콜스택에 주소와 값이 있지만, 그 값에는 메모리 힙의 주소값이 들어가있고(빨강색 표시가 참조 타입이다), 메모리 힙에는 주소값을 키처럼 쓰고, 그 값으로 실제 값이 들어있다. 결론적으로 한번더 말하면, 실제 값은 힙 메모리에 들어가 있다는 것이다(참조 타입의 경우).
그러면 이러한 배경을 가지고 다시 앞서 한 것과 같이 직접 수정을 가하는 케이스 시뮬레이션을 돌려보자.
const array = [1,2,3];
array.push(77);
console.log(array); // [1,2,3,77]
먼저, 맨위의 코드를 실행했을 경우 콜스택과 메모리 힙에는 다음과 같은 형태로 데이터가 저장될 것이다. 그런 다음에 push 문을 실행하면 ?
콜스택의 주소와 값과 메모리 힙의 주소값은 동일하지만, 값만 바뀐 것을 알 수 있습니다. 이처럼 참조 타입은 원시 타입과 다르게 직접 수정을 가하면 주소 자체가 변동되는 것이 아니라 즉, 변수가 다른 주소값을 참조하게 되는식으로 바뀌는게 아니라 참조하는 주소값은 같고, 해당 주소에 대응되는 메모리 힙 내의 값에 변화가 있는 방식으로 수정이 이뤄집니다.
: 앞서 JS Engine과 원시타입 그리고 참조타입 등을 저장할 때의 방식을 알아봤는데, 이를 통해 처음 주제였던 불변성에 대해서 정리해보면,
지켜져야
한다. 즉, 리액트에서는 값을(state) 직접 수정하면 안되고, 새로 만들어야한다. 얕은 비교
를 통해 비교해서 그 비교가 false가 되면 리렌더링을 트리거한다. 이렇게 봤을 때 특정 변수를 바탕으로 setState를 일으키는 로직이 있다면, 원시타입일 때랑 참조타입일 때를 잘 구분해서 처리해야한다.얕은비교
를 쓰기 때문인데 그 얕은 비교
를 쓰는 이유는 이걸 깊은 비교
를 통해하면 리소스 낭비가 생기기 때문이다(얕은 비교가 더 쉽고 심플하기에). 좀 더 정리해보면, 얕은 비교를 하기 위해 불변성을 지켜야하고, 불변성을 지킨다는 의미는 결과적으로 메모리 영역(힙)에서 값을 변경할 수 없게하는, 즉, 콜 스택의 주소값을 변경해주는 형태로 값을 변경해야함을 의미한다.loadash._clonDeep
등의 방법론을 우리가 알고 있고, 써먹는거다라고 할 수 있다. import './App.css'
import {useState} from 'react'
let myIdValue = '0715yk'
function App() {
const [myId, setMyId] = useState(myIdValue)
const onClick = () => {
myIdValue = 'yk0715'
setMyId(myIdValue)
}
return (
<div>
<p>{myId}님 환영합니다!</p>
<button onClick={onClick}>click 해서 이름 바꾸기</button>
</div>
)
}
export default App
위에를 보면 0715yk -> yk0715로 정상적으로 리렌더링 됐다. 하지만, 참조타입을 직접 수정하는 방식으로 하면 어떻게 될까.
import './App.css'
import {useState} from 'react'
let myIdValue = {id: '0715yk'}
function App() {
const [myId, setMyId] = useState(myIdValue)
const onClick = () => {
myIdValue.id = 'yk0715'
setMyId(myIdValue)
}
return (
<div>
<p>{myId.id}님 환영합니다!</p>
<button onClick={onClick}>click 해서 이름 바꾸기</button>
</div>
)
}
export default App
위의 경우는 참조 타입인 객체 내에 id라는 키에 값을 넣어놨고, 그 값을 직접 수정해서 해당 객체를 바탕으로 데이터 리렌더링을 노려봤다(?). 그 결과 리렌더링이 발생하지 않는 것을 볼 수 있었다. 그 이유는 메모리 힙에서는 id 값이 'yk0715'로 정상적으로 변경됐겠지만 콜스택의 주소값은 그대로이기 때문이다.
이런 예시와 같은 일이 발생할 수 있기 때문에 리액트를 사용할 때는 불변성(어떤 값을 직접 수정하면 안되고, 새로운 값으로 만들어야한다는 것)을 지켜줘야 한다.