JavaScript 객체와 불변성

손윤주·2022년 5월 20일
2
post-thumbnail

✔️ JavaScript 객체와 불변성이란?

Immutability(변경불가성)는 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미한다.
Immutability은 함수형 프로그래밍의 핵심 원리이다.

불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고 성능 개선에도 도움이 된다.
하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우 오히려 부적절한 경우가 있다.

ES6에서는 불변 데이터 패턴(immutable data pattern)을 쉽게 구현할 수 있는 새로운 기능이 추가되었다.


불변한 값 vs 변경 가능한 값

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

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

원시 타입 이외의 모든 값은 객체(Object) 타입이며 객체 타입 은 변경 가능한 값(mutable value)이다.
즉, 객체는 새로운 값을 다시 만들 필요없이 직접 변경이 가능하다는 것이다.

💡 객체는 변할 수 있는 값이고 원시 타입은 불변한 값이다.

예를 들어 살펴보자. C 언어와는 다르게 Javascript의 문자열은 변경 불가능한 값(immutable value) 이다. 이런 값을 “primitive values” 라 한다. (변경이 불가능하다는 뜻은 메모리 영역에서의 변경이 불가능하다는 뜻이다. 재할당은 가능하다)

아래 예시들을 보면 쉽게 이해할 수 있다.


var str = 'Hello';
str = 'world';

첫번째 줄이 실행되면 메모리에 문자열 'Hello'가 생성되고, 변수 str은 'Hello'의 메모리 주소를 가리킨다. 그리고 두번째 줄이 실행되면 이전에 생성되었던 문자열 'Hello'를 수정하는 것이 아니라 새로운 문자열 ‘world’를 메모리에 생성하고 식별자 str은 이것을 가리킨다. 이때 문자열 ‘Hello’와 ‘world’는 모두 메모리에 존재하고 있다. 변수 str은 문자열 ‘Hello’를 가리키고 있다가 문자열 ‘world’를 가리키도록 변경되었을 뿐이다. *(원시 타입은 불변한 값이기 때문)


var statement = 'I am an immutable value'; // string은 immutable value

var otherStr = statement.slice(8, 17);

console.log(otherStr);   // 'immutable'
console.log(statement);  // 'I am an immutable value'

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


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

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

위 예시를 보면 arr는 배열의 요소가 없다가, 생겼다.
결과의 복사본을 리턴하는 문자열의 메소드 slice()와는 달리 배열(객체)의 메소드 push()는 배열 원본 자체를 변경한다.
배열은 객체이고, 객체는 불변한 값이 아닌 변경 가능한 값이기 때문이다.

💡 원시 값의 메소드는 복사본을 리턴하고, 배열(객체)의 메소드는 기존 배열 자체를 변경한다!


📍 그래서 객체는 아래와 같은 일이 발생할 수 있어요.

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

var user2 = user1; // 변수 user2는 객체 타입이다.

user2.name = 'Kim';

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

위 코드를 보면 user2.name 값을 변경하니 user1의 값도 똑같이 변경된 것을 알 수 있다.
user1 와 user2는 값이 변할 수 있는 객체이며 동일한 값을 가리키는 메모리 주소(변수)이기 때문이다.

만약 의도적으로 하나의 값에 2개의 변수를 지정한 것이 아니라면 이 사실을 인지하고 적절히 대응해야 한다.


✔️ 위와 같은 일을 방지하려면,

객체를 불변객체로 만들어 값의 변경을 방지하거나, 객체의 변경이 필요한 경우에는 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경한다. 얕은 복사가 아닌 깊은 복사를 하는 것!

  • 객체의 방어적 복사(defensive copy)
    Object.assign
  • 불변객체화를 통한 객체 변경 방지
    Object.freeze

01 .assign() 을 이용해서 객체 복사하기

객체의 방어적 복사(Object.assign)는 신규 객체에 타겟(기존) 객체의 값을 복사하는 것을 말한다.
최종 return 값은 신규 객체를 반환한다. ES6에서 추가된 메소드이며 역시나 Internet Explorer는 지원하지 않는다.

💡 값은 동일하지만 복사해서 새로운 메모리 공간에 넣고, 메모리 주소(객체)가 가리키는 곳을 다르게 하는 것!

예제 1번

// Syntax
Object.assign(target, ...sources)
// Copy
const obj = { a: 1 };
const copy = Object.assign({}, obj); // assign은 배열을 복사하는 메서드
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(target, ...sources)을 사용하여 복사 후 기존 객체를 변경하지 않고 복사하여 사용하는 것은 얕은 복사(Shallow copy)라고 한다. Object.assign 자체로는 깊은 복사를 deep copy를 할 수 없기 때문에 아래와 같은 방법을 써야 깊은 복사가 가능해진다.

예제 2번

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로 선언되어 객체를 재할당 할 수 없지만 객체 내부의 값은 바꿀 수 있다.


02 .freeze()를 이용해서 불변 객체 만들기

Object.freeze()를 사용하면 마치 원시 타입처럼 값이 변하지 않는 불변(immutable) 객체로 만들수 있다.

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

// Object.assign은 완전한 deep copy를 지원하지 않는다.
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

다만 객체 내부의 객체(Nested Object)는 변경가능하다.


✍️ 총정리

JavaScript의 데이터타입 중 원시 타입은 값이 변하지 않는 불변 값이고,
객체는 값이 바뀔 수 있는 변경 가능한 값이다.

객체의 이러한 특성 때문에 하나의 값에 두 개의 객체명 (A, B)이 생겼을 경우, A 객체의 값을 변경했을 때 의도치 않게 B 객체의 값도 같이 변경되어 난감해질 수 있다.

이를 방지하기 위한 방법으로 .assign()를 사용하는 것이 있는데, 이 때 주의할 것은 얕은 복사는 원본 메모리까지 복사하지 못하기 때문에 같은 문제가 발생할 우려가 있고, 예제 2번처럼 깊은 복사를 해야한다. 그리고 값을 변경할 일이 없는 객체에 .freeze()를 사용해서 불변 객체로 만들어주면 해결!!

참고자료 출처

0개의 댓글