Object.freeze vs Object.seal - 불변성과 관련된 두 가지 네이티브 메서드


원문: https://javascript.plainenglish.io/object-freeze-vs-object-seal-immutability-7c22f80aa8ae

img

Photo by Christine Donaldson

데이터 불변성은 자바스크립트를 비롯한 프로그래밍 언어에서 중요한 자리를 차지해왔다. 여기 부분적으로 불변성을 보장하기 위한 두 가지 자바스크립트 메서드가 있다. 바로 Object.freezeObject.seal이다. 이번 포스트에서는 꽤 헷갈릴 수 있는 두 가지 메서드를 비교해 본다.

Object.defineProperty

freezeseal을 각각 알아보기 이전에, 먼저 객체의 defineProperty라는 메서드가 무엇인지 알아야 한다. 우선, 객체가 초기 처리 중 사용자 또는 엔진에 의해 생성되면 자바스크립트는 객체에 기본적인 속성들을 부여하여 속성 접근이나 속성 제거와 같은 외부 요청을 처리할 수 있게 한다.

수정하거나 설정할 수 있는 속성은 다음과 같다.

  • value — 속성의 값
  • enumerabletrue라면, for-in 루프문 또는 Object.keys()로 속성을 검색할 수 있다. 기본값은 false이다.
  • writablefalse라면 속성을 수정할 수 없다. 스트릭트 모드에선 에러를 발생시킨다. 기본값은 false이다.
  • configurablefalse라면 객체의 속성들을 열거할 수 없고(non-enumerable), 쓸 수 없으며(non-writable), 제거할 수 없다(non-deletable). 기본값은 false이다.
  • get — 속성에 접근하려 할 때 미리 호출되는 함수이다. 기본값은 undefined이다.
  • set — 속성에 어떤 값을 설정하려 할 때 미리 호출되는 함수이다. 기본값은 undefined이다.

여러분들이 이게 무슨 말인가 생각하고 있을 것이라 짐작한다. 그러니 몇 가지 간단한 예제를 살펴보자.

Enumerable

const obj = {};

Object.defineProperty(obj, 'a', {
  value: 100,
  enumerable: false
});

for (const key in obj) {
  console.log(key);
}
// undefined

Object.keys(obj);
// []

Writable

const obj = {};

Object.defineProperty(obj, 'a', {
  value: 100,
  writable: false
});

obj.a = 200;
obj.a === 100; // true

(() => {
  'use strict';
  obj.a = 100;
  // 스트릭트 모드에서 TypeError가 발생한다.
})()

Configurable

const obj = {};

Object.defineProperty(obj, 'a', {
  value: 100,
  configurable: false
});

// 1. non-enumerable
for (const key in obj) {
  console.dir(key);
}
// undefined

Object.keys(obj);
// []

// 2. non-writable
(() => {
  'use strict';
  obj.a = 200;
  // 스트릭트 모드에서 TypeError가 발생한다.
})()

// 3.non-deletable
delete obj.a;
obj.a === 100; // true

하지만, writable 또는 enumerabletrue면, configurable: false는 무시된다.

Object.Seal

Photo by 戸山 神奈 on Unsplash

“seal”이라는 단어를 처음 보면 무엇이 떠오르는가? “seal”의 첫 번째 의미는 편지나 무언가를 봉인하는 왁스를 뜻한다. 자바스크립트에서 Object.seal 또한 “seal”이 하는 것과 같은 동작을 한다.

Object.seal은 전달 받은 객체의 모든 속성을 구성할 수 없게(non-configurable) 만든다. 예제를 살펴 보자.

const obj = { a: 100 };
Object.getOwnPropertyDescriptors(obj);
/* {
 *   a: {
 *        configurable: true,
 *        enumerable: true,
 *        value: 100,
 *        writable: true
 *      }
 * }
 *
 */

Object.seal(obj);
Object.getOwnPropertyDescriptors(obj);

/* {
 *   a: {
 *        configurable: false,
 *        enumerable: true,
 *        value: 100,
 *        writable: true
 *      }
 * }
 *
 */

obj.a = 200;
console.log(obj.a);
// 200

delete obj.a;
console.log(obj.a);
// 200

obj.b = 500;
console.log(obj.b);
// undefined

객체 obj는 값이 100인 속성 하나를 가지고 있다. 그리고 obj의 초기 서술자(descriptor)는 위와 같은데, configurable, enumerable, writable 모두 true다. 이후 객체를 Object.seal로 봉인(seal)하고 서술자를 보면 딱 configurablefalse로 변경된 것을 볼 수 있다.

obj.a = 200;

봉인된 객체의 configurablefalse가 되어도 기존 속성값은 200으로 변경되게 된다. 앞서 설명했듯 configurablefalse로 설정하면 속성을 쓸 수 없도록 만들어지지만 writable이 명시적으로 true면 이는 동작하지 않는다. 그리고 객체를 만들고 새 속성을 설정하게 되면 writable: true가 기본으로 지정된다.

delete obj.a;

객체를 봉인하면 모든 속성은 구성할 수 없도록 만들어져 삭제할 수 없게(non-deletable) 된다.

obj.b = 500;

객체를 봉인한 후 이 명령은 실패했다. 이유는 Object.seal 또는 Object.freeze가 호출되면, 여기 전달된 객체는 확장 불가능한 객체로 바뀌기 때문이다. 즉, 객체에서 속성을 삭제하거나 추가할 수 없게 된다.

Object.freeze

Object.freeze에 객체가 전달되면 Object.seal보다 더 제한이 심해진다. 비슷한 예제를 살펴보자.

const obj = { a: 100 };
Object.getOwnPropertyDescriptors(obj);
/* {
 *   a: {
 *        configurable: true,
 *        enumerable: true,
 *        value: 100,
 *        writable: true
 *      }
 * }
 *
 */
Object.freeze(obj);
Object.getOwnPropertyDescriptors(obj);
/* {
 *   a: {
 *        configurable: false,
 *        enumerable: true,
 *        value: 100,
 *        writable: false
 *      }
 * }
 *
 */

obj.a = 200;
console.log(obj.a);
// 100

delete obj.a;
console.log(obj.a);
// 100

obj.b = 500;
console.log(obj.b);
// undefined

Object.seal과의 차이점은 동결(freeze)된 후 writable 또한 false로 설정된다는 것이다.

obj.a = 200;

그러므로, 기존 속성을 수정하는 것은 실패한다.

delete obj.a;

Object.seal처럼 Object.freeze 또한 객체를 구성 할 수 없게 만들며 모든 속성은 제거 불가능하게 된다.

obj.b = 500;

객체를 동결하는 것 또한 객체를 확장 불가능한 객체로 만든다.

이 둘의 공통점은 무엇일까?

  1. 전달된 객체들은 확장할 수 없게 되어 새로운 속성을 추가할 수 없다.
  2. 전달된 객체 내부의 모든 요소들은 구성 불가능하게 되어 삭제할 수 없다.
  3. 스트릭트 모드에서 두 메서드를 사용한 객체 모두 obj.a = 500 같은 연산을 실행하면 에러가 발생한다.

차이점은?

Object.seal은 속성을 수정할 수 있지만 Object.freeze는 그렇지 않다.

결함

Object.freezeObject.seal 모두 “실용적” 측면에서 “결함"이 있는데, 둘 다 중첩된 객체들을 동결하고 봉인할 수 없기 때문이다. 빠르게 비교해보자.

const obj = {
  foo: {
    bar: 10
  }
};

지금 obj는 내부에 중첩된 객체 foo를 가지고 있고, foo는 내부에는 bar가 있다.

Object.getOwnPropertyDescriptors(obj);
/* {
 *   foo: {
 *        configurable: true,
 *        enumerable: true,
 *        value: {bar: 10},
 *        writable: true
 *      }
 * }
 */

Object.getOwnPropertyDescriptors(obj.foo);
/* {
 *   bar: {
 *        configurable: true,
 *        enumerable: true,
 *        value: 10,
 *        writable: true
 *      }
 * }
 */

이제 이것들을 봉인 또는 동결하고 나서,

Object.seal(obj);
Object.freeze(obj); // 둘 중 무엇을 실행하더라도 이 테스트와 무관하다.

서술자가 어떻게 변했는지 보도록 하자.

Object.getOwnPropertyDescriptors(obj);
/* {
 *   foo: {
 *        configurable: false,
 *        enumerable: true,
 *        value: {bar: 10},
 *        writable: false
 *      }
 * }
 */

Object.getOwnPropertyDescriptors(obj.foo);
/* {
 *   bar: {
 *        configurable: true,
 *        enumerable: true,
 *        value: 10,
 *        writable: true
 *      }
 * }
 */

foo의 서술자는 변경되었지만 obj.foo는 중첩된 객체로 이것을 보면 여전히 수정 가능하다는 것을 의미하고 있다.

obj.foo = { bar: 50 };
// 동작하지 않는다.

obj.foo는 동결되었으므로 그 값은 변경되지 않는다.

obj.foo.bar = 50;
// 동작한다.

obj.foo.bar는 동결되어 있지 않기 때문에 그 값을 변경할 수 있다.

그렇다면 가장 하위에 중첩된 객체까지 객체를 동결하거나 봉인하는 방법은 어떤 것이 있을까? MDN은 이에 대한 해결 방안을 제시하고 있다.

function deepFreeze(object) {

  // 객체에 정의된 속성의 이름을 조회한다.
  var propNames = Object.getOwnPropertyNames(object);

  // 자신을 프리징하기 전 속성을 프리징한다.
  for (let name of propNames) {
    let value = object[name];

    if(value && typeof value === "object") {
      deepFreeze(value);
    }
  }

  return Object.freeze(object);
}

이것의 테스트 결과는 아래와 같다.

const obj = { foo: { bar: 10 } };
deepFreeze(obj);

obj.foo = { bar: 50 };
// 동작하지 않는다.

obj.foo.bar = 50;
// 동작하지 않는다.

결론

Object.freezeObject.seal은 확실히 유용한 메서드가 될 것이다. 하지만 중첩된 객체를 동결하기 위해선 deepFreeze를 사용해야 한다는 점을 반드시 고려해야 한다.

참조

이진우2022.04.20
Back to list