본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

속성 설명자(Property Descriptor)

작성 2026. 10. 21.추가
목차6개 항목
출처와 참고

web.dev의 「Learn JavaScript」 강좌의 「Property descriptors」 모듈과 javascript.info(모던 JavaScript 튜토리얼)의 「프로퍼티 플래그와 설명자」, 「프로퍼티 getter와 setter」 글을 바탕으로, MDN으로 확인해 재구성한 노트다. 「Frontend_TIL: 프로퍼티 어트리뷰트」을 바탕으로 보강했다.

속성 설명자란

객체의 속성은 "키 → 값"이 전부가 아니다. 각 속성에는 그 속성을 바꿀 수 있는지, 순회에 나오는지, 지우거나 재설정할 수 있는지를 정하는 숨은 속성(attribute)이 붙어 있다. 이 묶음을 속성 설명자(property descriptor) 라고 한다. 명세에서는 [[Writable]]처럼 이중 대괄호로 쓰는데, 코드로 직접 접근하는 이름이 아니라 내부 슬롯이라는 표시다([[Prototype]]과 같은 표기).

설명자는 두 종류다. 한 속성은 둘 중 하나만 될 수 있다.

종류가지는 설정뜻
데이터 속성(data)value, writable값을 그대로 저장한다
접근자 속성(accessor)get, set읽고 쓸 때 함수가 실행된다. 값 자체는 저장하지 않는다
공통enumerable, configurable순회 노출 여부, 삭제·재설정 가능 여부
설정뜻리터럴·대입으로 만든 속성defineProperty 기본값
value속성의 값대입한 값undefined
writable값을 바꿀 수 있나truefalse
get / set읽기·쓰기 때 부를 함수없음undefined
enumerablefor...in·Object.keys에 나오나truefalse
configurable지우거나 설정을 바꿀 수 있나truefalse

기본값이 정반대라는 점이 핵심 함정이다. { a: 1 }이나 obj.a = 1로 만든 속성은 세 플래그가 모두 true지만, Object.defineProperty로 만들면서 생략한 플래그는 모두 false가 된다.

읽고 정의하기

const user = { name: 'river' }
Object.getOwnPropertyDescriptor(user, 'name')
// { value: 'river', writable: true, enumerable: true, configurable: true }
 
Object.defineProperty(user, 'id', { value: 42 }) // 나머지는 전부 false
user.id = 7
user.id                    // 42: sloppy mode에서는 조용히 무시된다
Object.keys(user)          // ['name'], enumerable: false라 안 나온다
delete user.id             // false, configurable: false (strict mode에서는 TypeError)
API하는 일
Object.getOwnPropertyDescriptor(obj, key)속성 하나의 설명자
Object.getOwnPropertyDescriptors(obj)모든 자기 속성의 설명자. Symbol 키와 열거 불가 속성까지 포함한다. getter·setter까지 보존해 복사할 때 Object.defineProperties({}, Object.getOwnPropertyDescriptors(src))
Object.defineProperty(obj, key, desc)속성 하나를 만들거나 고친다
Object.defineProperties(obj, { key: desc, … })여러 개를 한 번에
Object.create(proto, { key: desc, … })프로토타입을 정해 새 객체를 만들면서, 두 번째 인자로 자기 속성을 설명자 형식으로 정의한다

writable: false인 속성에 쓰면 sloppy mode에서는 조용히 실패하고 strict mode에서는 TypeError 가 난다(strict mode). 모듈 코드는 strict이므로 대개 에러로 드러난다.

configurable: false여도 writable: true면 값은 바꿀 수 있다. configurable은 값이 아니라 속성의 "설정"을 잠근다. 한 번 false로 만들면 defineProperty로도 되돌릴 수 없다.

configurable: false일 때가능 여부
delete obj.key불가
configurable·enumerable 바꾸기불가
writable을 false → true불가
writable을 true → false가능(더 잠그는 방향만 허용)
접근자의 get·set 교체불가

writable: false와 configurable: false를 함께 주면 영원히 바뀌지 않는 상수 속성이 된다. 내장 Math.PI가 그렇다. 세 플래그가 모두 false라 Math.PI = 3은 무시(strict mode에서는 TypeError)되고 지울 수도 없다.

접근자 속성: getter와 setter

접근자 속성은 속성처럼 읽고 쓰지만 실제로는 함수가 돈다. 객체 리터럴과 class에서는 get·set 키워드로 쓰고, 그 결과가 접근자 설명자다.

const temp = {
  celsius: 20,
  get fahrenheit() { return this.celsius * 1.8 + 32 },
  set fahrenheit(f) { this.celsius = (f - 32) / 1.8 },
}
temp.fahrenheit = 212
temp.celsius   // 100
Object.getOwnPropertyDescriptor(temp, 'fahrenheit')
// { get: ƒ, set: ƒ, enumerable: true, configurable: true }: value·writable이 없다
  • getter만 있고 setter가 없으면 대입이 무시된다(strict mode에서는 TypeError).
  • 한 설명자에 value와 get을 함께 주면 TypeError다.
  • class의 get·set은 인스턴스가 아니라 prototype에 정의된다(class 문법).
  • Object.defineProperty(obj, 'x', { get() {…}, set(v) {…} })로도 만든다. 이때 enumerable을 주지 않으면 false라서 for...in에 나오지 않는다.

접근자는 두 가지 용도로 쓴다.

용도방법
값 검증·통제실제 값은 _name(관례상 내부용) 또는 #name(언어가 막는 private)에 두고, setter에서 검사한 뒤 저장한다
호환성 유지저장 형식이 바뀌어도 옛 속성 이름을 계산된 getter로 남긴다. age를 저장하다 birthday로 바꿨다면 get age()가 생일로 나이를 계산해, user.age를 읽는 기존 코드를 고치지 않아도 된다
function User(name, birthday) {
  this.name = name
  this.birthday = birthday
  Object.defineProperty(this, 'age', {
    get() { return new Date().getFullYear() - this.birthday.getFullYear() },
  })
}

enumerable이 정하는 것

열거 불가능한 속성은 "있지만 순회에는 안 나오는" 속성이다. 내장 메서드(Object.prototype.toString, 배열의 length, class 메서드)가 모두 enumerable: false라서 for...in에 끼어들지 않는다.

기능열거 불가 속성상속 속성Symbol 키
for...in제외포함(열거 가능한 것만)제외
Object.keys·values·entries제외제외제외
{ ...obj }, Object.assign제외제외포함
JSON.stringify제외제외제외
Object.getOwnPropertyNames포함제외제외
Reflect.ownKeys포함제외포함
key in obj포함포함포함

그래서 spread로 만든 복사본은 프로토타입과 열거 불가 속성을 잃고, getter는 그 순간의 값으로 바뀐다. 프로토타입·플래그·접근자까지 그대로인 얕은 복사본은 Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))로 만든다. 자기 속성인지는 Object.hasOwn(obj, key)로 판별한다(프로토타입과 상속).

객체 전체를 잠그기

속성 하나가 아니라 객체 단위로 플래그를 일괄 적용하는 메서드도 있다. 모두 얕게 동작한다. 중첩 객체는 따로 잠가야 한다.

메서드속성 추가속성 삭제값 변경판별
Object.preventExtensions(obj)불가가능가능Object.isExtensible
Object.seal(obj)불가불가(모든 속성 configurable: false)가능Object.isSealed
Object.freeze(obj)불가불가불가(모든 데이터 속성 writable: false)Object.isFrozen

중첩 객체까지 모두 얼린 불변 객체가 필요하면 객체인 속성을 재귀로 따라가며 freeze한다.

function deepFreeze(target) {
  if (target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target)
    Object.values(target).forEach(deepFreeze)
  }
  return target
}

const와 Object.freeze의 차이는 변수와 데이터 타입의 「const와 Object.freeze」에 있다.

정리

  • 속성마다 writable·enumerable·configurable(또는 get·set) 설정이 숨어 있다.
  • 리터럴로 만든 속성은 모두 true, defineProperty로 만들면서 생략한 플래그는 모두 false다.
  • 쓰기 실패는 sloppy mode에서 조용히 무시되고 strict mode에서 TypeError가 된다.
  • enumerable: false는 for...in·Object.keys·spread·JSON에서 빠진다. 내장 메서드가 순회에 안 나오는 이유다.
  • 객체 단위 잠금은 preventExtensions < seal < freeze 순으로 강하고, 모두 얕다.

관련 노트: 프로토타입과 상속, class 문법, 배열·객체·문자열 다루기, strict mode, Symbol, Proxy와 Reflect

원본 기록8

댓글

GitHub 계정으로 댓글을 남길 수 있어요.