다들 자바스크립트의 자료형에 대해서는 알고계시다고 생각하고 이 글을 씁니다
혹여나, 원시값과 참조값의 차이나 그 개념에 대해서 미흡하시거나, 더 알고 싶으시면 아래 링크를 참고해보세요!
[원시값]
https://ko.javascript.info/primitives-methods
[객체]
https://ko.javascript.info/object
React 개발을 하다 보면 종종 마주치는 개념들이 있습니다. 특히 '얕은 비교'와 '깊은 비교'라는 용어는 취업 준비 과정에서 한 번쯤 들어봤을 텐데요. React가 상태(state)와 속성(props)의 변화를 감지하는 과정에서 이 개념들이 매우 중요한 역할을 합니다.
왜 이해해야 할까?
처음에는 이런 생각이 들었습니다.
"이런 개념들을 몰라도 실무에서 코딩하는 데는 문제없지 않을까?"
하지만 곧 깨달았습니다.항상 느끼는거지만, 기본 개념에 대한 이해 없이는 진정한 성장에 한계가 있다는 것을요. React의 성능 최적화를 제대로 하기 위해서는 이 비교 방식들을 이해하는 것이 필수적이라고 생각해서 이 글을 쓰게 되었습니다.
이 주제를 두 편으로 나누어 설명하고자 합니다:
상편: 얕은 비교(Shallow Compare)와 깊은 비교(Deep Compare)의 기본 개념
하편: React에서 이러한 비교가 실제로 어떻게 적용되어 성능 최적화에 기여하는지
이번 글에서는 먼저 얕은 비교와 깊은 비교의 개념을 자세히 살펴보겠습니다. 이를 통해 React가 어떻게 효율적으로 상태 변화를 감지하고 리렌더링을 결정하는지 이해하고 살펴보도록 하겠습니다.
이제 비교에 대한 개념을 알아보도록 하죠!!
얕은 비교, 얕은 복사 이름은 많이 들어봤는데 도대체 무엇이 다른걸까요?
// 최상위 레벨만 새로운 참조 생성
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
original === shallowCopy; // false (새로운 최상위 참조)
original.b === shallowCopy.b; // true (중첩 객체는 같은 참조)
// 자주 사용되는 방법들
const copy1 = { ...object }; // 전개 연산자
const copy2 = Object.assign({}, object);
const copy3 = [...array]; // 배열의 경우
얕은 복사는 참조형 타입을 복사할 때 겉표면만 새로 만드는 복사입니다. 객체의 최상위 속성들만 새로운 메모리 공간에 복사하고, 내부의 중첩된 객체들은 원본의 참조를 유지합니다. 예를 들어 위 코드의 구조를 살펴보겠습니다.
최상위 속성 구조를 한 번 살펴보죠
a: 1
-> 최상위 속성 (값이 원시타입)b: { c: 2 }
-> 최상위 속성 (값이 객체)얕은 복사를 했을 때의 동작
const original = { a: 1, b: { c: 2 } };
const copied = { ...original };
// a와 b 모두 새로운 참조가 생성됨
copied.a === original.a // true (원시값은 그대로 복사)
copied.b === original.b // true (객체는 참조만 복사)
copied === original // false (새로운 객체 생성)
따라서 얕은복사는 쉽게말해서 마치 집의 주소만 새로만들고, 집 안의 모든 물건들은 원래 집의 물건을 그대로 가리키는 것이라고 생각하면 될 것 같습니다! 그렇다면 얕은 비교도 알아볼까요?
메모리 주소값을 비교하는 것입니다. 즉, 두 값이 정확히 같은 참조를 가리키고 있는지만 확인합니다.
위의 얕은 복사를 보고 얕은 비교를 보니까 좀 더 이해가 쉽지 않나요?
메모리의 주소를 비교해서 내용이 같더라도 주소가 다르면 false를 리턴하는거죠
// 최상위 레벨만 새로운 참조 생성
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
original === shallowCopy; // false (새로운 최상위 참조)
original.b === shallowCopy.b; // true (중첩 객체는 같은 참조)
// 자주 사용되는 방법들
const copy1 = { ...object }; // 전개 연산자
const copy2 = Object.assign({}, object); //Object.assign은 열거가능한 자체 속성을 복사해
//해당객체에 붙여넣는 메소드입니다.
const copy3 = [...array]; // 배열의 경우
비슷한 비유를 하자면 위의 얕은 복사와 유사하게 마치 두 사람이 같은 집을 가리키고 있는지만 보는 것과 같습니다. 집 안에 무엇이 있는지는 보지 않고, 단순히 같은 집을 가리키는지만 확인하는 것입니다.
얕은 비교와 복사를 알아봤으니 깊은 것도 알아봐야겠죠?
깊은 복사는 모든 중첩 레벨의 객체가 새로운 참조로 복사되고, 원본 객체의 변경이 복사본에 영향을 주지 않습니다. 즉 모든 중첩된 객체를 복사하여 새로운 메모리 주소에 할당하는거죠! 그래서 원본객체와 복사된 객체는 완전히 독립적인 메모리 공간을 사용하는거에요! JSON.parse,JSON.stringify 등을 써서 할 수 있고, 재귀함수 등을 사용해서 할 수 있죠 예를 봅시다.
// 중첩된 객체 예시
const originalPerson = {
name: "Kim",
info: {
age: 30,
address: {
city: "Seoul",
street: "Gangnam"
}
},
hobbies: ["reading", {type: "sports", detail: "swimming"}]
};
// 얕은 복사와 깊은 복사의 차이
const shallowCopy = { ...originalPerson };
const deepCopy = JSON.parse(JSON.stringify(originalPerson));
// 중첩된 객체 수정 테스트
originalPerson.info.address.city = "Busan";
originalPerson.hobbies[1].detail = "running";
console.log(shallowCopy.info.address.city); // "Busan" (참조가 같아서 영향 받음)
console.log(deepCopy.info.address.city); // "Seoul" (완전히 새로운 객체라 영향 없음)
console.log(shallowCopy.hobbies[1].detail); // "running" (참조가 같아서 영향 받음)
console.log(deepCopy.hobbies[1].detail); // "swimming" (완전히 새로운 객체라 영향 없음)
위처럼 집 비유를하면 집을 완전히 새로 짓는데, 지으려는 집을 다른 집을 모티브 삼아서 벽돌부터 티비까지 모두 똑같이 만드는거죠! 그러면 모티브 A집의 티비를 바꿔도 B집의 티비는 그대로겠죠? 이렇게 생각하니까 이해가 쉽네요 하하!! 그렇다면 깊은 비교도 한 번 볼까요?
그러면 깊은 비교도 깊은 복사와 비슷합니다! 객체의 모든 단계를 재귀적으로 비교해서 값이 일치하는지 확인하는 방법인데요, 얕은 비교의 코드 구현과, 깊은 비교의 코드를 비교해보면서 어떤 성능상의 차이가 있을지 알아보죠
얕은 비교 코드
function shallowCompare(obj1, obj2) {
// 참조가 같으면(같은 메모리 주소를 가르키면
if (obj1 === obj2) return true;) true
// null 또는 undefined 체크
if (obj1 == null || obj2 == null) return false;
// 타입이 다르면 false
if (typeof obj1 !== typeof obj2) return false;
// 객체가 아닌 경우 값 비교 (원시타입일 경우 처리)
if (typeof obj1 !== 'object') return obj1 === obj2;
// 배열인 경우
//두 값의 길이가 다르고, 각 요소를 얕은 비교합니다..
//혹시나 Array.every 모르시는 분을 위해.. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/every
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) return false;
return obj1.every((item, index) => obj1[item] === obj2[index]);
}
// 객체의 키 개수가 다르면 false
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
// 최상위 레벨의 값만 비교
return keys1.every(key => obj1[key] === obj2[key]);
}
function deepCompare(obj1, obj2) {
// 참조가 같으면 true
if (obj1 === obj2) return true;
// null 또는 undefined 체크
if (obj1 == null || obj2 == null) return false;
// 타입이 다르면 false
if (typeof obj1 !== typeof obj2) return false;
// 객체가 아닌 경우 값 비교
if (typeof obj1 !== 'object') return obj1 === obj2;
// 특수한 객체 타입 처리
// Date 객체 비교 (timestamp 값을 비교하는 이유는 같은 데이트 객체라도 각각의 데이트 객체가 실행되는 시간이 다르기 때문에
// 값이 달라질 수 있기 때문입니다. 또한 깊은 비교는 실제 값이 같은지를 체크하는 것이
// 목표이기 떄문에, 빠른 성능과 참조만 비교하는 얕은 비교에서는 Date 값을 처리하지 않습니다.
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime();
}
// RegExp 객체 비교
//마찬가지 입니다.
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString();
}
// 배열인 경우
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) return false;
return obj1.every((item, index) => deepCompare(item, obj2[index]));
}
// 객체의 키 개수가 다르면 false
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
// 모든 프로퍼티를 전부 비교할 때까지 재귀적으로 비교합니다.
return keys1.every(key => deepCompare(obj1[key], obj2[key]));
}
잠깐! 순환참조가 뭐죠?
객체가 직접 또는 간접적으로 자신을 참고하는 구조입니다.
무한루프의 가능성이 있고, JSON.stringfy로 변환할 수 없습니다.
// 1. 순환 참조의 간단한 예시
const obj1 = { name: "Object 1" };
const obj2 = { name: "Object 2" };
// 순환 참조 생성
obj1.ref = obj2;
obj2.ref = obj1;
console.log(obj1.ref.ref.ref.name); // "Object 2" (무한히 참조 가능)
// 테스트 케이스
const testCases = () => {
// 테스트 객체들
const obj1 = {
name: "Kim",
age: 30,
info: {
city: "Seoul",
hobbies: ["reading", "gaming"],
details: {
phone: "123-456-789"
}
},
date: new Date('2024-02-07'),
regex: /test/
};
const obj2 = {
name: "Kim",
age: 30,
info: {
city: "Seoul",
hobbies: ["reading", "gaming"],
details: {
phone: "123-456-789"
}
},
date: new Date('2024-02-07'),
regex: /test/
};
const obj3 = {
name: "Kim",
age: 30,
info: obj1.info // 같은 객체 참조
};
console.log("---- 얕은 비교 테스트 ----");
console.log("완전히 다른 객체 (얕은 비교):", shallowCompare(obj1, obj2)); // false
console.log("참조가 같은 객체 (얕은 비교):", shallowCompare(obj1, obj3)); // true
console.log("\n---- 깊은 비교 테스트 ----");
console.log("구조가 같은 객체 (깊은 비교):", deepCompare(obj1, obj2)); // true
console.log("다른 구조의 객체 (깊은 비교):", deepCompare(obj1, obj3)); // false
// 배열 테스트
const arr1 = [1, [2, 3], { a: 4 }];
const arr2 = [1, [2, 3], { a: 4 }];
const arr3 = [1, [2, 3], arr1[2]]; // 같은 객체 참조
console.log("\n---- 배열 테스트 ----");
console.log("완전히 다른 배열 (얕은 비교):", shallowCompare(arr1, arr2)); // false
console.log("참조가 같은 배열 (얕은 비교):", shallowCompare(arr1, arr3)); // true
console.log("구조가 같은 배열 (깊은 비교):", deepCompare(arr1, arr2)); // true
};
얕은 비교 (shallowCompare)
깊은 비교 (deepCompare)
쓰다보니 많이 길어졌네요, 이번 글에서는 얕은 복사, 얕은 비교, 깊은 복사, 깊은 비교를 알아봤습니다! 사실 이걸 안다고해서… 그래서 위에서 말했던 리액트에서는 참조가 어쩌구 했는데.. 그거는?! 다음 게시물에서 어떻게 적용되었는지 알아보도록 하겠습니다!
감사합니다!
[원시값]
https://ko.javascript.info/primitives-methods
[객체]