Javascript : 얕은 복사와 깊은 복사 그리고 불변 객체

HaByungNo·2022년 7월 21일
0

객체는 참조(reference)형 이다. 그래서 객체를 참조하고 있는 다른 곳에서 객체를 변경해 버릴 경우 그 참조를 공유하는 모든 장소가 영향을 받는다. 만약 그 동작이 개발자의 의도가 아니라면 개발자는 반드시 참조를 가지고 있는 다른 장소에 변경 사실을 알리고 대처하는 대응이 해야한다.

예제 1)

var statement = 'I am an immutable value'; // string은 immutable value
var otherStr = statement.slice(8, 17);

console.log(otherStr);   // 'immutable'
console.log(statement);  // 'I am an immutable value'

Stirng 객체의 slice() 메소드는 statement 변수에 저장된 문자열을 변경하는 것이 아니다.
새로운 문자열을 생성하여 반환하고 있다.
그 이유는 문자열은 변경할 수 없는 immutable value이기 때문이다.

예제 2)

var arr = [];
console.log(arr.length); // 0

var v = arr.push(2);    // arr.push()는 메소드 실행 후 arr의 length를 반환
console.log(arr.length); // 1

위 예제에서 변수 v의 값은 무엇일까? 예제1과 같이 동작한다면 v는 길이가 1이고 그 값은 2인 새로운 배열이 될 것이다. 그러나 배열은 객체이고 객체는 변경가능한 참조형이므로 push 메소드에 의해 수정되고 v에는 배열의 새로운 length 값이 반환된다.

처리 후 결과의 복사본을 리턴하는 문자열의 메소드 slice()와는 달리 배열(객체)의 메소드 push()는 직접 대상 배열을 변경한다. 그 이유는 배열은 객체이고 객체는 immutable value가 아닌 변경 가능한 값이기 때문이다.


얕은 복사와 깊은 복사

얕은 복사(shallow copy)

얕은 복사는 참조(주소)값의 복사를 말한다.

const obj = { vaule: 1 }
const newObj = obj;

newObj.vaule = 2; //newObj 객체의 value값을 변경

console.log(obj.vaule); // 2
console.log(obj === newObj); // true

obj 객체를 새로운 newObj 객체에 할당하였다. 이는 참조 할당이라 한다. 이 후 newObj 객체의 value값을 변경했더니 기존의 obj.vaule 도 복사되었다.
obj === newObj 비교하면 true 가 나온다.

자바스크립트의 참조형은 얕은 복사가 된다. 다시 말해 데이터가 그대로 복사 생성되는 것이 아니라 해당 데이터의 참조값(메모리 주소)를 전달하여 결국 한 데이터를 공유하는 것


깊은 복사(deep copy)

let a = 1;
let b = a;

b = 2;

console.log(a); // 1
console.log(b); // 2
console.log(a === b); // false

변수 a를 새로운 b에 할당하였고 b 값을 변경하여도 기존의 a의 값은 변경되지 않는다. 두 값을 비교하면 false가 출력되며 서로의 값은 단독으로 존재하다는 것을 알 수 있다.

자바스크립트의 원시 타입은 깊은 복사가 되며, 이는 독립적인 메모리에 값 자체를 할당하여 생성하는 것이라 볼 수 있다.

객체의 깊은 복사?

객체를 그대로 얕은 복사하여 사용할 경우 기존 객체의 원본 데이터가 훼손될 수 있다. 이를 방지하기 위해 객체도 깊은 복사를 해야할 때가 필요하다.


불변 객체

의도치 않은 객체의 변경이 발생하는 대부분의 원인은 “레퍼런스를 참조한 다른 객체에서 객체를 변경”했기 때문이다. 해결방법에는 비용이 조금 들어도 객체를 불변객체로 만들어 프로퍼티의 변경을 방지하며 객체의 변경이 필요한 경우에는 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경한다. 또는 Observer 패턴으로 객체의 변경에 대처할 수도 있다.

불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고 성능 개선에도 도움이 된다. 하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우 오히려 부적절한 경우가 있다.

객체의 방어적 복사(defensive copy)

Object.assign

Object.assign은 타킷 객체로 소스 객체의 프로퍼티를 복사한다. 이때 소스 객체의 프로퍼티와 동일한 프로퍼티를 가진 타켓 객체의 프로퍼티들은 소스 객체의 프로퍼티로 덮어쓰기된다. 리턴값으로 타킷 객체를 반환한다. ES6에서 추가된 메소드이며 Internet Explorer는 지원하지 않는다.

Object.assign(생성할 객체, 복사할 객체) 
// 메서드의 첫번째 인수로 빈 객체를 넣어주며, 
// 두번째 인수로 할당할 객체를 넣으면 된다.

// Copy
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
console.log(obj == copy); // false

// Merge 방법1
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const merge1 = Object.assign(o1, o2, o3);

console.log(merge1); // { a: 1, b: 2, c: 3 }
console.log(o1);     // { a: 1, b: 2, c: 3 }, 타겟 객체가 변경된다!

// Merge 방법2
const o4 = { a: 1 };
const o5 = { b: 2 };
const o6 = { c: 3 };

const merge2 = Object.assign({}, o4, o5, o6);

console.log(merge2); // { a: 1, b: 2, c: 3 }
console.log(o4);     // { a: 1 }

Object.assign을 사용하여 기존 객체를 변경하지 않고 객체를 복사하여 사용할 수 있다. Object.assign은 완전한 deep copy를 지원하지 않는다. 객체 내부의 객체(Nested Object)는 얕은 복사(shallow copy)가 된다.


Object.freeze

Object.freeze()를 사용하여 불변(immutable) 객체로 만들수 있다.

// Object.assign은 완전한 deep copy를 지원하지 않는다.
const obj = { 
  a: 1, 
  b: { c: 2, 
       d: 3
     }
};
const copy = Object.assign({}, obj, {a: 5});
console.log(copy); // { a: 1, b: {c: 2, d: 3}}
console.log(obj == copy); // false

console.log(obj.a); // 1
console.log(copy.a); // 5


Object.freeze(obj);

obj.a = 2; // 무시된다
console.log(obj); // { a: 1, b: { c: 2, d: 3 } }
console.log(Object.isFrozen(obj)); // true

하지만 객체 내부의 객체(Nested Object)는 변경 가능

const obj = { 
  a: 1, 
  b: { c: 2, 
       d: 3
     }
};

Object.freeze(obj);

obj.b.c = 'hey'; // 변경된다!

console.log(obj); // { a: 1, b: { c: 'hey', d: 3 } }

내부 객체까지 변경 불가능하게 만들려면 Deep freeze를 하여야 한다.

function deepFreeze(obj) {
  const props = Object.getOwnPropertyNames(obj);

  props.forEach((name) => {
    const prop = obj[name];
    if(typeof prop === 'object' && prop !== null) {
      deepFreeze(prop);
    }
  });
  return Object.freeze(obj);
}


const obj = { 
  a: 1, 
  b: { c: 2, 
       d: 3
     }
};

deepFreeze(obj);

obj.a = 2; 			// 무시된다
obj.b.c = 'hey'; 	// 무시된다

console.log(obj); // { a: 1, b: { c: 2, d: 3 } }

Immutable.js

Object.assign과 Object.freeze을 사용하여 불변 객체를 만드는 방법은 번거러울 뿐더러 성능상 이슈가 있어서 큰 객체에는 사용하지 않는 것이 좋다.

또 다른 대안으로 Facebook이 제공하는 Immutable.js를 사용하는 방법이 있다.

Immutable.js는 List, Stack, Map, OrderedMap, Set, OrderedSet, Record와 같은 영구 불변 (Permit Immutable) 데이터 구조를 제공한다.

npm을 사용하여 Immutable.js를 설치한다.

$ npm install immutable

Immutable.js의 Map 모듈을 임포트하여 사용한다.

const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50

map1.set(‘b’, 50)의 실행에도 불구하고 map1은 불변하였다. map1.set()은 결과를 반영한 새로운 객체를 반환한다.


레퍼런스:
https://poiemaweb.com/js-immutability

profile
프라고

0개의 댓글