JS - 객체와 변경불가성(Immutability)

chu·2021년 5월 11일
0
post-thumbnail

이번 시간에는 객체와 변경불가성인 Immutability에 대해 정리를 한다.
Immutability은 함수형 프로그래밍의 핵심 원리이다.

immutable value vs. mutable value

Javascript의 원시 타입(primitive data type) 은 변경 불가능한 값(immutable value)이다.

  • Boolean
  • null
  • undefined
  • Number
  • String
  • Symbol (New in ECMAScript 6)

원시 타입 이외의 모든 값은 객체(Object) 타입이며 객체 타입은 변경 가능한 값(mutable value)이다.

즉, 객체는 새로운 값을 다시 만들 필요없이 직접 변경이 가능하다는 것이다.

아래 예제 코드를 알아보자.

let str = 'Hello';
str = 'world';

첫번째 구문이 실행되면 메모리에 문자열 ‘Hello’가 생성되고, 식별자 str은 메모리에 생성된 문자열 ‘Hello’의 메모리 주소를 가리킨다.

두번째 구문이 실행되면 이전에 생성된 문자열 ‘Hello’을 수정하는 것이 아니라 새로운 문자열 ‘world’를 메모리에 생성하고 식별자 str은 이것을 가리킨다.

이때 문자열 ‘Hello’와 ‘world’는 모두 메모리에 존재하고 있다.
변수 str은 문자열 ‘Hello’를 가리키고 있다가 문자열 ‘world’를 가리키도록 변경되었을 뿐이다.


아래 예제 코드를 알아보자.

let statement = 'I am an immutable value';

let str = statement.slice(8, 17);

console.log(str); // immutable
console.log(statement); // I am an immutable value - 변화 X

2행에서 slice() 메소드는 statement 변수에 저장된 문자열을 변경하는 것이 아니라 사실은 새로운 문자열을 생성하여 반환하고 있다. 그 이유는 문자열 은 변경할 수 없는 immutable value 이기 때문이다.


이번에는 배열에 대해서 예제 코드를 알아보자.

const arr = [];
console.log(arr.length); // 0

const v2 = arr.push(2);    // arr.push()는 메소드 실행 후 arr의 length를 반환
console.log(arr.length); // 1

처리 후 결과의 복사본을 리턴하는 문자열의 메소드 slice()와는 달리 배열(객체)의 메소드 push()는 직접 대상 배열 을 변경한다. 그 이유는 배열은 객체이고 객체는 immutable value가 아닌 변경 가능한 값이기 때문이다.

이럴 경우에는 배열을 복사해서 원본은 건들지 않는 방법을 사용하면 된다. 예를 들면 push()가 아닌 concat() 메소드로 원본은 건들지 않고 복사하여 사용이 가능하다.

하지만 이 복사라는 개념은 직접 알아보고 어느 상황에서 써야할지 체크를 해바야한다.


다음 예제를 알아보자.

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

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

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

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

위 내용을 풀이하자면 myName에 Lee를 할당했다. 그리고 user.name은 kim으로 변경한 뒤myName을 확인해보니 변함없이 Lee를 출력한다. 바로 다음 myName에 다시 user.name을 할당하니 Kim을 출력한다.

여기서 Lee, Kim은 string 타입이다. 즉 immutable value 변경 불가능한 타입이기 때문에

첫번째 구문에서 Lee를 생성한 뒤 myName에 할당했다. 그럼 myName은 추가로 재할당을 하지 않는 이상 Lee를 참조하고 있다. 그래서 user.name을 Kim, Park, Choi 등 추가 생성하여 여러번 바꿔도myName은 변함이 없는 것이다.

마지막 구문 바로 위에서는 재할당을 했기 때문에 myName은 Kim을 참조하여 출력한다.


다음 예제 코드를 알아보자.

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

const user2 = user1; 

user2.name = 'Kim';

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

이제 슬슬 감이 와야한다. 위 예제는 string 즉, 변경 불가능한 타입이었다.
현재 예제는 user1 이라는 객체 리터널이며, 변경 불가능한 타입 아니다.

그래서 user1 user2name은 같은 메모리 주소를 참조하고 있기 때문에 프로퍼티 값을 Kim으로 변경하면 두 변수 모두 name의 값이 Kim으로 출력된다.

Object.assign

Object.assign은 타킷 객체로 소스 객체의 프로퍼티를 복사한다. 이때 소스 객체의 프로퍼티와 동일한 프로퍼티를 가진 타켓 객체의 프로퍼티들은 소스 객체의 프로퍼티로 덮어쓰기된다. 리턴값으로 타킷 객체를 반환한다.

ES6에서 추가된 메소드이며 Internet Explorer는 지원하지 않는다.

Object.assign(target, ...sources)

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

console.log(copy); // { a: 1 }
console.log(obj == copy); // false

// 병합 - 합칠 변수 명
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

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

console.log(merge1 === o1); // true

console.log(merge1); // { a: 1, b: 2, c: 3 }
console.log(o1);     // { a: 1, b: 2, c: 3 }, 타겟 객체가 변경된다!
  • Object.assign을 사용하여 기존 객체를 변경하지 않고 객체를 복사하여 사용할 수 있다.
  • 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 객체를 빈객체에 복사하여 새로운 객체 user2를 생성하였다. user1과 user2는 어드레스를 공유하지 않으므로 한 객체를 변경하여도 다른 객체에 아무런 영향을 주지 않는다.

주의할 것은 user1 객체는 const로 선언되어 재할당은 할 수 없지만 객체의 프로퍼티는 보호되지 않는다. 다시 말하자면 객체의 내용은 변경할 수 있다.

const로 할당되면 변경 불가능한거 아닌가요?
정확히는 재할당을 할 수 없는 것 뿐 내용은 변경이 가능하다.

const arr = [1,2,3];

arr = [4,5,6]; // 재할당 X 
arr[0] = 4; // 배열 요소 변경 O

그럼 객체를 불변으로 만들 수 없을까?

Object.freeze

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

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

// Object.assign은 완전한 deep copy를 지원하지 않는다.
// 얕은 복사를 하면서 name: Lee -> Kim으로 변경
const user2 = Object.assign({}, user1, {name: 'Kim'});

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

Object.freeze(user1); // freeze

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

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

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

하지만 이 또한 객체의 내부의 객체는 변경이 가능하다.

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

Object.freeze(user);

user.address.city = 'Busan'; // 변경된다!
console.log(user); // { name: 'Lee', address: { city: 'Busan' } }

물론 이 또한 불변으로 변경할 수 있다. 하지만 그렇게까지 해서 불변으로 만들까 싶다.

Object.assign과 Object.freeze을 사용하여 불변 객체를 만드는 방법은 번거러울 뿐더러 성능상 이슈가 있어서 큰 객체에는 사용하지 않는 것이 좋다.

또 다른 대안으로 Facebook이 제공하는 Immutable.js를 사용하는 방법이 있다.

위 대안은 필요하다면 직접 찾아서 사용을 해보자.
이렇게 이번 시간 객체와 변경불가성에 대해 정리를 했다. 아마 SPA 라이브러리나 프레임워크를 사용 했을 때 불변성을 지키기 위한 노력을 했을 것이다. 리액트를 사용할 때 그랬었고, 다행히 좋은 라이브러리가 많기 때문에 큰 무리없이 진행했다.

출처 : 객체와 변경불가성(Immutability)

다음 시간에는 함수형 프로그래밍에 중요한 기능인 클로저에 대해 다룰 예정이다.

profile
한 걸음 한걸음 / 현재는 알고리즘 공부 중!

0개의 댓글