객체와 원시 타입의 근본적인 차이 중 하나는 객체는 ‘참조에 의해(by reference)’ 저장되고 복사된다는 것입니다.
원시값(문자열, 숫자, 불린 값)은 ‘값 그대로’ 저장·할당되고 복사되는 반면에 말이죠.
예시:
let message = "Hello!";
let phrase = message;
예시를 실행하면 두 개의 독립된 변수에 각각 문자열 "Hello!"
가 저장됩니다.
그런데 객체의 동작 방식은 이와 다릅니다.
변수엔 객체가 그대로 저장되는 것이 아니라, 객체가 저장되어있는 '메모리 주소’인 객체에 대한 '참조 값’이 저장됩니다.
그림을 통해 변수 user에 객체를 할당할 때 무슨 일이 일어나는지 알아봅시다.
let user = {
name: "John"
};
객체는 메모리 내 어딘가에 저장되고, 변수 user
엔 객체를 '참조’할 수 있는 값이 저장됩니다.
따라서 객체가 할당된 변수를 복사할 땐 객체의 참조 값이 복사되고 객체는 복사되지 않습니다.
예시:
let user = { name: "John" };
let admin = user; // 참조값을 복사함
변수는 두 개이지만 각 변수엔 동일 객체에 대한 참조 값이 저장되죠.
따라서 객체에 접근하거나 객체를 조작할 땐 여러 변수를 사용할 수 있습니다.
let user = { name: 'John' };
let admin = user;
*admin.name = 'Pete'; // 'admin' 참조 값에 의해 변경됨*
alert(*user.name*); // 'Pete'가 출력됨. 'user' 참조 값을 이용해 변경사항을 확인함
객체를 서랍장에 비유하면 변수는 서랍장을 열 수 있는 열쇠라고 할 수 있습니다. 서랍장은 하나, 서랍장을 열 수 있는 열쇠는 두 개인데, 그중 하나(admin
)를 사용해 서랍장을 열어 정돈한 후, 또 다른 열쇠로 서랍장을 열면 정돈된 내용을 볼 수 있습니다.
문법과 동작 방식은 다음과 같습니다.
Object.assign(dest, [src1, src2, src3...])
dest
는 목표로 하는 객체입니다.src1, ..., srcN
는 복사하고자 하는 객체입니다. ...
은 필요에 따라 얼마든지 많은 객체를 인수로 사용할 수 있다는 것을 나타냅니다.src1, ..., srcN
의 프로퍼티를 dest
에 복사합니다. dest
를 제외한 인수(객체)의 프로퍼티 전부가 첫 번째 인수(객체)로 복사됩니다.dest
를 반환합니다.assign
메서드를 사용해 여러 객체를 하나로 병합하는 예시를 살펴봅시다.
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
*// permissions1과 permissions2의 프로퍼티를 user로 복사합니다.
Object.assign(user, permissions1, permissions2);*
// now user = { name: "John", canView: true, canEdit: true }
목표 객체(user
)에 동일한 이름을 가진 프로퍼티가 있는 경우엔 기존 값이 덮어씌워 집니다.
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // user = { name: "Pete" }
Object.assign
을 사용하면 반복문 없이도 간단하게 객체를 복사할 수 있습니다.
let user = {
name: "John",
age: 30
};
*let clone = Object.assign({}, user);*
예시를 실행하면 user
에 있는 모든 프로퍼티가 빈 배열에 복사되고 변수에 할당됩니다.
지금까진 user
의 모든 프로퍼티가 원시값인 경우만 가정했습니다. 그런데 프로퍼티는 다른 객체에 대한 참조 값일 수도 있습니다. 이 경우는 어떻게 해야 할까요?
아래와 같이 말이죠.
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
clone.sizes = user.sizes
로 프로퍼티를 복사하는 것만으론 객체를 복제할 수 없습니다. user.sizes
는 객체이기 때문에 참조 값이 복사되기 때문입니다. clone.sizes = user.sizes
로 프로퍼티를 복사하면 clone
과 user
는 같은 sizes를 공유하게 됩니다.
아래와 같이 말이죠.
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true, 같은 객체입니다.
// user와 clone는 sizes를 공유합니다.
user.sizes.width++; // 한 객체에서 프로퍼티를 변경합니다.
alert(clone.sizes.width); // 51, 다른 객체에서 변경 사항을 확인할 수 있습니다.
이 문제를 해결하려면 user[key]
의 각 값을 검사하면서, 그 값이 객체인 경우 객체의 구조도 복사해주는 반복문을 사용해야 합니다. 이런 방식을 '깊은 복사(deep cloning)'라고 합니다.
깊은 복사 시 사용되는 표준 알고리즘인 Structured cloning algorithm을 사용하면 위 사례를 비롯한 다양한 상황에서 객체를 복제할 수 있습니다.
자바스크립트 라이브러리 lodash의 메서드인 _.cloneDeep(obj)을 사용하면 이 알고리즘을 직접 구현하지 않고도 깊은 복사를 처리할 수 있으므로 참고하시기 바랍니다.
전개연산자를 이용한 중첩 객체 복사
const [person, setPerson] = useState({
name: "알린",
title: "개발자",
mentor: {
name: "우디",
title: "시니어개발자",
},
});
setPerson((person) => ({
...person,
mentor: { ...person.mentor, name: newName },
}));