본문 바로가기
책 정리

코어 자바스크립트 1장 데이터 타입(05 불변객체)

by 싼쵸 2022. 6. 13.
반응형
이 글은 전적으로 코어 자바스크립트를 읽고 나름대로 요약한 글이다.

출처 https://shylog.com/objects-and-immutability/

 

05 불변 객체

불변 객체를 만드는 간단한 방법

불변 객체(immutable object)는 최근의 React, Vue.js 등의 라이브러리나 프레임워크에서 뿐만 아니라 함수형 프로그래밍, 디자인 패턴 등에서도 매우 중요한 기초가 되는 개념입니다. 참조형 데이터의 '가변'은 데이터 자체가 아니 내부의 프로퍼티를 변경할 때만 성립합니다.

데이터 자체를 변경하고자 하면(새로운 데이터를 할당하고자 하면) 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않습니다.

그렇다면 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용한다면 객체 역시 불변성을 확보할 수 있을 것입니다. 

 

그럼 어떤 상황에서 불변 객체가 필요할까요?  값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우가 종종 발생합니다. 바로 이럴 때 불변 객체가 필요합니다. 

 

var user = {
		name : 'Jaenam',
        gender : 'male'
 }
 
 var changeName = function(user, newName){
	var newUser = user;
    newUser.name = newName;
    return newUser;
}

var user2 = changeName(user, 'Jung');

if(user !== user2){
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // Jung Jung
console.log(user === user2) ; //true;

출력 결과

객체의 가변성으로 인한 문제점을 보여주는 간단한 예시입니다. 1번째 줄에서는 user객체를 생성하고, 12번째 줄에서 user객체의 name 프로퍼티를 'Jung'으로 바꾸는 함수 changeName을 호출해서 그 결과를 user2 변수에 담았습니다.

 

user 변수와 user2 변수가 서로 같지 않다는 조건이 성립하면 15번째 줄의 내용이 출력되겠지만 실제로는 출력이 없이 통과합니다.

두 변수의 name 프로퍼티 모두 'Jung'으로 출력되고, 마지막 줄에서 결과가 두 변수가 서로 동일하다고 합니다.

 

만약 정보가 바뀐 시점에 알림을 보내야 한다거나, 바뀌기 전의 정보와 바뀐 후 정보의 차이를 가시적으로 보여줘야 하는 등의 기능을 구현하려면 이대는 안됩니다.

 

객체의 가변성에 따른 문제점의 해결 방법

var user = {
		name : 'Jaenam',
        gender : 'male'
 }
 
 var changeName = function(user, newName){
	return{
      name : newName,
      gender : user.gender
    }
}

var user2 = changeName(user, 'Jung');

if(user !== user2){
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // Jamnam Jung
console.log(user === user2) ; //false;

출력 결과

changeName 함수가 새로운 객체를 반환하도록 수정했습니다. 이제 user와 user2는 서로 다른 객체이므로 안전하게 변경 전과 후를 비교할 수 있습니다. 다만 아직 미흡한 점이 보입니다. changeName 함수는 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티를 하드 코딩을 입력했습니다. 이런 식으로는 대상 객체에 정보가 많을수록, 변경해야 할 정보가 많을수록 사용자가 입력하는 수고가 늘어납니다. 이런 방식보다는 대상 객체의 프로퍼티 개수에 상관없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을 것입니다.

 

기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)

var copyObject = function(target){
  var result = [];
  for(var prop in target){
    result[prop] = target[prop];
  }
  return result;
}

copyObject는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수입니다.

copyObject를 이용한 객체 복사

var copyObject = function(target){
  var result = [];
  for(var prop in target){
    result[prop] = target[prop];
  }
  return result;
}


var user = {
		name : 'Jaenam',
        gender : 'male'
 }
 

var user2 = copyObject(user);
user2.name = 'Jung';

if(user !== user2){
  console.log('유저정보가 변경되었습니다.')
}


console.log(user.name, user2.name);
console.log(user === user2)

출력 결과

copyObject 함수를 통해 간단하게 객체를 복사하고 내용을 수정하는 데 성공했습니다.

우리가 만든 copyObject 함수는 간단한 만큼 분명 아쉬운 점이 많습니다. 무엇보다도 '얕은 복사만을 수행한다'는 부분이 가장 아쉬운데,  밑에 내용으로 보완하고 마무리하겠습니다.

 

얕은 복사와 깊은 복사

얕은 복사(shallow copy)는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사(deep copy)는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법입니다. 위의 예제에서 copyObject함수는 얕은 복사만 수행했습니다. 이 말은 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주 솟값만 복사한다는 의미입니다. 그러면 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 됩니다. 사변을 바꾸면 원본도 바뀌고, 원본을 바꾸면 사본도 바뀝니다.

 

그런데 이런 현상은 기본형 데이터와 참조형 데이터를 복사할 때의 차이점, 4장에서 '변수 복사 비교'에서 다뤘던 내용입니다.

 

중첩된 객체에 대한 얕은 복사

var copyObject = function(target){
  var result = [];
  for(var prop in target){
    result[prop] = target[prop];
  }
  return result;
}


var user = {
		name : 'Jaenam',
       urls:{
         portfolio: 'http://github.com/abc',
         blog: 'http://blog.com',
         facebook : 'http://facebook.com/abc'
       }
 }
 

var user2 = copyObject(user);
user2.name = 'Jung';
console.log(user.name === user2.name); //false

user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio) //true

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); //true

사본인 user2의 name 프로퍼티를 바꿔도 user의 name프로퍼티는 바뀌지 않았습니다. 반면  urls객체의 경우 원본과 사본 중 어느 쪽을 바꾸더라도 함께 바뀐 것을 확인할 수 있습니다. 즉 user객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한 단계 더 들어간 urls 내부 프로퍼티들은 기존 데이터를 그대로 참조하는 것이죠. 이런 현상이 발생하지 않게 하려면 user.urls 프로퍼티에 대해서도 불변 객체를 만들 필요가 있습니다.

 

중첩된 객체에 대한 깊은 복사

var copyObject = function(target){
  var result = [];
  for(var prop in target){
    result[prop] = target[prop];
  }
  return result;
}


var user = {
		name : 'Jaenam',
       urls:{
         portfolio: 'http://github.com/abc',
         blog: 'http://blog.com',
         facebook : 'http://facebook.com/abc'
       }
 }
 

var user2 = copyObject(user);
user2.urls = copyObject(user.urls);


user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); //false

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); //false

출력 결과

urls 프로퍼티에 copyObject 함수를 실행한 결과를 할당했습니다. 이제 urls 프로퍼티의 내부까지 복사해서 새로운 데이터가 만들어졌으므로 값이 서로 다르다는 결과를 얻었습니다.

그러니까 어떤 객체를 복사할 때 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할 때, 객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대로 복사하면 되지만, 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 합니다. 

참조형 데이터가 있을 때마다 재귀적으로 수행해야만 비로소 깊은 복사가 돼 것이죠

 

객체의 깊은 복사를 수행하는 범용 함수

var copyObjectDeep = function(target){
  var result = [];
 if(typeof target==='object' && target !== null){
   result[prop] = copyObject(target[prop])
 }else{
   result = tartget;
 }
  return result;
}

target !== null 조건을 덧붙힌 이유는 typeof 명령어가 null에 대해서도 'object'를 반환하기 때문에 추가했다.

 

target이 객체인 경우 내부 프로퍼티들을 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고, 객체가 아닌 경우에는 target를 그대로 지정했습니다. 이 함수는 원본과 사본이 서로 완전히 다른 객체를 참조하게 되어 어느 쪽의 프로퍼티를 변경하더라도 영향을 주지 않습니다.

출력 결과

간단하게 깊은 복사를 처리할 수 있는 다른 방법 하나를 더 소개하겠습니다.

객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON객체로 바꾸는 겁니다.  httpRequest로 받은 데이터를 저장한 객체를 복사할 때 등 순수 한 정보만 다룰 때 활용하기 좋은 방법입니다.

 

JSON을 활용한 간단한 깊은 복사

var copyObjectViaJSON = function(target){
  return JSON.parse(JSON.stringify(target));
}

var obj ={
  a : 1,
  b : {
    c : null,
    d : [1,2],
    func1 : function(){console.log(3);}
  },
  func2: function(){console.log(4);}
  
};

var obj2 = copyObjectViaJSON(obj);

obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;

console.log('obj',obj);
console.log('obj2',obj2);

출력 결과

이번 5장은 복사에 관한 이야기였다. 앞에 부분들은 정리하면서 이해가 조금 됐지만, 뒤로 갈수록 어려웠다
객체에 대한 복사 변화가 생각보다 복잡하고 신기해서 재밌게 정리는 했으나 누군가에 설명을 한다면 조금 더 학습이 필요할 것 같다.

반응형

댓글