자바스크립트 퀴즈북 리마인드 Day 4: 프로퍼티.
객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.
다음 코드는 무엇을 출력할까요?
const user = {};
Object.defineProperty(user, "name", {
value: "Jingyu",
writable: false,
enumerable: true,
configurable: false,
});
user.name = "SEOJing";
console.log(user.name);
console.log(Object.user
Post Q&A
자바스크립트 퀴즈북 리마인드 Day 4: 프로퍼티 descriptor와 freeze의 경계 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
정답은 strict mode 여부에 따라 대입 실패가 조용히 무시되거나 에러가 될 수 있지만, 핵심 결과는 이렇습니다.
Jingyu
["name"]
true
name은 값만 가진 필드처럼 보입니다. 하지만 실제로는 “쓸 수 있는가”, “열거되는가”, “삭제/재정의할 수 있는가”라는 규칙을 함께 갖고 있습니다. 오늘은 객체를 단순한 key-value map으로만 보면 놓치는 descriptor 감각을 잡습니다.
일상적인 객체 literal은 이렇게 보입니다.
const article = {
title: "Hello",
views: 0,
};
그래서 프로퍼티를 title -> "Hello", views -> 0 정도로만 읽기 쉽습니다. 하지만 JavaScript 엔진 입장에서는 각 프로퍼티가 descriptor를 가집니다. 객체 literal로 만든 일반 프로퍼티는 대략 이런 기본값을 갖습니다.
{
value: "Hello",
writable: true,
enumerable: true,
configurable: true,
}
이 기본값을 모르면 Object.defineProperty로 만든 프로퍼티가 왜 평소와 다르게 동작하는지 헷갈립니다. defineProperty는 기본값이 관대하지 않습니다. 명시하지 않은 boolean 속성은 보통 false입니다.
const config = {};
Object.defineProperty(config, "apiBase", {
value: "/api",
});
Object.keys(config); // []
config.apiBase = "/v2"; // 바뀌지 않음 또는 strict mode에서 TypeError
리뷰에서는 “왜 값이 있는데 JSON/keys/spread에서 안 보이지?” 같은 증상을 descriptor부터 의심할 수 있어야 합니다.
프로퍼티 descriptor는 크게 두 모양입니다.
| 모양 | 핵심 필드 | 읽는 질문 |
|---|---|---|
| data descriptor | value, writable | 값이 무엇이고 다시 쓸 수 있는가? |
| accessor descriptor | get, set | 읽고 쓸 때 어떤 함수가 실행되는가? |
둘 다 공통으로 enumerable, configurable을 가질 수 있습니다.
const product = { price: 1000 };
Object.getOwnPropertyDescriptor(product, "price");
// {
// value: 1000,
// writable: true,
// enumerable: true,
// configurable: true
// }
descriptor를 확인하는 습관은 라이브러리 코드나 프레임워크 내부 객체를 읽을 때 특히 좋습니다. “분명 프로퍼티가 있는데 반복문에서는 왜 안 나오지?”, “대입했는데 왜 값이 안 바뀌지?”를 추측 대신 확인할 수 있습니다.
accessor descriptor는 프로퍼티 접근을 함수 호출처럼 바꿉니다.
const cart = {
items: [1000, 2000],
get total() {
console.log("recompute");
return this.items.reduce((sum, price) => sum + price, 0);
},
};
cart.total; // recompute -> 3000
cart.total; // recompute -> 3000
cart.total은 필드처럼 보이지만 매번 getter가 실행됩니다. 캐시된 값인지, 매번 계산되는 값인지, 사이드 이펙트가 있는지에 따라 리뷰 포인트가 달라집니다.
setter도 마찬가지입니다.
const state = {
_name: "",
set name(next) {
this._name = next.trim();
},
get name() {
return this._name;
},
};
state.name = " SEOJing ";
state.name; // "SEOJing"
폼 상태나 도메인 객체에서 setter가 검증/정규화를 숨기면 읽는 사람은 단순 대입으로 착각할 수 있습니다. 실무에서는 이런 질문을 해야 합니다.
이 프로퍼티 접근은 값 조회인가, 함수 실행인가?
getter/setter가 네트워크 요청, 로그, 상태 변경 같은 부작용을 숨기고 있지는 않은가?
preventExtensions, seal, freeze는 어디까지 막을까객체 전체에 거는 잠금도 세 단계로 나눠서 봅니다.
| API | 새 프로퍼티 추가 | 기존 프로퍼티 삭제/설정 변경 | 기존 값 변경 |
|---|---|---|---|
Object.preventExtensions(obj) | 막음 | 대체로 허용 가능 | 가능 |
Object.seal(obj) | 막음 | 막음 | writable이면 가능 |
여기서 중요한 함정은 “얕은 freeze”입니다.
const state = Object.freeze({
user: { name: "Jingyu" },
});
state.user = { name: "SEOJing" }; // 막힘
state.user.name = "SEOJing"; // 내부 객체는 바뀔 수 있음
freeze는 객체 자신의 프로퍼티 descriptor를 고정합니다. 중첩 객체까지 자동으로 깊게 얼리지 않습니다. React props나 캐시 데이터를 다룰 때 Object.freeze를 보고 “전체 데이터가 완전히 불변이다”라고 판단하면 위험합니다.
as const와 런타임 불변성은 다른 층이다TypeScript에서 자주 보는 as const도 같이 구분해야 합니다.
const routes = {
home: "/",
blog: "/blog",
} as const;
as const는 타입 층에서 literal type과 readonly 성격을 줍니다. 하지만 런타임 객체를 Object.freeze처럼 실제로 잠그는 것은 아닙니다. 컴파일된 JavaScript에서 외부 코드가 같은 객체를 잡고 바꾸는 상황까지 막아주지 않습니다.
그래서 리뷰 질문은 이렇게 나뉩니다.
타입 레벨에서 수정 금지 신호가 필요한가?
런타임에서도 객체 변경을 막아야 하는가?
중첩 객체까지 막아야 하는가?
이 세 질문은 답이 다를 수 있습니다.
defineProperty 기본값을 의도했나Object.defineProperty(obj, "id", { value: "a1" });
이 프로퍼티는 기본적으로 writable/enumerable/configurable이 false입니다. 의도한 보안/숨김 장치라면 좋지만, 단순 필드 추가 의도였다면 버그입니다.
const clone = { ...obj };
spread는 enumerable own property를 봅니다. non-enumerable 필드는 빠집니다. 직렬화, 로그, diff, 폼 payload에서 값이 사라진다면 descriptor를 봐야 합니다.
const total = cart.total;
필드처럼 보이지만 계산이 무거울 수 있습니다. 렌더링 중 반복 접근되는 getter라면 성능/부작용 체크가 필요합니다.
Object.freeze(options);
중첩 객체가 있다면 여전히 바뀔 수 있습니다. 정말 깊은 불변성이 필요하면 데이터 생성 경계, deep freeze, immutable update 패턴, 타입 readonly를 함께 봐야 합니다.
객체를 읽을 때 “프로퍼티가 있다/없다”에서 멈추지 말고 descriptor를 함께 봐야 합니다.
value와 writable을 가진다.get/set으로 읽기와 쓰기를 함수 실행으로 바꾼다.enumerable은 Object.keys, spread, 직렬화 감각에 영향을 준다.configurable은 삭제와 descriptor 재정의 가능성을 좌우한다.preventExtensions, seal, freeze는 객체 전체 규칙이지만 deep freeze는 아니다.as const는 타입 층의 신호이지 런타임 잠금이 아니다.이 감각이 있으면 라이브러리 객체, 브라우저 API, 프레임워크 내부 상태를 볼 때 “왜 평범한 객체처럼 안 움직이지?”라는 질문에 훨씬 빨리 답할 수 있습니다.
Object.defineProperty, property descriptors, Object.freeze, Object.seal, Object.preventExtensions객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
Object.freeze(obj)| 막음 |
| 막음 |
| 막음 |