리액트 불변성 지키기
메모리 구조를 파해쳐보자
들어가며😃
리액트를 사용하면서 배열이나 객체를 복사할때 마치 공식처럼 spread 연산자를 사용하곤 했다. 처음 공부 시작이 이론은 간단하게, 그리고 실습 위주로 빠르게 배우기도 했고, 큰 문제없이 사용했기 때문에 이론부터 다시 접근하려 하지 않았던 것 같다.🤯 매우,,부끄러운 발언이다. 어쨋던 그 벌로 이번주 원티드 프리온보딩 시간에 메모리제이션 최적화 파트에서 띵~ 해버렸다. React.memo 개념이 나와서 “아싸! 안그래도 정리하고 싶었던 개념이였는데 잘됐다!” 했는데 오히려 강의를 들을수록 기초부터 정리를 해야겠다라는 반성을 하게되었다.
어쨋던 강의를 통해 데이터의 불변성에 대해 알아보며 해당 개념에 대해 잘 알고 있어야 리액트를 잘 쓸 수 있겠구나 라는 생각이 들었다. 그래서 오늘은 리액트의 불변성에 대해 정리하며 개념을 더 확실히 익혀보려 한다.
javascript 메모리 구조와 데이터 타입
자바스크립트 엔진에는 call stack 과 heap 이라는 2가지 메모리 구조가 있다. call stack 에는 원시타입들이 저장되고 heap 메모리에는 참조 타입들이 저장된다.
그렇다면 타입은 뭘까?🤷🏻♀️
- 원시 타입 (기본형 타입) :
Boolean
,String
,Number
,null
,undefined
,Symbol
- 참조 타입 (객체형 타입) :
Object
,Array
해당 타입을 구별하는 것은 매우 중요하다. 왜냐하면 이 타입별로 데이터 저장 방식이 달라지기 때문이다. 아래에서 자세히 살펴보자.
타입별 데이터 저장 방식
원시타입
의 변수에 값을 선언하면 콜스택에 주소와 메모리 값이 저장된다. 위의 이미지를 응용해 만약 let b = 20 이 아닌 let b = 10 이었다면, a 가 가르키는 주소 값과 b가 가르키는 주소 값이 같아 지는 것이다. 그리고 a = 30 으로 변수 값을 변경하는 명령이 내려지면, 기존 콜스택 값을 변경하지 않고 새로운 주소와 메모리 값을 추가해 변수 a가 새로운 주소값을 바라보게 하는 것이다.
let a = 1;
let b = a;
console.log(b); //1
a = 3;
console.log(a); //3
console.log(b); //1
위에서 b는 a를 복사했지만, a의 값을 변경해도 b의 값은 변하지 않는다. 왜냐하면 b가 a를 복사하는 행위는 b가 1이라는 메모리 값의 주소를 바라보게 하는 것이고, a의 값이 3으로 변경되는 것은 a가 3이라는 메모리를 할당해 새로운 주소값을 바라보게 하는 것이기 때문이다.
메모리 영역의 값을 변경할 수 없는 것! 이것이 불변성인 것이다.
그렇다면 참조 타입
에서는 데이터를 어떻게 저장하고 할당할까?🤷🏻♀️
위의 이미지처럼 변수 b,c,d에 참조 타입의 데이터를 저장할 경우, 실제 값들은 메모리 힙에 저장되고, 메모리 힙의 주소가 콜스택의 값에 저장된다. 이는 원시 타입과는 다르게 데이터의 값이 변경되어도 콜스택의 같은 주소를 바라보고 있는 것을 뜻한다. 아래 이미지를 참조해보자.
기존의 a 와 b 를 변경한다면 메모리 힙에 담겨있는 값이 그자리에서 변경되고 콜스택의 값은 이전과 같은 메모리힙의 주소값을 가르키게 된다. 이는 아래와 같은 상황을 야기한다.
let arr1 = [1, 2, 3];
let arr2 = arr1;
console.log(arr2); // [1,2,3]
arr2.push(4); // [1,2,3,4]
console.log(arr1); // [1,2,3,4]
arr2 는 arr1 을 복사하였다. 그리고 arr2에 4를 추가한다. arr2는 당연히 [1,2,3,4]라는 배열이다. 그런데 건드린적 없는 arr1 또한 [1,2,3,4]로 바뀌어져있다.🥶
b가 a를 복사한 행위는 a의 메모리 힙 주소값을 가져오는 것이다. 그런데 콜스텍에 값을 할당하려니 같은 값을 가지게 되어 결국 a와 b는 콜스텍에서 같은 주소값을 가지게 되는 것이다. 그래서 b를 변경하면 원본인 a까지 변경되는 것이다.
리액트에서 불변성을 지켜야하는 이유
이것이 왜 중요하냐하믄 리액트에서 state 값이 변함에 따라 리렌더링이 일어나는데 state의 변화의 기준이 콜스텍의 주소값이기 때문이다. 이를 얕은 비교(shallow compare)
라고 한다.
원시타입의 경우 값을 재할당하면 새로운 메모리가 할당되어 콜스택의 주소 값이 변경되어 state 변화가 잘 감지될 것이다. 하지만 참조타입의 경우, 얕은 복사를 통해 값을 변경하면 메모리 힙의 값만 변할 뿐 콜스텍의 주소값은 변경이 없어 state 변화가 감지되지 않아 리렌더링 되지 않는 것이다. 그래서 spread 연산자를 통해 깊은 복사를 하거나, filter, slice, reduce 등의 메소드를 사용하며 새로운 배열을 만들어 의도적으로 불변성을 지켜주어야하는 것이다.
최적화를 위한 메모이제이션에 대해 더 포스팅 하기 전에 데이터 타입과 불변성에 대해 알아보았다. 시작은 원래 포스팅 하려했던 memo 파트의 의존성 배열 인자의 state를 더 잘 비교하기 위한 심화학습이였지만 왜 이제 했을까라는 아쉬움이 남는다. 그래도 이제라도 짚고 넘어가는게 참 다행인 부분,,😵💫🤕 오늘부터라도 왜 참조타입엔 깊은 복사가 필요한지 정확히 알고 설명할 줄 알았으면 된거다! 앞으로 더 찾아서 공부하자! 성장 성장 성장!!