원문: https://javascript.plainenglish.io/object-freeze-vs-object-seal-immutability-7c22f80aa8ae
Photo by Christine Donaldson
데이터 불변성은 자바스크립트를 비롯한 프로그래밍 언어에서 중요한 자리를 차지해왔다. 여기 부분적으로 불변성을 보장하기 위한 두 가지 자바스크립트 메서드가 있다. 바로 Object.freeze
와 Object.seal
이다. 이번 포스트에서는 꽤 헷갈릴 수 있는 두 가지 메서드를 비교해 본다.
freeze
와 seal
을 각각 알아보기 이전에, 먼저 객체의 defineProperty
라는 메서드가 무엇인지 알아야 한다. 우선, 객체가 초기 처리 중 사용자 또는 엔진에 의해 생성되면 자바스크립트는 객체에 기본적인 속성들을 부여하여 속성 접근이나 속성 제거와 같은 외부 요청을 처리할 수 있게 한다.
수정하거나 설정할 수 있는 속성은 다음과 같다.
value
— 속성의 값enumerable
— true
라면, for-in
루프문 또는 Object.keys()
로 속성을 검색할 수 있다. 기본값은 false
이다.writable
— false
라면 속성을 수정할 수 없다. 스트릭트 모드에선 에러를 발생시킨다. 기본값은 false
이다.configurable
— false
라면 객체의 속성들을 열거할 수 없고(non-enumerable), 쓸 수 없으며(non-writable), 제거할 수 없다(non-deletable). 기본값은 false
이다.get
— 속성에 접근하려 할 때 미리 호출되는 함수이다. 기본값은 undefined
이다.set
— 속성에 어떤 값을 설정하려 할 때 미리 호출되는 함수이다. 기본값은 undefined
이다.여러분들이 이게 무슨 말인가 생각하고 있을 것이라 짐작한다. 그러니 몇 가지 간단한 예제를 살펴보자.
const obj = {};
Object.defineProperty(obj, 'a', {
value: 100,
enumerable: false
});
for (const key in obj) {
console.log(key);
}
// undefined
Object.keys(obj);
// []
const obj = {};
Object.defineProperty(obj, 'a', {
value: 100,
writable: false
});
obj.a = 200;
obj.a === 100; // true
(() => {
'use strict';
obj.a = 100;
// 스트릭트 모드에서 TypeError가 발생한다.
})()
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
또는 enumerable
이 true
면, configurable: false
는 무시된다.
“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)하고 서술자를 보면 딱 configurable
만 false
로 변경된 것을 볼 수 있다.
obj.a = 200;
봉인된 객체의 configurable
이 false
가 되어도 기존 속성값은 200
으로 변경되게 된다. 앞서 설명했듯 configurable
을 false
로 설정하면 속성을 쓸 수 없도록 만들어지지만 writable
이 명시적으로 true
면 이는 동작하지 않는다. 그리고 객체를 만들고 새 속성을 설정하게 되면 writable: true
가 기본으로 지정된다.
delete obj.a;
객체를 봉인하면 모든 속성은 구성할 수 없도록 만들어져 삭제할 수 없게(non-deletable) 된다.
obj.b = 500;
객체를 봉인한 후 이 명령은 실패했다. 이유는 Object.seal
또는 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;
객체를 동결하는 것 또한 객체를 확장 불가능한 객체로 만든다.
obj.a = 500
같은 연산을 실행하면 에러가 발생한다.Object.seal
은 속성을 수정할 수 있지만 Object.freeze
는 그렇지 않다.
Object.freeze
와 Object.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.freeze
와 Object.seal
은 확실히 유용한 메서드가 될 것이다. 하지만 중첩된 객체를 동결하기 위해선 deepFreeze
를 사용해야 한다는 점을 반드시 고려해야 한다.