-
[Javascript] 객체와 불변성(Immutability)javascript 2022. 10. 26. 09:55
poiemaweb에서 javascript 기본 문법에 대해 복습하던 중 헷갈리거나 제대로 알지 못했던 부분들이 있어 기록으로 남기고자 한다.
웹 프로그래밍 튜토리얼 | PoiemaWeb
Front-end Development Tutorial
poiemaweb.com
Immutability(불변성)는 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미한다.
Immutability는 우리가 SPA를 사용할 때에도 강조했다시피 중요한 원리이다.
객체는 참조(reference) 형태로 전달하고 전달 받는다. 객체가 참조를 통해 공유되어 있다면 그 상태가 언제든지 변경될 수 있기 때문에 문제가 될 가능성도 커지게 된다.
이는 객체의 참조를 가지고 있는 어떤 장소에서 객체를 변경하면 참조를 공유하는 모든 장소에서 그 영향을 받기 때문인데 이것이 의도한 동작이 아니라면 참조를 가지고 있는 다른 장소에 변경 사실을 통지하고 대처하는 추가 대응이 필요하다.
불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화할 수 있고 성능 개선에도 도움이 된다.
1. 변경 가능한 값
user.name의 값을 변경했지만 변수 myName의 값은 변경되지 않았다.
이는 변수 myName에 user.name을 할당했을 때 user.name의 참조를 할당하는 것이 아니라 immutable한 값(string) ‘Lee’가 메모리에 새로 생성되고 myName은 이것을 참조하기 때문이다.
따라서 user.name의 값이 변경된다 하더라도 변수 myName이 참조하고 있는 ‘Lee’는 변함이 없다.
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
아래의 경우 객체 user2의 name 프로퍼티에 새로운 값을 할당하면 객체는 변경 불가능한 값이 아니므로 객체 user2는 변경된다.
그런데 변경하지도 않은 객체 user1도 동시에 변경된다.
var user1 = { name: 'Lee', address: { city: 'Seoul' } }; var user2 = user1; // 변수 user2는 객체 타입이다. user2.name = 'Kim'; console.log(user1.name); // Kim console.log(user2.name); // Kim
이는 user1과 user2가 같은 어드레스를 참조하고 있기 때문이다.
이렇듯 객체에서는 참조의 의미가 매우 중요하다.
2. 불변 데이터 패턴(immutable data pattern)
위 경우들과 같이 의도하지 않은 객체의 변경이 발생하는 원인의 대다수는 레퍼런스를 참조한 다른 객체에서 객체를 변경하기 때문이다.
이 문제의 해결 방법은 객체의 변경이 필요한 경우에는 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경한다.
(1) Object.assign
Object.assign은 타킷 객체로 소스 객체의 프로퍼티를 복사한다.
이때 소스 객체의 프로퍼티와 동일한 프로퍼티를 가진 타켓 객체의 프로퍼티들은 소스 객체의 프로퍼티로 덮어쓰기된다. ES6에서 추가된 메소드이며 Internet Explorer는 지원하지 않는다.
// Syntax Object.assign(target, ...sources)
기존 객체를 변경하지 않고 객체를 복사하고 싶으면 target을 빈 객체로 설정하면 된다.
// 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은 완전한 deep copy를 지원하지 않고 객체 내부의 객체(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
user1 객체는 const로 선언되어 재할당은 할 수 없지만 객체의 프로퍼티는 보호되지 않는다. 다시 말하자면 객체의 내용은 변경할 수 있다.
(2) Object.freeze
Object.freeze()를 사용하여 불변(immutable) 객체로 만들수 있지만 객체 내부의 객체(Nested Object)는 변경가능하다.
내부 객체까지 변경 불가능하게 만들려면 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' } }
(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 모듈을 import하여 사용한다.
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()은 결과를 반영한 새로운 객체를 반환한다.
'javascript' 카테고리의 다른 글
[Javascript] insertAdjacentHTML (0) 2023.01.30 [Javascript] JSDoc (0) 2022.10.26 [Javascript] 객체 (0) 2022.10.25 [Javascript] 단축평가 (0) 2022.10.25 [Javascript] 데이터 타입과 변수 (0) 2022.10.24