05 불변 객체
앞장에서 보았듯이 참조형 객체는 기본적으로 가변적이다. 하지만 이는 참조형 데이터 자체가 아니라 참조형 데이터의 내부 프로퍼티를 변경할 때 성립한다. 그렇기 때문에 참조형 데이터 자체를 바꾸면 불변성을 보장할 수 있다. 이번 장에서는 참조형 데이터에서 불변성을 확보하는 방법에 대해 알아볼 것이다.
객체를 가변적으로 사용한다면 어떤 문제점이??
const user = {
name: 'Milk717',
gender: 'female'
};
const changeName = (user, newName) => {
const newUser = user;
newUser.name = newName;
return newUser;
};
const user2 = changeName(user,'Sumin');
if (user !== user2){
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Sumin Sumin
console.log(user === user2); // true
위 코드는 changeName
을 사용해서 유저의 이름을 변경한 후 로그를 띄워주는 프로그램이다. user
와 user2
를 비교해서 유저 정보가 변경된 시점에 로그를 출력해야 하는데 user
와 user2
는 동일한 주소값을 가지기 때문에 항상 같다고 표시된다. 이 문제를 해결하려면 아래와 같이 코드를 변경해서 서로 다른 객체를 바라보게 만들어야한다.
const user = {
name: 'Milk717',
gender: 'female'
};
const changeName = (user, newName) => {
return {
name: newName,
gender: user.gender,
};
};
const user2 = changeName(user,'Sumin');
if (user !== user2){
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Sumin Sumin
console.log(user === user2); // true
changeName
함수를 변경해서 새로운 객체를 반환하도록 만들어 Code-1의 문제를 수정했다. 하지만 이 경우 기존 객체의 프로퍼티를 하드코딩으로 입력했으므로 만약 대상 객체에 정보가 많아진다면 코드를 작성하기 힘들다. 따라서 아래와 같이 객체를 복사해주는 함수를 만드는 것이 좋다.
const copyObject = (target) => {
const result = {};
for (let prop in target){
result[prop] = target[prop];
}
return result;
}
이 코드를 사용한다면 객체의 프로퍼티가 늘어나더라도 함수를 통해 간단하게 복사할 수 있다. 하지만 여기에도 문제점이 있다. 바로 얕은 복사만 수행한다는 것이다.
얕은 복사와 깊은 복사
얕은 복사는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사는 내부의 모든 값을을 찾아서 전부 복사하는 방법이다.
얕은 복사 예시
아래 코드는 Code-3에서 작성한 copyObect
코드를 사용해서 얕은 복사를 사용하는 코드이다.
const user = {
name: 'Milk717',
urls: {
protfolio: 'http://github.com/milk717',
blog: 'http://blog.milk717.com',
}
};
const user2 = copyObject(user);
console.log(user.name === user2.name); //false
user.urls.protfolio = 'https://blog.naver.com/lindasoo';
console.log(user.urls.protfolio === user2.urls.portfolio); //true
얕은 복사를 통해 name 프로퍼티는 완전한 복사가 이루어졌지만 user객체 안에 중첩된 객체는 복사가 수행되지 않아서 user.urls
와 user2.urls
가 동일하다. 이런 문제를 해결하기 위해 깊은 복사가 필요하다
깊은 복사 예시
const user = {
name: 'Milk717',
urls: {
protfolio: 'http://github.com/milk717',
blog: 'http://blog.milk717.com',
}
};
const user2 = copyObject(user);
const user2 = copyObject(user.urls);
console.log(user.name === user2.name); //false
user.urls.protfolio = 'https://blog.naver.com/lindasoo';
console.log(user.urls.protfolio === user2.urls.portfolio); //false
중첩된 객체의 값까지 copyObject
를 사용해서 복사해주면 깊은 복사가 된다. 어떤 객체를 복사할 때 깊은 복사를 수행하려면 객체의 프로퍼티 중 기본형 데이터는 그냥 복사하고, 내부의 참조형 데이터들은 다시 복사를 해주어야 한다.
중첩되는 객체가 많을 때 수동으로 모두 복사하는 것은 힘들기 때문에 깊은 복사를 수행하는 코드를 다음과 같이 작성할 수 있다.
const copyObject = (target) => {
const result = {};
if (typeof target === 'object' && target !== null){
for (let prop in target){
result[prop] = copyObject(target[prop]);
}
}else{
result = target;
}
return result;
}
JSON을 활용해서 간단하게 깊은 복사를 수행하는 방법
const copyObjectViaJSON = () => {
return JSON.parse(JSON.stringify(target));
}
const obj = {
a: 1,
b: {
c: null,
d: [1,2],
func1: function () { console.log(3); },
func2: function () { console.log(4); }
}
}
const obj2 = copyObjectViaJSON(obj);
obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;
console.log(obj); //{ a:1, b: { c: null, d: [1, 3], func1: f() }, func2: f() }
console.log(obj2); //{ a:3, b: { c: 4, d: [1, 2] } }
객체를 JSON 문자열로 전환했다가 다시 JSON 객체로 바꿔서 간단하게 객체의 깊은 복사를 수행할 수 있다. 하지만 이 방법은 함수나 숨겨진 프로퍼티인 proto 나 getter/setter 같이 JSON으로 변경할 수 없는 프로퍼티들은 복사되지 않는다. 이 방법은 httpRequest로 받은 데이터를 저장한 객체를 복사할 때 유용하다.