본문 바로가기

프로그래밍/js

lessons 11. 객체와 변경불가성(Immutability)

2023.02.08

자바스크립트 스터디 4회차

공부 사이트: https://poiemaweb.com/

 

 

11. 객체와 변경불가성(Immutability)

변경불가성이란, 객체 생성 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미한다.

lessons 10에서 포스팅했듯, 객체는 pass-by-reference 특성을 가지고 있다. 객체가 참조를 통해 공유되어 있다면 그 상태가 언제든 변경될 수 있기 때문에 문제가 될 수도 있다. 

객체의 참조(주소)를 가지고 있는 변수가 객체를 변경하면, 참조를 공유하는 모든 곳에서 그 영향을 받기 때문이다. 

 

의도치 않은 객체의 변경이 발생하는 원인의 대다수가 "레퍼런스를 참조한 다른 객체에서 객체를 변경하기 때문" 이다.

이러한 문제의 해결방법은 ①불변객체로 만들기 ②Observer 패턴 이용하는 것이다.

#Observer 패턴: 객체의 상태 변화를 관찰하는 관찰자인 옵저버(Observer). 옵저버들의 목록을 객체에 등록한다. 그리고 객체 상태 변경 시 그 객체에 의존하는 다른 객체들에게 통지되는 디자인 패턴이다.

 

불변객체란, 객체 생성 후 그 상태를 바꿀 수 없는 객체를 의미한다. 프로퍼티의 변경을 방지할 수 있으며, 변경이 필요한 경우 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경한다.

 

 

1) 변경불가능한 값 vs 변경 가능한 값

- 원시 타입: 변경 불가능한 값(immutable value)

- 객체 타입: 변경 가능한 값(mutable value)

 

예제 ①

var str1 = 'immutable value';  //문자열은 변경 불가능한 값
//slice()는 str1을 변경하는 것이 아니라, 동작에 따라 새로운 문자열을 생성해서 반환.
var str2 = str1.slice(10,15);  

console.log(str2);
console.log(str1);     //str1 정상적으로 출력 (변경X)

 

예제 ②

var arr = [];
console.log('arr의 길이: ' + arr.length);

var tmp = arr.push(2);
console.log('push 후 arr의 길이: ' + arr.length);

console.log('tmp의 길이: ' + tmp.length);
console.log('tmp: ' + tmp);

slice() 메소드는 동작에 따라 처리 후 새로운 문자열을 반환하나, push() 메소드는 직접 대상 배열을 변경한다. 그래서 아래 코드를 보면 tmp의 길이가 undefined인 것을 확인할 수 있다. 그러나 막상 tmp을 출력해보면 1이 있다.

 

push() 메소드는 배열의 끝에 요소를 추가하고, 배열의 새로운 길이를 반환하기에 tmp에는 1이라는 숫자가 할당된다. 또한 length 속성은 문자열의 길이를 구하는 속성이기에, 숫자형인 tmp에는 사용 불가능하여 undefined가 반환된다.

 

var arr = [];
var tmp = arr.push(2);

console.log('arr 타입: ' + typeof arr);
console.log('tmp 타입: ' + typeof tmp);

배열은 객체이고, 객체는 변경 가능한 값이기 때문에 push()함수가 직접 대상 배열을 변경할 수 있는 것.

 

 

예제 ③ 

var person = {
	name: "홍길동",
	address: {
		city: 'Seoul'
	}
};

var myName = person.name;
console.log(myName);

person.name = "홍길빵";
console.log(myName);

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

처음 myName 변수를 출력할 때, '홍길동' 문자열이 할당되어 있다.

그리고 person.name에 '홍길빵' 할당 후 출력 시, 여전히 '홍길동' 문자열을 확인할 수 있다.

마지막으로, myName 변수에 person.name을 할당하면 '홍길빵' 문자열이 출력된다.

 

이 이유는 너무나 당연하다. 

- 첫 번째 출력: person 객체의 name 값은 '홍길동'. 문자열이 myName에 저장됨.

- 두 번째 출력: person.name에 '홍길빵'을 할당했으나, console.log()통해 출력한건 myName 변수(문자열)

- 세 번째 출력: 현재 person 객체의 name 값은 '홍길빵' 으로 변경된 상태임. 이를 myName 변수에 할당하기에 당연히 myName 변수에는 '홍길빵' 문자열이 저장된다.

 

즉, myName 변수에는 문자열이 저장될 뿐이다. myName 변수는 메모리에 있는 변경 불가능한 값(immutable value)을 가리킨다.

 

 

예제 ④

var person1 = {
	name: "홍길동",
	address: {
		city: 'Seoul'
	}
};

var person2 = person1;     //person2: Object
person2.name = "홍길빵";

console.log('person1.name: ' + person1.name);
console.log('person2.name: ' + person2.name);

이 경우, person1과 person2는 같은 주소를 가리키고 있다. person2의 타입은 Object가 되며, person2의 name 값 변경 시 변경되는 객체는 person1이 가리키는 객체와 동일하다.

 

 

 

2) 불변 데이터 패턴(immutable data paatern)

의도치 않은 객체의 변경이 발생하는 원인의 대다수가 "레퍼런스를 참조한 다른 객체에서 객체를 변경하기 때문" 이다.

 

[해결방법]

- 객체의 불변객체화 → Object.assign  (ES6에서 추가된 메소드)

- 객체 변경 시에는 객체의 방어적 복사 → Object.freeze

 

① Object.assign 

- 타겟 객체로 소스 객체의 프로퍼티를 복사한다.

- 이 때, 소스 객체의 프로퍼티와 동일한 프로퍼티를 가진 타겟 객체의 프로퍼티들은 덮어씌워진다.

- 반환값은 타겟 객체.

// Syntax
Object.assign(target, ...source)

 

//Copy
const obj = { a:1 };
const copy = Object.assign({}, obj);

console.log(obj);
console.log(copy);
console.log(obj == copy);

 

 

// Merge
const o1 = { a: 0 };
const o2 = { a: 1, b: 2 };
const o3 = { c: 3 };

const merge = Object.assign(o1, o2, o3);

console.log(merge); 
console.log(o1);
console.log(merge == o1);

- 이 경우, 타겟 객체는 o1이고, 소스 객체는 o2와 o3이 된다.

- o1에 존재하는 프로퍼티 a는 o2의 프로퍼티 a의 값으로 덮어씌워진다.

- o1에 존재하지 않는 프로퍼티 b와 c가 추가된다.

- 추가된 o1이 반환되어 merge 변수에 저장.

 

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

→ 여기서 말하는 기존 객체는 소스 객체를 의미하는 듯 하다. 소스 객체의 프로퍼티를 복사해서 사용할 수 있다는 의미인 듯.

Object.assign은 완전한 deep copy를 지원하지 않는다. 객체 내부의 객체는 Shallow copy된다.

 

 


[TIP] 깊은 복사와 얕은 복사

- 깊은 복사(deep copy): 원본 객체에 중첩되어있는 객체까지 복사.

- 얕은 복사(Shallow copy): 한 단계만 복사. 내부 필드에 있는, 중첩되어있는 객체는 원본 객체와 같은 참조를 가리킴.

즉, Shallow copy의 경우 내부 필드에 있는 Address 객체는 공유됨. 

→ 객체 내부의 객체(Nested Object)는 공유된다.


 

 

② Object.freeze

- 해당 메소드를 사용하여 불변 객체로 만들 수 있다.

const person1 = {
  name: '홍길동',
  address: {
    city: 'Seoul'
  }
};

// Object.assign은 완전한 deep copy를 지원하지 않는다. Nested Object인 address는 변경가능하다.
// 소스 객체로 사용된 person1이 변경되지 않고 복사되어 person2로 반환된다.
const person2 = Object.assign({}, person1, {name: '홍길빵'});

console.log(person1.name);  //홍길동
console.log(person2.name);  //홍길빵

Object.freeze(person1);     //불변객체화
person1.name = '고길동';
console.log(person1);       //변하지 않는다.

console.log(Object.isFrozen(person1));   //불변객체인지?

// 변경 가능한 address
person1.address.city = 'Busan';
console.log(person1);
console.log(person2);

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

 

//Deep freeze func
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: '홍길동',
  address: {
    city: 'Seoul'
  }
};

deepFreeze(person);

person.name = '고길동';           // 무시된다
person.address.city = '길동이네집'; // 무시된다

console.log(person);

[Deep freeze func]

- getOwnPropertyNames(obj): 객체의 모든 속성을 배열로 반환한다.

- 배열명.forEach((요소명) => { ... });

- 배열이 가지고있는 모든 요소를 한번씩 반환하여 반복. deepFreeze()를 재귀적으로 호출함으로서 객체의 내부 객체 모두를 freeze한다.

 

 

 

③  Immutable.js

- Object.assign과 Object.freeze를 사용하여 불변 객체를 만드는 방법은 번거롭고, 성능상 이슈가 있음.

- 또 다른 대안으로 Immutable.js를 사용

- List, Stack, Map, OrderedMap, Set, OrderedSet,  Record와 같은 영구 불변 데이터 구조를 제공함.

//Immutable.js의 Map 모듈을 임포트하여 사용
npm install immutable

 

 

const { Map } = require('immutable')
const map1 = Map({ a:1, b:2, c:3 })
const map2 = map1.set('b', 50)
console.log(map1.get('b'))
console.log(map2.get('b'))

map.set(key, value): key 를 이용해 value 를 저장 후 결과를 반영한 새로운 객체 반환.

map1.set() 통해서 b 속성을 50으로 변경했음에도 map1은 변함없다. 

 


[TIP] JS에서의 모듈 불러오기

- require 키워드: node.js에서 사용. 하나의 파일에서 다른 파일의 코드를 불러온다. (CommonJS 키워드)

- import 키워드:  require와 같은 기능. 필요한 모듈 부분만 선택해서 로드 가능. require보다 성능 우수. (ES6 키워드)

// CommonJS
const module_name = require('./module.js')
// ES6
import module_name from './module.js'

// MomentJs 라이브러리 불러오기
const mnt = require("moment")
import mnt from "moment"

 

[TIP] JS에서의 모듈 내보내기

- exports 키워드: node.js에서 사용. 모듈로부터 내보내지는 데이터들을 담고있는 하나의 객체. (CommonJS 키워드)

- export 키워드: exports와 같은 기능. (ES6 키워드)

// CommonJS
const str = 'test';
module.exports = str;

// ES6
const str = 'test';
export default str;

 

'프로그래밍 > js' 카테고리의 다른 글

lessons 13. 타입 체크  (0) 2023.02.08
lessons 12. 함수  (0) 2023.02.07
lessons 10. 객체  (0) 2023.02.05
lessons 9. 타입 변환과 단축 평가  (1) 2023.02.05
lessons 8. 제어문  (0) 2023.02.03