본문 바로가기

javascript

객체와 변경불가성(Immutability)

mutate 에서 im을 붙인다면?

Mutate - 변화, Mutable - 변화 가능한(형용사), mutability - 변화 가능함(명사)
다시 말해서 mutability는 정보의 원본이 변경될 수 있다는 것입니다.

부정의 의미인 im을 붙이면 immutability, 즉 변화 가능하지 않음 이라는 뜻이 됩니다. 정보의 원본을 훼손되는 것을 막는 것이 immutability 입니다.

Immutability 배경

자바스크립트에서 객체는 참조(reference) 형태로 전달하고 전달 받으며, 객체가 참조를 통해 공유되고 있다면 그 상태가 언제든지 변경될 수 있기 때문에 문제가 될 가능성도 커집니다.
객체의 참조를 가지고 있는 어떤 장소에서 객체를 변경하면 참조를 공유하는 모든 장소에 그 영향을 받기 때문에 이것이 의도한것이 아니라면 추가적인 대응이 필요합니다.

이런 경우 참조가 아닌 객체 전체를 방어적 복사(defensive copy) 하는 간단한 방법으로 대응할 수 있으며 또는, Observer 패턴을 가변 객체의 변경에 대처하는 방법으로 사용할 수 있습니다.

참고 - https://ko.wikipedia.org/wiki/불변객체

immutable vs mutable

Primitive

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (New in ECMAScript 6)

Object

  • Object
  • Array
  • Function

Javascript에서 원시 타입(primitive data type)은 변경 불가능한 값(immutable value)이고 객체 타입은 변경 가능한 값(mutable value)입니다.

변경이 불가능하다는 뜻은 메모리 영역에서의 변경이 불가능하다는 뜻이며 재할당은 가능합니다.

var mad = '매드 몬스터'; // (1)
mad = '내루돌프'; // (2)

(1) 실행 되면 메모리에 문자열 '매드 몬스터'가 생성 되고, 변수 mad는 메모리에 생성된 문자열 '매드 몬스터'의 메모리를 가르키게 됩니다.

(2) 실행되면 이전에 생성된 문자열 '매드 몬스터'를 수정하는 것이 아니라 새로운 문자열 '내루돌프'를 메모리에 생성하고 mad은 이것을 가리킵니다. 이때 '매드 몬스터' 와 '내루돌프'는 메모리에 존재합니다. mad는 '매드 몬스터'를 가르키고 있다가 '내루돌프'를 가르키도록 변경 된거 뿐

var rudolf = '이 어둠을 빨간 코로 비춰줄래'; // string은 immutable value
var red = rudolf.slice(12, 16);

console.log(rudolf); // '이 어둠을 빨간 코로 비춰줄래'
console.log(red); // '비춰줄래'

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

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

var v8 = a1.push(2);
console.log(a1.length); // 1

배열이 동작한다면 v8은 새로운 배열을 가지게 된다. 그러나 객체인 a1은 push 메소드를 통해 업데이트되고 v8은 배열의 새로운 length 값이 반환된다.

문자열 메소드 slice() 와 다르게 배열(객체) 메소드 push()는 직접 대상 배열을 변경한다. 배열은 객체이고 객체는 immutable value가 아니기 때문이다.

var user = {
  name: 'seungjin',
};

var id = user.name;

user.name = 'choonsik';
console.log(id); // seungjin

id = user.name;
console.log(id); // choonsik

user.name 의 값을 변경했지만 변수 id 의 값은 변경되지 않았습니다. 이는 변수 id 에 user.name 을 할당했을 때 참조를 할당하는 것이 아니라 immutable한 값 seungjin 을 메모리에 새로 생성되고 id 는 이것을 참조하기 때문입니다.

user.name 의 값을 변경 하더라도 변수 id 는 참조하는 seungjin은 변함이 없습니다.

var user1 = {
  name: 'seungjin',
};

var user2 = user1;

user2.name = 'choonsik';

console.log(user1.name); // choonsik
console.log(user2.name); // choonsik
// 바꾸지 않은 user1.name 값도 변경된걸 볼 수 있습니다.

객체 user2의 name 프로퍼티에 새로운 값을 할당하면 객체는 변경 불가능한 값이 아니기 때문에 객체 user2는 값이 변경됩니다. 하지만 변경하지 않은 객체 user1도 같이 바뀌게 됩니다. user1 과 user2 는 같은 어드레스를 참조하고 있기 때문입니다.

Object.assign

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

출처 -https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

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

// Merge - 타겟 객체를 넣지 않고 병합
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 - 객체 병합
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 }

// Merge - 같은 프로퍼티를 가진 객체 병합
const o7 = { a: 1, b: 1, c: 1 };
const o8 = { b: 2, c: 2 };
const o9 = { c: 3 };

const obj = Object.assign({}, o7, o8, o9);
console.log(obj); // { a: 1, b: 2, c: 3 }

Object.assign을 사용하면 기존 객체를 변경하지 않고 객체를 복사하여 사용할 수 있습니다.

const user1 = {
  name: 'seungjin',
  address: {
    city: 'geumcheon',
  },
};

const user2 = Object.assign({}, user1); // 새로운 빈 객체에 user1을 copy

console.log(user1 === user2); // false
//user1과 user2는 참조값이 디릅니다.

user2.name = 'choonsik';
console.log(user1.name); // seungjin
console.log(user2.name); // choonsik

console.log(user1.address === user2.address); // true
// 객체 내부의 객체(Nested Object)는 Shallow copy

user1.address.city = 'Yangjae';
console.log(user1.address.city); // Yangjae
console.log(user2.address.city); // Yangjae

let obj1 = { a: 0, b: { c: 0 } };
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}

user1 객체를 빈객체에 복사하여 새로운 객체 user2를 생성한다. user1과 user2는 어드레스를 공유하지 않으므로 한 객체를 변경해도 아무런 영향을 주지 않습니다.

user1 객체는 const로 선언되어 재할당은 할 수 없지만 객체의 프로퍼티는 보호되지 않는다 객체의 내용은 변경할 수 있습니다.

Object.assign은 완전한 deep copy를 지원하지 않고 shallow copy이기 때문에 만약 완전한 deep copy를 원한다면 lodash의 deepClone()을 사용하기를 추천합니다.

let copy = JSON.parse(JSON.stringify(original))
JSON.parse, JSON.stringify 를 사용하여 간단하게 deep copy가 가능하다고 명세에 나와 있지만 object내에 function이 value로 있을 경우 적합하지 않습니다.

Object.freeze

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

const user1 = {
  name: 'seungjin',
};

// Object.assign은 완전한 deep copy를 지원하지 않습니다.
const user2 = Object.assign({}, user1, { name: 'choonsik' });

console.log(user1.name); // seungjin
console.log(user2.name); // choonsik

Object.freeze(user1);

user1.name = 'choonsik'; // 무시

console.log(user1); // { name: 'seungjin' }

console.log(Object.isFrozen(user1)); // true

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

const user = {
  name: 'seungjin',
  address: {
    city: 'geumcheon',
  },
};

Object.freeze(user);

user.address.city = 'Yangjae';
console.log(user); // { name: 'seungjin', address: { city: 'Yangjae' } }

내부 객체까지 변경 불가능하게 만드려면 Deep freeze 를 해야합니다.

const deepFreeze = (object) => {
  // 객체에 정의된 속성명을 추출 getOwnPropertyNames 사용하여 배열로 반환
  var propNames = Object.getOwnPropertyNames(object);

  // 스스로를 동결하기 전에 속성을 동결

  for (let name of propNames) {
    let value = object[name];

    object[name] = value && typeof value === 'object' ? deepFreeze(value) : value;
  }

  return Object.freeze(object);
};

const user = {
  name: 'seungjin',
  address: {
    city: 'geumcheon',
  },
};

deepFreeze(user);

user.name = 'choonsik'; // 무시
user.address.city = 'Yangjae'; // 무시

console.log(user); // { name: 'seungjin', address: { city: 'geumcheon' } }

출처 -https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze

const 와 Object.freeze 의 차이

const 는 name 을 immutable로 만드는 것이고 Object.freeze는 속성의 값을 immutable 로 만드는 것입니다. const 와 Object.freeze를 동시에 사용한다면 Object를 완벽하게 보호할 수 있습니다.

출처 - 생활코딩(https://opentutorials.org/module/4075)

추천

Object.assign, Object.freeze 사용하여 불변 객체를 만들려면 번거로운점이 많습니다. 그래서 대안으로 immer js 나 immutable.js를 사용하시기를 추천합니다.

immer js - https://github.com/immerjs/immer

immutable.js - https://github.com/immutable-js/immutable-js