늘 겸손하게

JavaScript - 불변성(Immutability)을 유지하려면 어떻게 해야하나요? 본문

Programming/JavaScript

JavaScript - 불변성(Immutability)을 유지하려면 어떻게 해야하나요?

besforyou999 2022. 8. 9. 22:11

1. 불변성 (Immutability)?

 

 

말 그대로 한번 설정되면 변하지 않는다는 의미이다. 자바스크립트에서 원시값은 불변값이므로 한 번 생성되면 변경되지 않는다. 그래서 재할당시 변수가 가리키고 있는 메모리 위치에 새로운 값을 저장하는 것이 아닌, 새로운 메모리 위치에 값을 할당하고 새로운 메모리 위치의 주소값을 변수에 저장시킨다.

 

왜 중요한가요?

  • 불변성을 지키지 않으면 데이터가 어디서 어떻게 바뀌는지 파악하기가 어려워집니다. (값이 예측불가능해진다) 
  • 불변성을 지키지 않으면 예기치못한 side effect를 발생시켜 버그를 유발할 수 있습니다.
  • 객체의 불변성을 지키지 않고 참조 값을 여러 객체가 공유할 경우 어떤 객체에서 값이 변경되었을때 의도치 않게 객체를 참조중인 객체에서도 값이 변경되어 변경된 곳을 찾기 어려워집니다.
  • 전역 변수를 많이 사용하는것을 지양하는것과 비슷한 이유

 

불변값, 가변값

 

 

자바스크립트에는 Immutable typeMutable type이 존재합니다.

변경 가능한 자료형과 변경 불가능한(불변하는) 자료형이 존재합니다.

 

1.1 Immutable type

자바스크립트에서 불변하는 타입원시 타입(primitive type)입니다. 아래는 원시 타입 목록입니다.

 

  • string
  • number
  • bigint
  • boolean
  • symbol
  • null
  • undefined

 

아래의 예시를 통해 원시 타입은 불변한다는것을 알 수 있습니다.

 

var statement = 'I am an immutable value';

var otherStr = statement.slice(8, 17);

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

 

slice() 메소드는 statement 변수에 저장된 문자열을 변경하는 것이 아니라 새로운 문자열을 생성하여 반환하고 있습니다.

그 이유는 문자열은 변경할 수 없는 원시 타입이기 때문입니다.

 

1. 2 Mutable type

자바스크립트에서 원시 타입 이외의 모든 값은 객체(Object) 타입이며 객체 타입은 변경 가능한 값(mutable value)이다. 즉, 객체는 직접 변경이 가능하다는것.

 

< 예 >

 

var user = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

var myName = user.name; // 변수 myName은 string 타입이다.

user.name = 'Kim';
console.log(myName); // Lee

myName = user.name;  // 재할당
console.log(myName); // Kim

 

위 예시를 보면 "user.name = 'Kim';" 부분에서 객체의 속성이 변경된것을 알 수 있다.

하지만 string 타입 데이터를 받은 변수 myName는 원시 타입인 string 타입을 저장하고 있기 때문에 변수에 저장된 값이 변경되지 않은것을 볼 수 있다.

 

 

객체는 변경 가능한 값(mutable value)이기 때문에 다음과 같은 오류가 발생할 수도 있다.

 

var user1 = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

var user2 = user1; // 변수 user2는 객체 타입이다.

user2.name = 'Kim';

console.log(user1.name); // Kim
console.log(user2.name); // Kim

 

변수 user2는 변경 가능한 값인 객체를 할당하면 객체의 레퍼런스를 참조하게된다.

그러므로 user2에서 속성을 변경하면 같은 레퍼런스를 가리키고 있는 user1 변수의 속성도 변하게 되는것.

이러한 특징이 변수값을 예측 불가능하게 만든다.

 

 

불변성을 유지하는 방법

 

1. 불변 데이터 패턴 (immutable data pattern)

 

의도하지 않은 객체 변경이 일어나는 경우은 레퍼런스를 참조한 다른 객체에서 객체를 변경하기 때문이다. 이 문제를 막기 위해서는 객체를 불변객체로 만들거나 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경한다.

 

  • 객체의 방어적 복사(defensive copy) : Object.assign
  • 불변객체화를 통한 객체 변경 방지 : Object.freeze

 

1. 1 Object.assign

Object.assign은 타깃 객체에 소스 객체의 속성을 복사한다. 이때 소스 객체의 속성과 동일한 타깃 객체의 속성들은 소스 객체의 속성으로

덮어쓰기된다. ES6에서 추가된 메소드이며 IE는 지원하지 않는다.

 

// Syntax
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 }

 

Object.assign을 이용해 기존 객체를 변경하지 않고 객체를 복사하여 사용할 수 있다. 단, 객체 내부의 객체(Nested Object)는 Shallow copy된다.

 

const user1 = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

// 새로운 빈 객체에 user1을 copy한다.
const user2 = Object.assign({}, user1);
// user1과 user2는 참조값이 다르다.
console.log(user1 === user2); // false

user2.name = 'Kim';
console.log(user1.name); // Lee
console.log(user2.name); // Kim

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

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

 

 

2.2 Object.freeze

 

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

 

const user1 = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

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

console.log(user1.name); // Lee
console.log(user2.name); // Kim

Object.freeze(user1);

user1.name = 'Kim'; // 무시된다!

console.log(user1); // { name: 'Lee', address: { city: 'Seoul' } }

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


user1.address.city = 'Busan';
console.log(user1) // { name: 'Lee', address: { city: 'Busan' } }

 

 

Object.freeze()를 통해 객체 속성이 변경되는것을 막을 수 있다.

 

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

 

코드 아래부분에 address 객체의 city 속성은 변경되는것을 볼 수 있다.

 

 

내부 객체까지 변경 불가능하게 만들려면 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 user = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

deepFreeze(user);

user.name = 'Kim';           // 무시된다
user.address.city = 'Busan'; // 무시된다

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

 

위 코드로 객체 내부의 객체를 포함한 모든 속성을 freeze하여 변경을 막을 수 있습니다.

 

2.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

 

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은 불변하였다.

 

 

 

출처

https://velog.io/@co_mong/JS-%EB%B6%88%EB%B3%80%EC%84%B1Immutability

https://poiemaweb.com/js-immutability

https://velog.io/@yunju/JavaScript-%EA%B0%9D%EC%B2%B4%EC%99%80-%EB%B6%88%EB%B3%80%EC%84%B1