immutability ( 변경불가성 ) 은 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미
# immutable value vs mutable value
javascript의 원시 타입 ( primitive data type ) 은 변경 불가능한 값 ( immutable value )
- Boolean
- null
- undefined
- Number
- String
- Symbol
원시 타입 제외 모든 값은 객체 (Object) 타입이며 변경 가능한 값 ( mutable value )
let str = 'hi';
str = 'bye';
식별자 str은 메모리에 생성된 문자열 'hi'의 메모리 주소를 가리키게 되고 두번째 구문이 실행되면 생성된 문자열 'hi'을 수정하는 것이 아니라 새로운 문자열 'bye'를 메모리에 생성하고 식별자 str은 이것을 가리키게 된다. 'hi' , 'bye' 모두 메모리에 존재하게 되며 변수 str은 문자열 'hi'를 가리키고 있다가 'bye'를 가리키게 변경되는 것이다.
// string은 immutable value
let statement = 'i am an immutable value';
let sliceState = statement.slice(8,17);
// immutable
console.log(sliceState);
// i am an immutable value
console.log(statement);
String 객체의 slice() 메소드는 statement 변수에 저장된 문자열을 변경하는 것이 아닌 새로운 문자열을 생성하여 반환하는데 그 이유는 문자열은 변경할 수 없는 immutable value 이기 때문.
let arr = [];
// 0
console.log(arr.length);
let arr2 = arr.push(1);
// 1
console.log(arr.length);
문자열의 예제와 같이 배열이 동작한다면 arr2는 새로운 배열을 가지게 될 것이다. 그러나 객체인 arr은 push 메소드에 의해 update 되고 arr2에는 배열의 새로운 length 값이 반환.
처리 후 결과의 복사본을 리턴하는 문자열의 메소드 slice()와 달리 배열(객체)의 메소드 push()는 직접 대상 배열을 변경 한다. 그 이유는 배열은 객체이고 객체는 immutable value 가 아닌 mutable value 이기 떄문이다.
let person = {
name : 'kim' ,
address : {
city : 'seoul'
}
};
let myName = person.name;
person.name = 'gan';
// kim
console.log(myName);
// 재할당
myName = person.name;
// gan
console.log(myName);
person.name의 값을 변경했지만 변수 myName의 값은 변경되지 않는다. 변수 myName에 person.name을 할당했을 때 person.name의 참조를 할당하는 것이 아니라 immutable value 인 'kim'이 메모리에 새로 생성되고 myName은 이것을 참조하기 때문. person.name의 값이 변경된다 하더라도 변수 myName이 참조하고 있는 'kim'은 변함 없다.
let person = {
name : 'kim',
address : {
city : 'seoul'
}
};
// 변수 otherPerson은 객체 타입
let otherPerson = person;
otherPerson.name = 'gan';
// gan
console.log(person.name);
// gan
console.log(person.name);
객체 otherPerson의 name 프로퍼티에 새로운 값을 할당하면 객체는 변경 불가능한 값이 아니기 떄문에 객체 otherPerson 은 변경된다. 변경하지도 않은 객체 person 도 동시에 변경이 되는데 person과 otherPerson 이 같은 어드레스를 참조하고 있기 때문.
# 불변 데이터 패턴 ( immutable data pattern )
의도하지 않은 객체의 변경이 발생하는 원인의 대다수는 "래퍼런스를 참조한 다른 객체에서 객체를 변경" 하기 때문이다. 이 문제의 해결 방법은 비용은 조금 들지만 객체를 불변객체로 만들어 프로퍼티의 변경을 방지하며 객체의 변경이 필요한 경우에는 참조가 아닌 객체의 방어적 복사 ( defensive copy ) 를 통해 새로운 객체를 생성한 후 변경한다.
- 객체의 방어적 복사 ( defensive copy ) - Object.assign
- 불변객체화를 통한 객체 변경 방지 - Object freeze
# Object.assign
Object.assign 은 타깃 객체로 소스 객체의 프로퍼티를 복사하는데 소스 객체의 프로퍼티와 동일한 프로퍼티를 가진 타겟 객체의 프로퍼티들은 소스 객체의 프로퍼티로 덮어쓰기 되며 리턴값으로 타깃 객체를 반환함
Object.assign(target, ...sources)
// copy
const obj = { a : 1 };
const copy = Object.assign( {} , obj );
// { a:1 }
console.log(copy);
// false
console.log(obj == copy)
// merge
const o1 = { a:1 };
const o2 = { b:2 };
const o3 = { c:3 };
const merge1 = Object.assign(o1, o2, o3);
// { a:1 , b:2 , c:3 }
console.log(merge1);
// { a:1 , b:2 , c:3 } --> 타겟 객체가 변경된다
console.log(o1);
// merge
const o4 = { a:1 };
const o5 = { b:2 };
const o6 = { c:3 };
const merge2 = Object.assign( {} , o4, o5, o6);
// { a:1 , b:2 , c:3 }
console.log(merge2);
// { a:1 }
console.log(o4);
Object.assign 을 사용하여 기존 객체를 변경하지 않고 객체를 복사하여 사용할 수 있다. Object.assign은 완전한 deep copy를 지원하지 않는다.
const person = {
name : 'kim',
address : {
city : 'seoul'
}
};
const otherPerson = Object.assign( {} , person );
// person 과 otherPerson 은 참조값이 다르다
// false
console.log( person == otherPerson );
otherPerson.name = 'gan';
// 'kim'
console.log(person.name);
// 'gan'
console.log(otherPerson.name);
// 객체 내부의 객체는 얕은 복사가 된다.
// true
console.log( person.address == otherPerson.address );
person.address.city = 'busan';
// busan
console.log(person.address.city);
// busan
console.log(otherPerson.address.city);
person 객체를 빈객체에 복사하여 새로운 객체 otherPerson 를 생성하였다. person 과 otherPerson 은 어드레스를 공유하지 않기 떄문에 한 객체를 변경하여도 다른 객체에 아무런 영향을 주지 않음.
주의할 것은 person 객체는 const 로 선언되어 재할당은 할 수 없지만 객체의 프로퍼티는 보호되지 않음.
# Object.freeze
Object.freeze()를 사용하여 불변(immutable) 객체로 만들 수 있다.
const person = {
name : 'kim',
address : {
city : 'seoul'
}
};
// Object.assign은 완전한 deep copy를 지원하지 않음
const otherPerson = Object.assign( {} , person , {name: 'gan'} );
// 'kim'
console.log(person.name);
// 'gan'
console.log(otherPerson);
Object.freeze(person);
// 무시됨.
person.name = 'gan';
// { name : 'kim' , address : {city:'seoul'}}
console.log(person);
내부 객체까지 변경 불가능하게 만들려면 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 person = {
name : 'kim';
address : {
city : 'seoul'
}
};
deepFreeze(person);
// 무시
person.name = 'gan';
// 무시
person.address.city = 'busan';
'Frontend > js' 카테고리의 다른 글
[ js ] ES5 vs ES6 차이 (0) | 2024.04.22 |
---|---|
[ js ] symbol (0) | 2023.10.02 |
#IT #먹방 #전자기기 #일상
#개발 #일상