Frontend/js

[ js ] Immutability(변경불가성)

IT grow. 2024. 5. 9. 19:58
반응형

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';
반응형