JavaScript에서 데이터 관리는 프로그래밍의 핵심 중 하나입니다.
특히, 원시 값과 참조 값의 차이, 그리고 객체의 얕은 복사와 깊은 복사를 이해하는 것은 매우 중요합니다.
이 글에서는 이 개념들을 코드 예시를 통해 쉽고 명확하게 설명하겠습니다.
원시 값과 참조 값
원시 값(Primitive Values)
원시 값(원시 타입의 값)은 변경 불가능한(immutable) 데이터 유형으로,
변수에 값을 직접 저장합니다.
JavaScript 원시 값의 종류는 다음과 같습니다.
- Boolean
- Number
- String
- null
- undefined
- Symbol
- BigInt
참조 값(Reference Values)
참조 값(객체 타입의 값)은 객체, 배열, 함수 등 원시 값을 제외한 모든 데이터 유형으로, 변경 가능한(mutable)한 값입니다.
참조 값은 실제 데이터가 저장된 메모리 위치를 가리키는 포인터를 저장합니다.
이를 통해 여러 변수가 동일한 객체를 가리킬 수 있습니다.
복사와 참조
JavaScript에서 변수를 다른 변수에 할당할 때, 복사와 참조라는 두 가지 방식이 있습니다.
복사(Copy)
복사는 변수에 값을 그대로 복사하는 것을 의미합니다.
원시 값의 경우 복사가 이루어지며, 변수 간에 독립적인 값을 갖게 됩니다.
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
위 예시에서 a와 b는 각각 독립적인 값을 가지며, b를 변경해도 a에 영향을 미치지 않습니다.
참조(Reference)
참조는 변수에 객체의 실제 값이 아닌 객체의 메모리 주소(참조 값)가 저장되는 방식입니다.
이를 통해 여러 변수가 동일한 객체를 가리킬 수 있습니다.
let obj1 = { a: 1 };
let obj2 = obj1;
obj2.a = 2;
console.log(obj1.a); // 2
위 예시에서 obj1과 obj2는 동일한 객체를 참조하고 있기 때문에,
obj2를 통해 객체의 값을 변경하면 obj1에서도 그 변경 사항이 반영됩니다.
원시 값이 참조 관계를 가지지 않는 이유(원시 값이 복사만 가능한 이유)
- 성능과 메모리 효율성
원시 값은 비교적 작고 간단한 데이터 타입이기 때문에
직접 값을 저장하고 사용하는 것이 성능 면에서 더 효율적입니다.
참조를 통해 접근하게 되면 불필요한 메모리 접근과 포인터 연산이 추가되어 성능이 저하될 수 있습니다. - 변경 불가능성
원시 값은 변경 불가능합니다. 즉, 한 번 생성된 원시 값은 절대로 변하지 않습니다.
이 특성 덕분에 원시 값을 참조할 필요 없이 값을 그대로 전달하는 것이 더 간단하고 직관적입니다. - 데이터 일관성
원시 값이 참조 관계를 가진다면, 여러 변수가 동일한 값을 가리킬 때
그 값이 변경될 수 있는 가능성이 생깁니다. 이는 예기치 않은 부작용을 초래할 수 있으며,
코드의 가독성과 유지보수성을 떨어뜨립니다.
반면, 객체와 배열 같은 참조 값은 그 자체로 복잡한 구조를 가지고 있으며,
동일한 객체나 배열을 여러 변수에서 참조하는 것이 더 자연스럽습니다.
이는 실제로 동일한 데이터를 공유하면서도 데이터의 구조적인 변경이 필요한 경우에 유용합니다.
객체의 얕은 복사와 깊은 복사
객체는 원시 값과 달리 변경 가능합니다.
이 특성 때문에 객체를 복사할 때는 얕은 복사와 깊은 복사의 차이를 이해하는 것이 중요합니다.
얕은 복사(Shallow Copy)
얕은 복사는 객체에서 가장 상위 객체만 복사되며,
내부 객체의 참조 값은 원본 객체와 참조 관계를 유지하는 방식입니다.
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { ...obj1 };
obj2.b.c = 3;
console.log(obj1.b.c); // 3
위 예시에서 obj2는 obj1의 얕은 복사본이므로 b 객체는 동일한 참조를 공유합니다.
따라서 obj2.b.c를 변경하면 obj1.b.c에도 그 변경 사항이 반영됩니다.
얕은 복사 방법
- 스프레드 연산자: [... 배열], {... 객체}
- concat(), slice() 메서드 (인수 없이 사용): 배열.slice(), 배열.concat()
- Object.assign() 메서드: Object.assign({}, 원본객체)
깊은 복사(Deep Copy)
깊은 복사는 객체의 모든 수준에서 복사가 되며, 내부 객체에서도 참조 관계가 모두 끊어지면서 복사되는 방식입니다.
원본 객체와 복사본은 완전히 독립적인 객체가 됩니다.
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 2
위 예시에서 obj2는 obj1의 깊은 복사본이므로 b 객체도 독립적인 복사본입니다.
따라서 obj2.b.c를 변경해도 obj1.b.c에는 영향을 미치지 않습니다.
깊은 복사 방법
- JSON.parse(JSON.stringify()): JSON.parse(JSON.stringify(원본객체))
(단, 이 방법은 함수, undefined, 순환 참조가 포함된 객체에서는 사용하지 못함) - Lodash 라이브러리의 _.cloneDeep():
_.cloneDeep(originalObject)를 사용하여 originalObject의 깊은 복사본을 생성했습니다. 이 복사본을 변경해도 원본 객체는 영향을 받지 않습니다.
const _ = require('lodash');
let deepCopy = _.cloneDeep(originalObject);
- 재귀적으로 객체의 모든 속성을 복사하는 사용자 정의 함수:
deepCopy 함수는 재귀적으로 객체를 순회하며 모든 속성을 복사합니다. 배열인 경우에는 배열을, 객체인 경우에는 객체를 새로 만들어 속성 값을 복사합니다. 이 방식으로 원본 객체와 독립적인 복사본을 생성할 수 있습니다.
function deepCopy(obj) {
// 원시 값이면 반환
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 배열이면 배열로 복사
if (Array.isArray(obj)) {
let copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
return copy;
}
// 객체이면 객체로 복사
let copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
let originalObject = {
name: "Bob",
details: {
age: 30,
city: "Builderland"
}
};
// 깊은 복사
let deepCopyObject = deepCopy(originalObject);
// 복사본을 변경
deepCopyObject.details.age = 35;
console.log(originalObject.details.age); // 30
console.log(deepCopyObject.details.age); // 35
요약
- 원시 값: 변경 불가능하며 값을 직접 저장.
- 참조 값: 객체와 배열 같은 복합 데이터 유형을 나타내며, 메모리 주소를 저장.
- 얕은 복사: 객체의 최상위 수준의 속성만 복사하며, 중첩된 참조 값은 공유.
- 깊은 복사: 객체의 모든 수준에서 완전히 독립적인 복사본을 만듦.
원시 값과 참조 값의 차이, 그리고 얕은 복사와 깊은 복사를 이해하는 것은 JavaScript 데이터 관리를 효율적으로 하는 데 필수적입니다.
이를 통해 복사 방식에 따라 코드의 동작이 어떻게 달라지는지 명확히 이해함으로써,
예기치 않은 버그를 피하고 더 안정적인 코드를 작성할 수 있습니다.