[JavaScript Deep Dive] 16. 프로퍼티 어트리뷰트

소정·2024년 1월 13일
1
post-thumbnail

나름 익숙했던 변수, 스코프, 함수 이런 내용을 다루다가 .. !
갑자기 등장해주신 프로퍼티 어트리뷰트 ,, ,, ? (너 뉘기야)
아직은 어색한 프로퍼티 어트리뷰트랑 친해지러 고고씽 ~


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

프로퍼티 어트리뷰트도 어색한데 내부 슬롯내부 메서드는 또 뭘까 ? (너 뉘기야2)
프로퍼티 어트리뷰트를 이해하기 위해선, 내부 슬롯과 내부 메서드를 알아야 한다.

📌 내부 슬롯과 내부 메서드?

  • 내부 슬롯 객체의 내부 저장 공간 | 의사(가상) 프로퍼티 (pseudo property)
  • 내부 메서드 객체가 할 수 있는 특별한 동작 | 의사(가상) 메서드 (pseudo method)

자바스크립트의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용한다.
이중 대괄호로 감싼 이름 대부분이 해당한다. [[ ...]]

내부 슬롯과 내부 메서드는 자바스크립트 엔진에서 실제로 동작하지만, 외부로 공개된 객체의 프로퍼티는 아니다.
자바스크립트 엔진의 내부 로직으로, 직접 접근하거나 호출할 수 없다.

📌 내부 슬롯과 내부 메서드의 예외적인 접근 방법

하지만 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하기도 한다.

  • 모든 객체가 [[prototype]]라는 내부 슬롯을 가짐
  • __proto__를 통해 간접적으로 내부 슬롯에 접근 가능

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

아직 어트리뷰트랑 어색한데 냅다 디스크립터 객체까지 등장했다.
자 당황하지 말고..! 가보자고 !

📌 익숙하지 않은 단어, 먼저 정리하고 가기

  • 프로퍼티 어트리뷰트 객체의 특징 (= 내부 슬롯)
  • 프로퍼티 디스크립터 객체 객체의 특징을 한 눈에 볼 수 있는 설명서

1. 프로퍼티 어트리뷰트

프로퍼티 어트리뷰트는 자바스크립트 객체와 관련있다.
프로퍼티 어트리뷰트객체의 동작과 특성을 제어하는데, 마치 물건의 특징이나 성질로 비유할 수 있다.

  • 프로퍼티 어트리뷰트 자바스크립트 엔진이 프로퍼티를 생성할 때, 기본값으로 자동으로 정의된 프로퍼티의 상태
  • 프로퍼티 상태 (데이터 프로퍼티)
    • 프로퍼티의 값 [[Value]] (객체 { 키:값 } 의 '값' 맞음)
    • 값의 갱신 가능 여부 [[Writable]]
    • 열거 가능 여부 [[Enumerable]]
    • 재정의 가능 여부 [[Configurable]]
      -> 값을 제외한 3가지의 상태가 눈에 보이진 않지만, 자동으로 정의됨
const person = {
  name: 'Lee'
}

// [[Value]] = "Lee"
// [[Writable]] = true
// [[Enumerable]] = true
// [[Configurable]] = true
// (해당 상태에 접근하는 방법은 아래에 나옴)

이렇게 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트는, 내부 슬롯이라서 직접 접근할 수 없다.
접근을 위해선, 프로퍼티 디스크립터 객체가 필요하다.

2. 프로퍼티 디스크립터 객체

내부슬롯인 프로퍼티 어트리뷰트에 접근하기 위해선 Object.getOwnPropertyDescriptor 메서드를 사용한다.
Object.getOwnPropertyDescriptor 메서드는 프로퍼티 디스크립터 객체를 반환한다.

프로퍼티 디스크립터 객체프로퍼티 어트리뷰트 정보를 제공한다.
프로퍼티 어트리뷰트가 물건의 특징이라면 프로퍼티 디스크립터 객체물건의 설명서로 비유할 수 있다.
만약 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터를 요구하면, undefined가 반환된다.

📌 Object.getOwnPropertyDescriptor

  • 첫 번째 매개변수 객체의 참조를 전달
  • 두 번째 매개변수 프로퍼티 키를 문자열로 전달

📌 Object.getOwnPropertyDescriptors

  • 매개변수 객체의 참조를 전달
  • ES8에서 도입되었고 두 번째 매개변수는 없음
const person = {
  name: 'Lee'
}

// 1️⃣ Object.getOwnPropertyDescriptor
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
// { value: "Lee", writable: true, enumerable: true, configurable: true }

person.age = 20; // 프로퍼티 동적 생성

// 2️⃣ Object.getOwnPropertyDescriptors
console.log(Object.getOwnPropertyDescriptors(person))
/* { 
name: "Lee", writable: true, enumerable: true, configurable: true }
age: "20", writable: true, enumerable: true, configurable: true }
} */

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

프로퍼티 어트리뷰트데이터 프로퍼티접근자 프로퍼티로 구분할 수 있다.

📌 익숙하지 않은 단어, 먼저 정리하고 가기

  • 데이터 프로퍼티 키, 값으로 구성된 일반적인 프로퍼티 (지금까지 살펴본 프로퍼티가 해당됨 )
    객체를 물건으로 비유하자면, 물건의 실제 내용물과 같음 (value, writable, enumerable, configurable)
  • 접근자 프로퍼티 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티
    객체를 물건으로 비유하자면, 물건의 특별한 동작이나 기능과 같음 (get, set, enumerable, configurable)

1. 데이터 프로퍼티

데이터 프로퍼티는 아래와 같이 키와 값으로 구성된 프로퍼티 어트리뷰트를 갖는다.
프로퍼티 어트리뷰트는 자바스크립트 엔진이 프로퍼티를 생성할 때, 기본값으로 자동 정의된다.

📌 [[Value]]

  • 프로퍼티 어트리뷰트 [[Value]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 vlaue
  • 설명
    • 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값
    • 프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]] 값 재할당
      프로퍼티가 없다면, 프로퍼티를 동적 생성하고 생성된 프로퍼티의 값을 저장함
    • 객체가 사과라면, '빨간색', '달콤한 맛' 등 실제 값이 해당됨
  • 기본값 프로퍼티가 생성될 때 프로퍼티 값으로 초기화 됨

📌 [[Writable]]

  • 프로퍼티 어트리뷰트 [[Writable]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 writable
  • 설명
    • 프로퍼티 값의 변경 가능 여부를 나타내고 불리언 값을 가짐
    • false인 경우, [[Value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티
    • 객체가 사과라면, 사과의 맛이나 색의 변경 여부에 해당 됨
  • 기본값 프로퍼티가 생성될 때 true로 초기화 됨

📌 [[Enumerable]]

  • 프로퍼티 어트리뷰트 [[Enumerable]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 enumerable
  • 설명
    • 프로퍼티의 열거 가능 여부를 나타내고 불리언 값을 가짐
    • false인 경우, for...in문이나 Object.keys 메서드 등으로 열거 불가
    • 객체가 방 안에 있는 물건이라면, 물건을 세어볼 수 있는지의 여부에 해당 됨
  • 기본값 프로퍼티가 생성될 때 true로 초기화 됨

📌 [[Configurable]]

  • 프로퍼티 어트리뷰트 [[Configurable]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 configurable
  • 설명
    • 프로퍼티의 재정의 가능 여부를 나타내고 불리언 값을 가짐
    • false인 경우, 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값 변경 금지
    • true인 경우, [[Value]] 변경과 [[Writable]]을 false로 변경 가능
    • 객체가 물건이라면, 물건의 특성을 바꿀 수 있는지에 해당 됨
  • 기본값 프로퍼티가 생성될 때 true로 초기화 됨

2. 접근자 프로퍼티

  • 접근자 프로퍼티는 자체적인 값을 갖지 않는다.
  • 접근자 프로퍼티는 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성되어 있는 프로퍼티다.

📌 [[Get]]

  • 프로퍼티 어트리뷰트 [[Get]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 get
  • 설명
    • 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출됨
    • 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 getter 함수가 호출되어 프로퍼티 값으로 반환됨

📌 [[Set]]

  • 프로퍼티 어트리뷰트 [[Set]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 set
  • 설명
    • 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출됨
    • 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 setter 함수가 호출되어 프로퍼티 값으로 저장됨

📌 [[Enumerable]]

  • 프로퍼티 어트리뷰트 [[Enumerable]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 enumerable
  • 설명
    • 데이터 프로퍼티의 [[Enumerable]]과 같음

📌 [[Configurable]]

  • 프로퍼티 어트리뷰트 [[Configurable]]
  • 프로퍼티 디스크립터 객체의 프로퍼티 configurable
  • 설명
    • 데이터 프로퍼티의 [[Configurable]]과 같음

3. 예시

데이터 프로퍼티접근자 프로퍼티를 구분하기 위해 예시를 확인해보자.

const person = {
  // 데이터 프로퍼티
  firstName = 'sozzang';
  lastName = 'Lee';
  
  // 접근자 프로퍼티 : fullName
  get fullName(){ // 읽기용 getter 함수
    return `${this.firstName} ${this.lastName}`
  }
  set fullName(name){ // 저장용 setter 함수
    [this.firstName, this.lastName] = name.split(' ')
  }
}

// 1️⃣ 데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName + ' ' + person.lastName); // sozzang Lee

// 2️⃣ 접근자 프로퍼티를 통한 프로퍼티 값의 저장 (setter)
person.fullName = 'jjajang Lee';
console.log(person); // { firstName: 'jjajang', lastName: 'Lee' }

// 3️⃣ 접근자 프로퍼티를 통한 프로퍼티 값의 참조 (getter)
console.log(person.fullName); // 'jjajang Lee'

// 4️⃣ 데이터 프로퍼티인 firstName의 프로퍼티 어트리뷰트
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor); 
// { value: "jjajang", writable: ture, enumerable: ture, configurable: true }

// 5️⃣ 접근자 프로퍼티인 fullName의 프로퍼티 어트리뷰트
descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor); 
// { get: f, set: f, enumerable: ture, configurable: true }

위의 예제에서 데이터 프로퍼티firstNamelastName이다.
메서드 앞에 get과 set이 붙어 있는 메서드가 바로 getter, setter 함수이고
getter, setter 함수의 이름인 fullName접근자 프로퍼티다.

접근자 프로퍼티 fullName으로 프로퍼티 값에 접근하면
내부적으로 [[Get]] 내부 메서드가 호출되어 다음과 같이 동작한다.

  1. 프로퍼티 키가 유효한지 확인 (유효한 프로퍼티 키 - 문자열 또는 심벌)
  2. 프로토타입 체인에서 프로퍼티 검색 (person 객체에 fullName 프로퍼티 존재함)
  3. 검색된 fullName의 프로퍼티가 데이터 프로퍼티인지, 접근자 프로퍼티인지 확인
  4. 접근자 프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]]의 값인 getter 함수를 호출하여 결과 반환

프로토타입 어떤 객체의 상위 객체의 역할을 하는 객체 (19장 참고)


4. 프로퍼티 정의

프로퍼티 정의란 아래의 두 가지 경우를 의미한다.
즉, 프로퍼티 자체를 정의하는 것보단, 프로퍼티 어트리뷰트를 새로 명시하거나 재정의하는 것이다.

  1. 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의
  2. 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의

📌 Object.defineProperty

  • 프로퍼티 어트리뷰트를 정의할 수 있는 메서드 (한번에 하나의 프로퍼티만 정의)
  • 인수 객체의 참조와 데이터 프로퍼티의 키 문자열, 프로퍼티 디스크립터 객체
const person = {};

// 1️⃣ 데이터 프로퍼티 정의
// 인수 : 객체의 참조 (person), 데이터 프로퍼티의 키인 문자열 ('firstName'), 프로퍼티 디스크립터 객체 ( { ... } )
Object.defineProperty(person, 'firstName', {
  value: 'sozzang',
  writable: true,
  enumerable: true,
  configurable: true
})

// 2️⃣ 접근자 프로퍼티 정의
Object.defineProperty(person, 'fullName', {
  get(){ 
    return `${this.firstName} ${this.lastName}`
  },
  set(name){ 
    [this.firstName, this.lastName] = name.split(' ')
  },
  enumerable: true,
  configurable: true
})

📌 Object.defineProperties

  • Object.defineProperty와 다르게 여러 개의 프로퍼티 어트리뷰트를 한 번에 정의할 수 있는 메서드
const person = {};

Object.defineProperties(person, {
  // 1️⃣ 데이터 프로퍼티 정의
  firstName : {
    value: 'sozzang',
    // writable 정의 생략
    // enumerable 정의 생략
    // configurable 정의 생략
  },
  
  // 2️⃣ 접근자 프로퍼티 정의
  fullName : {
    get(){ 
      return `${this.firstName} ${this.lastName}`
    },
    set(name){ 
      [this.firstName, this.lastName] = name.split(' ')
    },
    enumerable: true,
    configurable: true
  },
})

📌 프로퍼티 디스크립터 객체의 프로퍼티 일부를 '생략'했을 경우

다음의 값이 기본값으로 적용된다.

  • value undefined
  • get undefined
  • set undefined
  • writable false
  • enumerable false
  • confiturable false

📌 프로퍼티 디스크립터 객체의 프로퍼티 어트리뷰트 값이 'false'인 경우

  • [[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값 변경 불가
  • [[Enumerable]]의 값이 false인 경우 : 해당 프로퍼티는 열거되지 않음
    -> for ... in문이나 Object.keys 등으로 열거 불가
  • [[Confiturable]]의 값이 false인 경우 : 해당 프로퍼티 삭제/재정의 불가

5. 객체 변경 방지

객체는 변경 가능한 값이기 때문에, 재할당 없이 직접 변경이 가능하다.
즉, 프로퍼티 추가/삭제/갱신이 가능하고 프로퍼티 어트리뷰트의 재정의도 가능하다.

경우에 따라, 이러한 객체의 변경을 방지하는 메서드가 필요하다.
다음의 3가지 메서드는 객체의 변경을 금지하는 강도가 다르다.

1. 객체 확장 금지

객체 확장 금지는, 프로퍼티 추가를 금지하는 것을 의미한다.
즉, 확장이 금지된 객체는 프로퍼티 추가가 금지된다.

프로퍼티 추가는 1️⃣ 프로퍼티 동적 추가와 2️⃣ Object.defineProperty 메서드로 가능한데, 두 가지 모두 금지된다.

  • Object.preventExtensions 객체 확장 금지 설정 메서드
  • Object.isExtensible 확장 가능한 객체인지 확인하는 메서드
const person = { name: 'sozzang' };

// 1️⃣ person 객체는 확장이 금지된 객체가 아님
console.log(Object.isExtensible(person)); // true

// 2️⃣ person 객체의 확장을 금지하여 프로퍼티 추가를 금지함
Object.preventExtensions(person);

// 3️⃣ person 객체는 확장이 금지됨
console.log(Object.isExtensible(person)); // false

// 4️⃣ 프로퍼티 추가 금지
person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "sozzang"}

// 5️⃣ 프로퍼티 정의에 의한 추가도 금지
Object.defineProperty(person, 'age', { value: 20 });
// TypeError: Cannot define property age, object is not extensible

2. 객체 밀봉

객체 밀봉은, 프로퍼티 추가와 삭제, 프로퍼티 어트리뷰트의 재정의를 금지하는 것을 의미한다.
즉, 밀봉된 객체는 프로퍼티 읽기와 쓰기만 가능하다.

  • Object.seal 객체 밀봉 메서드
  • Object.isSealed 밀봉된 객체인지 확인하는 메서드
const person = { name: 'sozzang' };

// 1️⃣ person 객체는 밀봉된 객체가 아님
console.log(Object.isSealed(person)); // true

// 2️⃣ person 객체를 밀봉함
Object.seal(person);

// 3️⃣ person 객체가 밀봉됨
console.log(Object.isSealed(person)); // false

// 4️⃣ 밀봉된 객체는 configurable이 false !! 
console.log(Object.getOwnPropertyDescriptors(person));
/*
{ name: {value: "sozzang", writable: true, enumerable: true, configurable: false}, }
*/

// 5️⃣ 프로퍼티 추가 금지
person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "sozzang"}

// 6️⃣ 프로퍼티 삭제 금지
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "sozzang"}

// 7️⃣ 프로퍼티 값 갱신 가능
person.name = "jjajang";
console.log(person); // {name: "jjajang"}

// 8️⃣ 프로퍼티 어트리뷰트 재정의 금지
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name

3. 객체 동결

객체 동결은, 프로퍼티 추가와 삭제, 프로퍼티 어트리뷰트의 재정의를 금지, 프로퍼티 값 갱신 금지를 의미한다.
즉, 동결된 객체는 프로퍼티 읽기만 가능하다.

  • Object.freeze 객체 동결 메서드
  • Object.isFrozen 동결된 객체인지 확인하는 메서드
const person = { name: 'sozzang' };

// 1️⃣ person 객체는 동결된 객체가 아님
console.log(Object.isFrozen(person)); // true

// 2️⃣ person 객체를 동결함
Object.freeze(person);

// 3️⃣ person 객체가 동결됨
console.log(Object.isFrozen(person)); // false

// 4️⃣ 동결된 객체는 writable과 configurable이 false !! 
console.log(Object.getOwnPropertyDescriptors(person));
/*
{ name: {value: "sozzang", writable: false, enumerable: true, configurable: false}, }
*/

// 5️⃣ 프로퍼티 추가 금지
person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "sozzang"}

// 6️⃣ 프로퍼티 삭제 금지
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "sozzang"}

// 7️⃣ 프로퍼티 값 갱신 금지
person.name = "jjajang"; // 무시. strict mode에서는 에러
console.log(person); // {name: "sozzang"}

// 8️⃣ 프로퍼티 어트리뷰트 재정의 금지
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name

4. 객체 불변

앞의 3가지는 객체의 얕은 변경 방지로, 직속 프로퍼티만 변경이 방지된다.
중첩 객체는 영향을 받지 않는다.
즉, Object.freeze 메서드로 객체를 동결하여도 중첩 객체까지 동결시키는 것은 불가능하다.

const person = {
  name: 'sozzang',
  address: { city: 'seoul' }
};

// 얕은 객체 동결
Object.freeze(person);

// 직속 프로퍼티만 동결
console.log(Object.isFrozen(person)); // true
// 중첩 객체까지 동결하지 못함
console.log(Object.isFrozen(person.address)); // false

person.address.city = 'Suwon';
console.log(person); // {name: "sozzang", address: {city: "Suwon"}}

객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 객체로 구현해야 한다면,
객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야 한다.

// 재귀적인 Object.freeze 메서드 호출
function deepFreeze(target) {
  if(target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target);
  	Object.keys(target).forEach(key => deepFreeze(target[key]));
  }
  return target;
}

이렇게 ... 익숙하지 않았던 프로퍼티 어트리뷰트도 완료했다 !
사실 프로퍼티 어트리뷰트는, 이름만 어색했던 것이었다. 프로퍼티의 값은 아주 익숙했는데 말이다 ..!

익숙하지 않은 용어가 많이 나왔으니까, 이해한 내용을 간략하게 정리해보자 !

  • 객체의 특징이나 속성이 프로퍼티 어트리뷰트에 담겨있다.
  • 이것은 내부 슬롯이기 때문에 프로퍼티 디스크립션 객체로 접근해야 한다.
  • 프로퍼티 어트리뷰트는 데이터 프로퍼티접근자 프로퍼티로 구분된다.
  • 데이터 프로퍼티는 보통 알고 있던 값이 포함되어 있다.
  • 접근자 프로퍼티는 프로퍼티의 값을 읽거나 저장하기 위한 동작으로 구성된다.
  • Object.defineProperty 메서드를 통해 프로퍼티 어트리뷰트를 새로 정의하거나 재정의할 수 있다.
  • 객체의 값이나 프로퍼티 어트리뷰트를 재정의하는 것을 방지하여 객체를 사용할 수 있다.
  • 방지하는 강도에 따라 확장 금지, 객체 밀봉, 객체 동결로 나뉜다.

이해가 잘 안되는 부분은 챗지피티랑 싸워서 ... 내가 이긴 듯 하다 !
만약 .. 틀린 내용이 있다면 언제든지 댓글로 알려주세요 !

profile
" 퍼블리셔에서 프론트엔드로 Level up 중 ... 💨 "

0개의 댓글