모던 자바스크립트 딥다이브 16장 - 프로퍼티 어트리뷰트

 

1. 내부 슬롯과 내부 메서드

 

자바스크립트의 객체는 단순한 키-값 쌍이 아닌, 더 복잡한 내부 구조를 가지고 있다. 이러한 내부 구조를 이해하면 객체를 더 효과적으로 다룰 수 있다.

 

자바스크립트 엔진은 객체를 관리하기 위해 내부 슬롯과 내부 메서드를 사용한다.

 

1.1 내부 슬롯

내부 슬롯은 객체의 숨겨진 속성으로, ECMAScript 명세에서 [[...]] 형태로 표기된다.

직접 접근은 불가능하지만 간접적인 방법으로 접근할 수 있다.

const o = {}; // 직접 접근 불가능 o.[[Prototype]]; // ❌ 오류 발생 // 
간접적으로 접근 가능 console.log(o.__proto__); // ✅ 가능
 

1.2 내부 메서드

내부 메서드는 객체의 기본적인 동작을 정의하는 메서드이다.

직접 호출할 수는 없지만 Object.getPrototypeOf() 같은 표준 API를 통해 사용할 수 있다.


2. 프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

모든 프로퍼티는 생성될 때 프로퍼티 어트리뷰트라는 기본 값이 자동으로 정의된다.

 

프로퍼티 어트리뷰트의 4가지 특성

  • [[Value]]: 프로퍼티의 실제 값
  • [[Writable]]: 값의 변경 가능 여부를 지정
  • [[Enumerable]]: 프로퍼티의 열거 가능 여부를 지정
  • [[Configurable]]: 프로퍼티의 삭제 및 어트리뷰트 변경 가능 여부를 지정
const person = { name: 'Kim' };

// 프로퍼티의 어트리뷰트 확인
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// 결과: 
// {
//   value: "Kim",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }
 

3. 데이터 프로퍼티와 접근자 프로퍼티

객체의 프로퍼티는 두 종류로 구분된다.

 

3-1) 데이터 프로퍼티

일반적인 프로퍼티로, 위에서 설명한 4가지 어트리뷰트를 모두 가진다.

const person = { name: 'Kim' };

// 프로퍼티 어트리뷰트 정보 확인
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// 결과:
// {
//   value: "Kim",
//   writable: true,     // 값 변경 가능
//   enumerable: true,   // 열거 가능
//   configurable: true  // 삭제 및 어트리뷰트 변경 가능
// }
 

이러한 프로퍼티 어트리뷰트를 이해하고 활용하면, 객체의 프로퍼티를 더 세밀하게 제어할 수 있다.

예를 들어 writable: false로 설정하면 값을 변경할 수 없는 읽기 전용 프로퍼티를 만들 수 있다.


3-2) 접근자 프로퍼티

 

접근자 프로퍼티는 실제 값을 저장하지 않고, 다른 데이터 프로퍼티의 값을 가져오거나 저장하는 특별한 함수로 구성된다.

 

접근자 프로퍼티의 특성

[[Get]] (getter): 값을 읽을 때 자동으로 호출되는 함수

[[Set]] (setter): 값을 저장할 때 자동으로 호출되는 함수

[[Enumerable]]: 반복문에서 나열 가능한지 여부

[[Configurable]]: 프로퍼티 삭제나 속성 변경이 가능한지 여부

const person = {
  firstName: 'Ungmo',
  lastName: 'Lee',

  // 값을 읽을 때 사용하는 getter
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  // 값을 저장할 때 사용하는 setter
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(' ');
  }
};

// getter 사용하기
console.log(person.fullName); // Ungmo Lee

// setter 사용하기
person.fullName = 'Heegun Lee';
console.log(person.firstName); // Heegun
console.log(person.lastName); // Lee
 

 

 

접근자 프로퍼티 정보 확인하기

// 일반 프로퍼티 정보
console.log(Object.getOwnPropertyDescriptor(person, 'firstName'));
// {
//   value: "Heegun",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

// 접근자 프로퍼티 정보
console.log(Object.getOwnPropertyDescriptor(person, 'fullName'));
// {
//   get: f,
//   set: f,
//   enumerable: true,
//   configurable: true
// }
 

 

동작 과정

1. 프로퍼티 이름이 올바른지 확인

2. 객체에서 해당 프로퍼티 찾기

3. 찾은 프로퍼티가 일반 프로퍼티인지 접근자 프로퍼티인지 확인

4. 접근자 프로퍼티면 상황에 따라 getter나 setter 함수 실행

 

이렇게 접근자 프로퍼티를 사용하면 객체의 프로퍼티 값을 더 유연하게 다룰 수 있다.


프로토타입이란?

 

자바스크립트의 모든 객체는 다른 객체를 '상속'받을 수 있다. 이때 상속해주는 객체를 프로토타입이라고 한다.

 

쉽게 말해서, 프로토타입은 객체들이 공통으로 사용하는 속성과 메서드를 보관하는 곳이다

// 동물 생성자 함수
function Animal(name) {
this.name = name;
}

// 모든 동물이 공통으로 사용할 메서드를 프로토타입에 저장
Animal.prototype.speak = function() {
console.log(`${this.name}이 소리를 냅니다`);
};

// 새로운 동물 만들기
const dog = new Animal('멍멍');
const cat = new Animal('야옹');

// 두 동물은 같은 speak 메서드를 공유해서 사용
dog.speak(); // "멍멍이 소리를 냅니다"
cat.speak(); // "야옹이 소리를 냅니다"
 

4. 프로퍼티 정의

 

프로퍼티를 정의할 때는 Object.defineProperty()와 Object.defineProperties() 메서드를 사용해 더 자세한 설정을 할 수 있다.

 

4.1 Object.defineProperty() 사용하기

const person = {};

// 일반적인 프로퍼티 정의
Object.defineProperty(person, 'name', {
  value: '김철수',
  writable: true,      // 값 변경 가능
  enumerable: true,    // 열거 가능
  configurable: true   // 삭제/재정의 가능
});

// 읽기 전용 프로퍼티 정의
Object.defineProperty(person, 'age', {
  value: 20
  // 나머지 속성을 작성하지 않으면 모두 false가 된다
});
 

 

4.2 프로퍼티 설정 확인하기

// 프로퍼티의 상태 확인
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "김철수", writable: true, enumerable: true, configurable: true}
 

 

4.3 프로퍼티 제한사항

  • writable: false: 값을 변경할 수 없다
  • enumerable: false: for...in 등에서 보이지 않는다
  • configurable: false: 삭제나 재정의가 불가능하다

 

4.4 여러 프로퍼티 한번에 정의하기

const person = {};

Object.defineProperties(person, {
  name: {
    value: '김철수',
    writable: true
  },
  age: {
    value: 20,
    writable: false // 읽기 전용
  }
});
 

 

*

1. 프로퍼티 정의 시 속성을 지정하지 않으면 모두 false가 된다

2. writable, enumerable, configurable이 false면 각각 값 변경, 열거, 삭제가 불가능하다

3. Object.defineProperties()로 여러 프로퍼티를 한번에 정의할 수 있다


5. 객체 변경 방지

 

자바스크립트에서는 객체를 기본적으로 자유롭게 수정할 수 있다. 즉,

  • 새로운 프로퍼티를 추가할 수 있고,
  • 기존 프로퍼티를 삭제할 수도 있으며,
  • 프로퍼티 값을 변경할 수도 있다.

 

하지만 객체의 구조를 보호하려면 객체 변경 방지 기능을 사용할 수 있다.

자바스크립트는 객체 변경을 제한하는 다양한 방법을 제공하며,

변경을 방지하는 강도에 따라 preventExtensions, seal, freeze 세 가지 메서드가 있다.

 

1. Object.preventExtensions() - 확장 금지

가장 기본적인 제한이다. 새로운 속성을 추가하는 것만 막아준다.

const user = { id: 1, name: '김철수' };
Object.preventExtensions(user);

// 새로운 속성 추가만 안된다
user.age = 20;        // 추가 안됨
delete user.name;     // 삭제 됨
user.id = 2;         // 수정 됨

console.log(user);    // { id: 2 }
 

2. Object.seal() - 밀봉

중간 단계의 제한이다. 추가와 삭제를 막아주고, 수정만 가능하다.

const user = { id: 1, name: '김철수' };
Object.seal(user);

// 수정만 가능하다
user.age = 20;        // 추가 안됨
delete user.name;     // 삭제 안됨
user.id = 2;         // 수정 됨

console.log(user);    // { id: 2, name: '김철수' }
 

3. Object.freeze() - 완전 동결

가장 강력한 제한이다. 모든 변경이 불가능하다.

const user = { id: 1, name: '김철수' };
Object.freeze(user);

// 아무것도 변경할 수 없다
user.age = 20;        // 추가 안됨
delete user.name;     // 삭제 안됨
user.id = 2;         // 수정 안됨

console.log(user);    // { id: 1, name: '김철수' }
 

 

중첩 객체의 문제점

객체 안에 있는 다른 객체는 보호되지 않는다는 문제가 있는데

const user = {
    id: 1,
    name: '김철수',
    info: {
        age: 20,
        city: '서울'
    }
};

Object.freeze(user);

// 바깥 객체는 변경 불가능
user.gender = '남';     // 실패
user.name = '김영희';   // 실패

// 안쪽 객체는 자유롭게 변경 가능
user.info.age = 30;     // 성공
user.info.city = '부산'; // 성공

console.log(user.info); // { age: 30, city: '부산' }
 
 

해결방법으로 깊은 동결 함수를 사용하면 된다.

모든 단계의 객체를 동결하려면 재귀 함수(자기 자신을 다시 호출)를 사용해야 한다.

function 깊은동결(대상) {
    if (대상 && typeof 대상 === 'object' && !Object.isFrozen(대상)) {
        Object.freeze(대상);
        Object.keys(대상).forEach(key => 깊은동결(대상[key]));
    }
    return 대상;
}

const user = {
    id: 1,
    name: '김철수',
    info: {
        age: 20,
        city: '서울'
    }
};

깊은동결(user);

// 이제 안쪽 객체도 변경 불가능
user.info.age = 30;      // 실패
console.log(user.info);  // { age: 20, city: '서울' }
 

 

1. preventExtensions: 추가만 안됨

2. seal: 추가, 삭제 안됨 (수정 가능)

3. freeze: 추가, 삭제, 수정 모두 안됨

 

중첩 객체까지 보호하려면 깊은동결 함수를 사용해야 한다.