왜 데이터의 불변성에 대해서 학습하는가? Redux 미션을 하던 도중. 불변성에 대한 질문을 받았다.
그러면 의도하지 않은 객체의 변경이 발생하는 원인의 대다수는 무엇인가..?
modern JS에 따르면, 의도하지 않은 객체의 변경이 발생하는 원인의 대다수는 “레퍼런스를 참조한 다른 객체에서 객체를 변경” 하기 때문이다….. → 의도하지 않은 객체의 변경의 본질적 원인을 파악하기 위해서 이 글을 쓴다.
그리고 이 글의 마지막에는 이 결론도 알 것이다. 참조형에서 왜 기존 데이터를 복사해와서 새로운 데이터를 생성하는 것인지
const updatedStudyLogWeek = [
...studyLogWeeks.slice(0, dayIndex),
{
...selectedDay,
studyTime: totalSecond,
},
...studyLogWeeks.slice(dayIndex + 1),
];
1️⃣ 데이터 타입
데이터 타입에 대해서 먼저 알아보자. 크게 2종류로 분류할 수 있다.
- 기본형 타입
- 참조형 타입
기본형 타입에는 Number, String, Boolean, undefined, null, Symbol이 있다.
참조형 타입은 Object, Array, Function, Date, RegExp, Map, Set이 있다.
기본형과 참조형을 구분할 수 있는 방법은, 할당이나 연산 시 값을 복사하면 기본형이고, 값을 복사할 때 참조하면 참조형 타입이다.
var a = 10
2️⃣ 변수의 선언 과정(기본형과 참조형)
✏️ 2-1. 기본형의 변수 선언 과정
- 식별자를 a로 주고, number 타입인 10을 할당한다.
이 코드를 실행했을 때?
- 메모리의 한 공간을 확보한다.(1002)
- 식별자를 붙인다. (name : a)
- 바로 10을 할당하지 않고 데이터를 저장하기 위한 다른 메모리 공간을 하나 더 확보한다. (5001)
- 그곳의 주소를 식별자가 저장된 곳의 값으로 집어 넣는다.
- 그리고 10이라는 값을 집어 넣는다.
즉 변수는 값의 위치(주소)를 기억하는 메모리 공간인데, 값의 위치란 값이 위치하고 있는 메모리 상의 주소(address)를 의미한다. 즉, 변수란 값이 위치하고 있는 메모리 주소에 접근하기 위해 사람이 이해할 수 있는 언어로 지정한 식별자다.
2-1.1 기본형의 값을 가져오는 과정
console.log로 a에 접근했을 때
- 메모리 공간에서 식별자가 a인 메모리 공간을 찾고 (1002를 찾는다)
- 메모리 공간에 저장된 실제 값이들어 있는 공간의 주소로 값을 가져오는 것이다. (5001에서 값을 가져온다.)
✏️ 2-2. 참조형 타입의 변수 선언 과정
number 타입은 기본형 타입으로 주소가 한 번 연결되었다.
참조형은 주소가 2번 연결된다. 그 과정을 살펴 보자.
var obj = {
a: 10,
b: 'abc',
};
저기 보면 변수 영역과 데이터 영역이 있다.
변수 영역: 변수의 이름과 메모리 주소를 연결하는 공간
데이터 영역: 값의 주소를 저장하는 공간 (이 걸 잘 보자!!!!)
1. 할당
obj라는 식별자를 가진 변수를 선언하고 참조형 타입인 객체 하나를 할당했다. (할당) 그리고 기본형과 동일하게 메모리 공간(@1002)을 하나 확보하고 obj라는 식별자를 주었다. (메모리 공간 확보)
식별자: obj
값: value(10, abc) → 메모리 공간에 저장 (5001) 그리고 그 메모리 공간의 주소를 저장
여기서 값이 여러개 일 때 어떻게 저장하는지 잘 보자.
값을 저장하려고 다른 메모리 공간(@5001)을 하나 더 확보했더니 기본형으로 저장해야 하는 값이 여러 개가 들어있다.
(값을 저장하는 메모리 공간) → 참조형은 이 값을 저장하는 메모리 공간이 여러개 필요하다. (10, ‘abc’)
2. 메모리 공간 확보
이 프로퍼티들을 저장하기 위해 방금 새로 확보한 공간 외 프로퍼티 갯수에 맞게 또 여러 개의 공간(@7103, @7104)→ 갯수에 맞게 메모리 공간 확보했다. 을 확보하고 식별자를 지정한다. (a, b가 식별자)
그리고 그 식별자(a)의 실제 데이터가 저장할 공간을 다시 확보하고(@5003, @5004) 실제 값을 저장한다.
@7103, @7014에 방금 저장한 곳의 주소를 저장 후, @5001에 그룹의 주소를 저장한다.
마지막으로, 아까 확보해둔 메모리 공간(@5001)에 값이 저장된 (@7103, @7104)의 공간 주소를 저장한다.
3️⃣ 불변 값이란?
불변 값은, 변하지 않는 값이라는 뜻이다.
그리고 참조형(레퍼런스) 타입은 대체적으로 가변 값이다. 그 뜻은, 원시타입들은 대체로 불변 값이다.
불변 값 타입
- boolean
- null
- undefined
- Number
- String
- Symbol
불변 값이랑 변경 불가성은같은거? → Yes
즉, 객체가 생성된 이후, 그 상태를 변경할 수 없는 디자인 패턴을 의미한다.
그럼 기본형 타입과 참조형 타입의 재할당을 보면서, 불변 값을 이해해보자.
4️⃣ 재할당 ( 재할당으로 기본형이 불변 값임을 알아보자.)
이제 왜 기본형이 불변 값이고, 참조형이 가변 값인지, 재할당을 통해서 알아볼것이다.
4-1. 기본형 타입의 재할당
10을 삭제하고, 20을 할당하는 것으로 생각할 수 도 있다.
하지만 자바스크립트는 값을 없애지 않고, “값을 새로 생성 한 후, 주소를 수정한다.”
var a = 10;
a = 20;
console.log(a);//20
그림으로 보면 다음과 같다.
4-2. 참조형 타입의 재할당
var obj = {
a: 10,
b: 'abc',
}
var obj.a = 20;
obj의 프로퍼티의 값을 재 할당하면(예를 들어 obj.a = 20으로 재할당하는 것), obj이 가지고 있는 메모리 주소(즉 5001)는 변경되지 않고 obj.a가 가지고 있는 주소가 변경된다. (5003이 뭐 5005로 변하는거?)
무슨뜻이냐…
즉 5003의 주소 값이 5005로 변경된다 그리고 그 주소를 참조한다.
💡 즉 ‘새로운 객체’가 만들어진 것이 아니라 기존 객체 내부의 값만 바뀐 것이다.
정리
기본형에서는 재할당을 할 때 a의 값 자체가 새로 생성되었다. 이 과정에서 기존 값은 변경되지 않고, 새로운 메모리 공간이 할당된다. 따라서 기본형은 불변 값이다.
참조형에서는 재할당을 할 때 obj가 “새로 생성”된 것이 아니라 (새로운 객체가 생성되는 것이 아니라) 기존 객체의 속성만 변경된다. 이때 객체는 여전히 같은 메모리 주소를 가리키고 있기 때문에, 객체는 가변으로 취급된다. 따라서 obj는 가변 값이다.
📝 깊은 복사란?
여기서 말하는 복사란 원본을 베낌. 종이를 포개고 그 사이사이에 복사지를 받쳐 한 번에 여러장을 쓴다는 의미를 가지고 있다. 즉 동일한 내용을 그대로 다른 곳에서 사용하는 것을 말한다.
💡 그리고, 깊은 복사란, 기존 값의 모든 참조가 끊어지는 것을 말한다. 참조형 타입 값(객체)에서 내부에 있는 모든 값이 새로운 값이 되는 것을 말한다.
✏️ 기본형 타입의 깊은 복사
var a = 10;
var b = a;
console.log(a); // 10
console.log(b); // 10
1번의 과정을 이해하기 위해서 얕은 복사, 깊은 복사, 불변성에 대해 알아보았다. 참조형 타입의 얕은 복사
얕은 복사란 참조형 타입의 값의 바로 아래 단계의 값만 복사하는 방법이다.
var obj1 = {
a: 1,
b: {
c: 2,
},
};
var obj2 = { ...obj1 };
console.log(obj1 === obj2); // false
console.log(obj1.b === obj2.b); // true
여기서 5001까지만 복사하는 것이다.
두 개의 객체는 다른 주소(1002, 1003)를 가지고 있지만 객체 안 프로퍼티는 동일한 주소를 가지고 있다. 쉽게 말하면 {}이 껍데기는 새로 생성된 객체이며, 새로운 주소를 갖게 되었고 spread 연산자로 풀어진 프로퍼티들은 처음 선언된 obj1의 프로퍼티들이 사용되었다.
얕은 복사는 후술할 React, Vue.js에서 중요한 개념으로 다루어진다. 얕은 복사를 만드는 방법은 위에 사용한 spread 연산자를 통해 만들 수도 있고 다른 방법으로도 만들 수 있다. spread 연산자는 객체 뿐 아니라 배열에서도 동일하게 동작한다.
💡
얕은 복사는 한 단계까지만 복사하고, 깊은 복사는 객체에 중첩된 객체까지 모두 복사한다. 얕은 복사와 깊은 복사 모두 복사한 대상에 대해서 새로운 객체를 생성하여 기존 객체에는 영향을 주지않는다.
그래서 그래서.. 이제 결론 지을 시간
왜,,? 의도치 않게 값이 변경 되는것이냐.. 참조형이 가변 값이기 때문?
React에서는 상태 값이 변경 될 때 마다 데이터로 DOM을 다시 그리는 리렌더링 작업을 한다.
그리고 이 과정에서 React는 상태 변경을 어떻게 감지하느냐? -> 얕은 비교를 통해 한다.
얕은 비교란? -> 주소 값을 비교하는 것
📝 참조형에서 불변성을 유지하지 않는 예시
const state = { count : 1 };
state.count = 2;
불변성이 보장되어야 하는 이유는 상태 값이 직접 바뀌게 될 경우에 해당 상태의 주소 값은 변하지 않아서, state 변화를 인지하지 못하기 때문이다. 분명 state의 값은 바꼈는데 말이다.
📝 참조형에서 불변성을 유지하는 예시
const state = { count: 1 };
// 상태를 새롭게 생성
const newState = { ...state, count: 2 };
React는 상태 변화를 인지해야지 새로운 데이터를 업데이트 하는데 이를 인지하지 못할 경우에는 뭐 이전 값이 계속 보여진다.
따라서 Redux에서도 상태를 관리할 때 값을 변경할 경우, 객체나 배열을 생성한 후, 새로운 값을 넣어줘야지 React는 얕은 비교를 통해서 상태 변화를 인지 시키고, 리렌더링을 발생 시킬 수있는 것이다.
참고 자료
https://pozafly.github.io/javascript/shallo-copy-and-deep-copy/
'React' 카테고리의 다른 글
제어 컴포넌트 vs 비제어 컴포넌트와 react-hook-form (0) | 2025.01.01 |
---|---|
[React + Typescript] : 무한 스크롤 Intersection Observer API (0) | 2024.09.03 |
Error: React Hook useEffect has missing dependencies:...Either include them or remove the dependency array (0) | 2024.08.19 |
React: [Error] Firebase 사용자 photoURL (0) | 2024.01.19 |
React: Path Parameters (0) | 2023.12.27 |