slice메소드나 Spread Operator를 활용해서 새로운 배열을 생성한 경우 그냥 복사가 되었다고 넘어가면 절대 안된다.
"새로운 배열이 생성이 되었기 때문에 원본엔 영향을 주지 않겠지?" 라고 생각한다면 다음과 같은 문제를 겪을 수 있다.
동일한 배열을 생성하기 위해서 다음과 같이 spread 연산자를 사용하였다. 이제 복사를 했으니 한번 복사본을 변경해볼려고 하는데 원본도 변경이 되는 현상이 발생했다. 우리는 원본이 변경되는 것을 막기 위해서 spread 연산을 하였지만 우리가 원하는 결과를 얻을 수 없었다.
근데 왜 이런 현상이 발생하게 된 걸까?
const array = [1, [2, 3]];
const shallowCopy = [...array];
shallowCopy[1][0] = 9;
console.log(array === shallowCopy); // false
console.log(array); // [1, [9, 3]]
이 부분을 이해 하기 위해선 자바스크립트의 참조 타입에 대해서 이해할 필요가 있다. 참조타입의 데이터를 이해하기 위해서 데이터영역과 변수영역 개념에 대해서 알아야 하지만 지금은 왜 저런 현상이 발생했는지를 쉽게 이해하기 위해서 해당 단어는 제외하도록 하겠습니다.
위의 그림과 같이 얕은 복사를 한 경우에는 새로운 배열을 생성 한 뒤 shallowCopy는 해당 배열의 주소값을 가리키도록 한다. 그러면 결국 array와 shallowCopy는 서로 다른 배열의 주소값을 가지고 있기 때문에 array === shallowCopy는 false 라는 것이 이해될 수 있다.
그러면 원본은 왜 변경되는거지?
"array와 shallowCopy가 서로 다른 배열이니깐 원본엔 영향을 줄 수 없지" 하고 array를 출력할 경우 [1, [9, 3]] 이라는 배열로 변경이 된 것을 볼 수 있다. 이 부분 또한 그림에서 볼 수 있듯이 내부 속성에 다른 배열(참조타입)이 있는 경우 두개의 변수 모두 동일한 주소값을 가리키고 있기 때문에 shallowCopy의 속성을 변경하였을 때 array에도 영향을 주게 되어 버린다.
결국 완전한 복사가 이루어진 것이 아니였다.
1. Spread Operator
사실 스프레드 연산자는 얕은 복사밖에 할 수 없다. 그래서 2차원 배열이나 배열안에 객체가 있는 경우는 완벽한 복사를 이룰 수 없다. 만약 한단계 아래의 참조가 존재하게 된다면 그 데이터를 변경할 경우 원본도 동일하게 변경된다. 위를 예제를 참고해서 이해 해보도록 하자.
2. slice
사용해본 사람들은 slice가 배열을 잘라서 새로운 배열을 반환하는 메소드라는 것을 알고 있을 것이다.
일단 slice의 경우 splice와 다르게 원본을 변경하지 않는다. 메소드를 호출한 당시에는 원본을 변경하지 않지만, splice의 반환값의 경우엔 얕은 복사로 이루어진 배열이기 때문에 만약 내부에 배열, 객체와 같이 참조 타입의 데이터가 있는 경우에는 원본에 영향을 줄 수 있다.
const array = [1, [2, 3]];
const shallowCopy = array.slice(1, 2); // [[2, 3]]
shallowCopy[0][0] = 3;
console.log(array); // [1, [3,3]]
3. 그러면 DeepCopy를 하기 위해선 어떻게 해야 될까?
대부분은 사람들이 이용하는 방식은 총 3가지가 있다.
- JSON.stringify와 JSON.parse 사용하기
- 재귀적으로 내부의 모든 참조타입에 대해서 새로운 주소값을 변경하기
- lodash 라이브러리 사용하기
자세하게 다뤄진 블로그 글이 많기 때문에 자세한 설명에 대해서 넘어가려고 한다.
4. 리액트는 불변성을 지켜야 한다. 이게 도대체 무슨 말이야?
리액트의 경우 얕은 비교를 데이터의 변경을 확인한다. 제일 최상위의 주소값(변수가 가리키고 있는 주소값)을 비교하여 변경이 되었는 지 확인하고 변경이 되었다면 새롭게 렌더링을 진행하게 된다.
리액트를 배우신 분들이라면 모두 이런 말을 한번씩 들어봤을 것이다.
"리액트는 불변성을 지켜야 된다."
대부분의 블로그에선 "리액트는 불변성을 지켜야하고 불변성을 지키기 위해선 새로운 배열을 생성해서 갈아끼워야 된다. 이것을 지키게 된다면 리액트의 성능이 좋아진다." 이런식의 설명된 글이 많은 거 같다.
리액트에서의 "불변성을 지켜야 된다"는 말은 무엇일까?
이 개념을 이해하기 위해선 원시타입과 참조타입에 대해서 알아야 된다.
그림과 같이 원시타입의 경우 3, 4는 절대 값을 변경할 수 없는 것을 볼 수 있다. 즉, 메모리 영역에서 들어있는 값을 변경할 수 없고 이러한 이유로 원시타입의 경우에는 불변값이라고 말하는 것이다.
다만 헷갈리기 시작한 것은 바로 이 참조타입이라는 놈 때문이다. 참조타입의 경우에는 내부에 있는 속성들이 가리키고 있는 주소값을 마음대로 변하게 할 수 있기 때문에 불변성이 아닌 가변값이라고 불리게 된다.
이제 본론으로 돌아와서 결과적으로 원시타입의 경우에는 불변값이기 때문에 불변성을 지켜준다고 볼 수 있지만, 객체나 배열은 왜 불변성이 아닌데 사용 안해야 되는 거 아니야? 애는 가변값인데 어떻게 불변성을 지켜주지? 라는 생각을 할 수 도 있다.
변수가 가리키고 있는 주소값은 얼마든지 변할 수 있다. 변수에 원시타입을 대입할 때, 각각의 원시타입 데이터들의 주소값을 대입하게 된다. 참조타입의 데이터의 경우에는 내부 속성들이 가리키고 있는 주소값을 변경할 수 있기 때문에 가변성이라고 불린다고 하였다. 이러한 가변성을 없애기 위해서 변수에 새로운 참조타입을 생성 후 해당 주소값을 가리키게 하면 된다.
위의 그림을 보면서 더 자세히 이해해보도록 하자. 변수 b에 [1, 2, 3] 배열이 있는 주소값(0X1000)대입 되어있는 상태였고 마지막 인덱스의 값을 변경하기 위해서 [1, 2, 4] 배열을 생성하였다. 그 후 b에 새로운 배열 있는 주소값(0X1001)을 대입하였다. 그러면 그림에서 볼 수 있듯이 원시타입과 동일하게 0x1000배열의 내부 속성들이 가리키고 있는 주소값들을 변경할 수 없게 되고 가비지 컬렉터가 수거하기 전까지 메모리 영역에 남아있게 된다. 이말은 즉, "불변성을 지켰다" 라고 말할 수 있다.
리액트에서의 불변성을 지켜야된다는 말은 원시타입의 경우에는 문제가 없지만, 참조타입의 경우에는 가변성 데이터이기 때문에 복사를 통해 참조타입도 불변성 데이터처럼 동작하게 만들라는 것과 같다. 즉 메모리 영역에서 변수를 제외한 모든 데이터들은 변경할 수 없다.
이렇게 불변성을 지켜주게 되면서 리액트의 얕은 비교를 통해 쉽게 변화를 감지하여 재렌더링을 진행할 수 하였습니다.
4. Remind
얕은복사와 깊은 복사를 이해하기 위해 메모리영역과 데이터의 불변성과 가변성에 대해서도 이해를 하고 있어야 된다. 모든 상위 개념을 이해하기 위해선 하위개념을 알고 있어야 된다는 것을 다시 한번 느낄 수 있었다.
'Programming > JavaScript' 카테고리의 다른 글
Call by reference와 Call by value의 차이 (0) | 2024.03.17 |
---|---|
Comma를 찍는 다양한 방법 (0) | 2024.03.09 |
빈칸을 채우기 위한 padStart, padEnd (0) | 2022.11.17 |