notes
16장. 프로퍼티 어트리뷰트
모던 JS Deep Dive — 프로퍼티 어트리뷰트와 객체 속성 제어 핵심 정리.
참고: 이웅모 《모던 자바스크립트 Deep Dive》 16장 · [MDN Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) · [MDN Object.freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
객체의 키가 “값만”인 줄 알았다. 각 키에는 value(또는 get/set)와 Writable · Enumerable · Configurable 플래그가 붙는다.
Object.freeze를 걸었는데 nested가 바뀌어 멘붕한 적이 있다 — freeze는 얕다. 이 장은 “키의 규칙”과 “객체를 얼마나 잠글지”의 지도다.
flowchart LR
K[프로퍼티 키] --> D[디스크립터]
D --> DATA[데이터: value/writable]
D --> ACC[접근자: get/set]
D --> E[enumerable]
D --> C[configurable]
LOCK[preventExtensions / seal / freeze] --> D
16.1 내부 슬롯 · 내부 메서드
스펙이 엔진 구현을 설명할 때 쓰는 [[...]] 의사 프로퍼티/메서드다. 예: [[Prototype]], [[Get]], [[Set]]. 코드에서 직접 obj.[[Prototype]]처럼 접근하지는 못하고, Object.getPrototypeOf 같은 통로로 간접 접근한다. “엔진이 몰래 들고 있는 칸”이라고 기억한다.
16.2 프로퍼티 어트리뷰트와 디스크립터
엔진이 프로퍼티를 만들 때 상태(값, 쓰기 가능, 열거 가능, 재정의 가능)를 붙인다. 직접 슬롯을 못 만지지만 Object.getOwnPropertyDescriptor로 디스크립터 객체를 볼 수 있다.
const person = { name: 'Lee' };
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { value: 'Lee', writable: true, enumerable: true, configurable: true }
리터럴로 만든 프로퍼티는 보통 플래그가 전부 true라 “그냥 객체”처럼 느껴진다. defineProperty로 만들면 생략한 플래그 기본값이 false라 체감이 다르다.
16.3 데이터 프로퍼티 vs 접근자 프로퍼티
16.3.1 데이터 프로퍼티
16.3.2 접근자 프로퍼티
자체 value 대신 get/set 함수. 창구 직원이다.
const person = {
first: 'Min',
last: 'Kim',
get fullName() {
return `${this.first} ${this.last}`;
},
set fullName(name) {
[this.first, this.last] = name.split(' ');
},
};
console.log(person.fullName); // Min Kim
person.fullName = 'Lee Soo';
console.log(person.first); // Lee
WHY: 계산·검증·동기화를 “프로퍼티처럼” 노출할 때. 남용하면 숨은 부수효과가 늘어난다.
16.4 프로퍼티 정의
Object.defineProperty / defineProperties로 플래그를 명시한다.
const person = {};
Object.defineProperty(person, 'age', {
value: 20,
writable: true,
enumerable: true,
configurable: true,
});
Object.defineProperty(person, 'secret', {
value: 42,
// writable/enumerable/configurable 생략 → false
});
console.log(Object.keys(person)); // ['age'] — secret은 enumerable false
// person.secret = 1; // writable false면 슬롭에서 무시, strict에서 TypeError
함정: “플래그 안 쓰면 리터럴과 같다”가 아님. defineProperty 기본은 닫힌 쪽(false).
Object.defineProperties(person, {
id: { value: 1, enumerable: true },
label: {
get() { return `id=${this.id}`; },
enumerable: true,
},
});
16.5 객체 변경 방지
강도만 다른 자물쇠들. 모두 얕다(직속 프로퍼티 기준).
확인: isExtensible / isSealed / isFrozen.
16.5.1 확장 금지
const o = { a: 1 };
Object.preventExtensions(o);
// o.b = 2; // 실패 (strict TypeError)
o.a = 10; // OK
16.5.2 밀봉
추가·삭제·어트리뷰트 재정의 금지. 값 변경은 가능(writable인 한).
const o = { a: 1 };
Object.seal(o);
o.a = 2; // OK
// delete o.a; // 실패
사실상 각 프로퍼티 configurable=false + 확장 금지.
16.5.3 동결
읽기 위주. writable까지 false로 내리는 효과.
const o = { a: 1 };
Object.freeze(o);
// o.a = 2; // 실패
16.5.4 불변 객체 — 얕음에 주의
const user = {
name: '민섭',
nest: { a: 1 },
};
Object.freeze(user);
// user.name = 'Kim'; // TypeError (strict)
user.nest.a = 2; // OK — nest는 동결 안 됨
console.log(Object.isFrozen(user.nest)); // false
깊은 동결은 재귀 freeze 유틸이나 불변 라이브러리(Immer 등) 영역이다.
function deepFreeze(obj) {
Object.freeze(obj);
for (const key of Object.keys(obj)) {
const v = obj[key];
if (v && typeof v === 'object' && !Object.isFrozen(v)) {
deepFreeze(v);
}
}
return obj;
}
WHY: 실무에선 “전부 deepFreeze”보다 불변 업데이트 습관(스프레드로 새 객체)이 더 흔하다. freeze는 실수 방지용 가드.
예제 — 왜 이렇게 쳐 봤냐
// API 응답을 실수로 변이하지 않게
function hold(response) {
return Object.freeze({ ...response });
}
// 열거만 숨긴 메타 필드
const entity = { id: 1, name: 'x' };
Object.defineProperty(entity, '_internal', {
value: { cached: true },
enumerable: false,
});
console.log(Object.keys(entity)); // ['id','name']
// 접근자로 검증
const account = {
_balance: 0,
get balance() { return this._balance; },
set balance(n) {
if (n < 0) throw new Error('negative');
this._balance = n;
},
};
헷갈리던 장면 / 함정
- defineProperty 기본 false ↔ 리터럴 기본 true
- freeze했는데 nested가 바뀜
- seal/freeze 후에도
writable:true로 남은 프로퍼티가 있나 확인 (getOwnPropertyDescriptors) - for…in은 프로토타입 체인 enumerable까지 (19장)
- getter에 무거운 로직 → 프로퍼티 접근이 비싼 함수 호출
질문 → 자답
Q. freeze가 const와 다르냐? A. const는 바인딩 재할당 금지(15). freeze는 객체 프로퍼티 변이 제한. 축이 다르다.
Q. 언제 defineProperty? A. 열거 숨김, 읽기 전용 필드, 접근자, 라이브러리 객체 메타. 일상 데이터는 리터럴 + 불변 업데이트가 먼저.
Q. preventExtensions만으로 충분? A. “새 키만 막기”. 값 변경·삭제를 막으려면 seal/freeze.
웹 보강 메모
MDN getOwnPropertyDescriptors, fromEntries와 함께 “디스크립터를 복사해 복제” 패턴을 훑었다. 상태 관리 라이브러리가 불변을 강제하는 방식과 freeze 가드를 비교해 메모했다.
실무로 바로
- 설정 객체: freeze(얕음) 또는 as const 마인드
- 숨은 필드: enumerable:false 또는 WeakMap/#private
- 디버그:
Object.getOwnPropertyDescriptors(obj) - nested 불변: 구조적 공유 / Immer / deepFreeze 선택
여기서 잠깐 💁♂️
15의 const(바인딩)와 16의 freeze(프로퍼티)를 한 축으로 섞지 말 것. 다음은 생성자(17)·프로토타입(19)에서 “키가 어디에 붙는지”로 이어진다.