속성 설명자 (PropertyDescriptor)

속성 설명자란 무엇인가

객체의 속성들은 그 자체로 객체 내부의 정보와 기능을 표현하지만, 각 속성들은 다시 그 자신들의 값과 성질에 대한 눈에 보이지않는 내부 속성들을 가지고 있습니다.

위에서 말한 속성의 성질이란 이 속성이 읽기전용인지, 나열될 수 있는지 등의 정보를 의미합니다.

자바스크립트에서는 이러한 속성의 세부적인 성질을 직접 설정하거나 조회할 수 있는 방법을 제공하는데, 이 때 이용되는 특수한 객체가 바로 속성 설명자(PropertyDescriptor) 입니다.

생김새

속성 설명자는 아래와 같이 약속된 구성대로 정형화된 객체의 모양새를 갖추고 있습니다.

{
    value: ...,
    writable: true,
    enumerable: true,
    configurable: true
}

속성 설명자는 키값으로 value, get, set, writable, enumerable, configureable 을 가질 수 있습니다.

속성 설명자를 확인하는 방법

어떤 속성(들)의 설명자를 확인하는 방법은 아래와 같습니다.

특정 한 개의 속성의 설명자를 확인

속성 설명자는 Object 내장 객체의 Object.getOwnPropertyDescriptor() 메서드를 이용해 확인이 가능합니다.

  • Object.getOwnPropertyDescriptor(객체, 속성명)

매개변수로 조회하고자하는 객체와 함께 속성의 이름(키값)을 문자열 형태로 받습니다. (문자열이 아닌 값이 오면 내부적으로 형변환을 진행합니다.)

const human1 = {
  firstName: "영재",
  lastName: "주",
  age: 30
};

const result = Object.getOwnPropertyDescriptor(human1,"age");
console.log(result);
//출력 : {value: 30, writable: true, enumerable: true, configurable: true}

모든 속성들에 대해 설명자를 확인

Object.getOwnPropertyDescriptors(객체) 라는 Object 내장함수를 사용하여 해당 객체의 모든 속성들의 설명자를 일괄 조회할 수도 있습니다.

const human1 = {
  firstName: "영재",
  lastName: "주",
  age: 30
};

const result = Object.getOwnPropertyDescriptors(human1);
console.log(result);

/* 출력 :
{
    firstName: {
        value: 영재,
        writable: true,
        enumerable: true,
        configurable: true
    },
    lastName: {
        value: 주,
        writable: true,
        enumerable: true,
        configurable: true
    },
    age: {
        value: 30,
        writable: true,
        enumerable: true,
        configurable: true
    }
}*/

속성 설명자를 이용해 속성을 정의하는 방법

어떤 속성(들)을 설명자를 통해 정의하는 방법은 아래와 같습니다.

특정 한 개의 속성을 설정

속성 설명자를 확인할 때와 마찬가지로 내장객체인 Object의 메서드를 사용합니다.

  • Object.defineProperty(객체, 속성명, 설명자 객체)

매개변수로 조회하고자하는 객체와 함께 속성의 이름(키값)을 문자열 형태로 받습니다. (문자열이 아닌 값이 오면 내부적으로 형변환을 진행합니다.)

그리고 마지막 인자로 해당 속성을 정의하는데 사용할 설명자 객체를 받습니다.

예시

const arr = [1, 2, 3];

Object.defineProperty(arr, "4", {
  value: 5,
  writable: false,
  configurable: true,
  enumerable: true
});

console.log(arr); // 출력 : [1, 2, 3, undefined, 5]
arr[4] = 2; // writable 키를 이용해 읽기전용으로 설정했으므로 오류 발생.

여러가지 속성을 한 번에 설정

한 번에 여러가지 속성을 일괄적으로 설정하는 것도 가능합니다.

  • Object.defineProperties(객체, 속성명, { 설명자 객체1, 설명자 객체2, ... })

예시

const arr = [];

Object.defineProperties(arr, {
  0: {
    value: 1,
    writable: false,
    configurable: true,
    enumerable: true
  },
  1: {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
  },
});

console.log(arr); // 출력 : [1, 2]

설명자의 키 값을 생략했을 때

속성 설명자를 기술할 때, 기술하지 않은 키들이 있을 경우의 동작은 다음 두 가지 경우 중 하나입니다.

기존 설명자가 있을 경우

즉, 이미 존재하는 속성에 대해 변경을 시도할 때에는 직접 서술한 키값 외에는 기존의 것을 유지합니다.

const arr = [1, 2, 3];

console.log(Object.getOwnPropertyDescriptor(arr, "0"));
// 출력 : Object {value: 1, writable: true, enumerable: true, configurable: true}

Object.defineProperty(arr, "0", { writable: false });

console.log(Object.getOwnPropertyDescriptor(arr, "0"));
// 출력 : Object {value: 1, writable: false, enumerable: true, configurable: true}

기존 설명자가 없을 경우

기존 설명자가 없을 경우, 즉 새로운 속성을 설명자를 통해 할당할 때에는

비어있는 키값은 모두 false를 기본값으로 갖습니다.

const obj = {};

console.log(Object.getOwnPropertyDescriptor(obj, "name"));
// 출력 : undefined

Object.defineProperty(obj, "name", { value: "주영재" });

console.log(Object.getOwnPropertyDescriptor(obj, "name"));
// 출력 : Object {value: "주영재", writable: false, enumerable: false, configurable: false}

이 경우,

{
    value: 1,
}

{
    value: 1,
    writable: false,
    enumerable: false,
    configurable: false
}

와 같습니다.

속성 설명자의 구성요소

이제 속성 설명자의 키값에 대해 각각의 역할과 기능을 알아보도록 합시다.

value

말 그대로 해당 속성의 '값'을 나타냅니다.

{
    value: function name() {
        return this.name;
    }
}

이 value 키값에 어떤 값을 지정하고 어떤 속성을 정의할 경우, 해당 속성의 값으로 value에 지정된 값이 할당하게 됩니다.

value 예시

const arr = [1, 2, 3];
Object.defineProperty(arr, "4", { value: 5 });

console.log(arr); // 출력 : [1, 2, 3, undefined, 5]

get과 set

getter와 setter 접근자 속성은 get과 set이라는 특수한 설명자 요소를 이용해 정의할 수 있습니다.

{
    get: [함수],
    set: [함수]
}

writable, value와 공존할 수 없음

get/set 키값은 writable, value 키값과 함께 선언될 수 없습니다.
getter/setter는 할당 연산자를 이용한 문법으로 연결된 메서드를 동작하는 속성으로 writable, value와 공존할 수 없는 것은 사실 당연합니다.

함께 사용하고자 시도할 경우 관련된 오류가 다음과 같이 발생합니다.

TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute

같은 속성에 두 개의 get, set을 설정 가능

아시다시피 getter과 setter는 동일한 속성명으로 한 쌍을 구성하여 사용할 수 있기에(오버로딩) 하나의 속성에 대해 get, set 키값을 동시에 정의하는 것이 가능합니다.

get set 예시

const human1 = {
  firstName:"영재",
  lastName: "주",
}

Object.defineProperty(human1, "name", {
  get:function () {
    return this.lastName + this.firstName;
    },
  set: function(name) {
    this.firstName = name.substr(1,2);
    this.lastName = name[0];
  }
})

console.log(human1.name); // 출력 : "주영재"
human1.name = "김개똥"
console.log(human1); // 출력 : Object {firstName: "개똥", lastName: "김"}

writable

writable 키값이 false 일 경우 해당 속성은 읽기전용(read-only)이 됩니다.
할당연산자를 통해 새로운 값을 할당하는 것이 불가능해집니다.

{
    writable: false
}

writable이 false인 읽기전용 속성에 대해 값을 할당하고자하면 다음과 같은 오류가 발생합니다.

TypeError: Cannot assign to read only property '[프로퍼티명]' of object '[객체명]'

writable 예시

const arr = [1, 2, 3];
Object.defineProperty(arr, "1", { writable: false });

arr[1] = 3; // 오류 발생

enumerable

enumerable 키값이 false 일 경우 해당 속성은 나열 불가능한 상태가 됩니다.

나열이 불가능하다는 의미

우리가 어떤 객체의 속성들에 대해 순차적으로 참조를 하고자한다면 크게 두 가지 방법을 생각해볼 수 있습니다.

하나는 for in 루프를 사용하는 것, 둘째는 Object.keys() 함수를 사용하는 것입니다.

이 때 enumerable 키값이 false로 설정된 속성은 위 방법을 통해 참조할 수 없습니다.

예시

const human1 = {
  name: "주영재",
  age: 30,
  height: "174cm",
  hobby: "자바스크립트 공부"
};

Object.defineProperties(human1, {
  height: {
    enumerable: false
  }
});

console.log(Object.keys(human1));
// 출력 : ["name", "age", "hobby"]

const result = [];

for (const one in human1) {
  result.push(one);
}

console.log(result);
// 출력 : ["name", "age", "hobby"]

여담이지만 enumrable 값이 false 일 경우 크롬 디버거에서도 흐린 값으로 표시가 됩니다.

스크린샷, 2019-09-08 20-44-33.png

configurable

configurable 키값을 false로 설정하면 무슨 일이 일어날까요?

더이상 속성의 내부속성을 더이상 재정의할 수 없게됩니다.

configurable 값이 false인 경우 Object.defineProperty 메서드를 통해 해당 속성의 내부속성을 재설정하는 것이 불가능합니다.

조금 더 구체적으로 configuralbe이 false인 경우 enumerable, configurable 속성을 다시 변경하는 것이 불가능합니다.

단, value 속성을 변경하는 것은 가능하고 writable 속성이 true일 경우 false로 변경하는 것은 가능합니다. (반대로 writable을 false에서true로 변경하는 것은 불가능합니다.)

configurable 속성 그 자신도 더이상 변경이 안 된다는 부분에 주목해주십시오.

즉, 한 번 false로 설정이 되면 해당 속성은 더이상 내부 속성을 변경할 수 없게된다는 이야기입니다.

또한 속성을 삭제할 수 없습니다

delete 키워드를 사용하던, 해당 속성에 null 값을 할당하던, 더이상 객체에서 해당 속성을 삭제할 수 없게 됩니다.

예시

const human1 = {
  name: "주영재",
  age: 30,
  height: "174cm",
  hobby: "자바스크립트 공부"
};

Object.defineProperties(human1, {
  height: {
    configurable: false // (*)
  }
});

Object.defineProperty(human1, "height", {
  configurable: true
});
// 오류 : TypeError: Cannot redefine property: height

configurable 과 writable 의 차이점

configurable과 writable은 별개의 것으로 각각 다른 값을 지정할 수 있습니다.
경우의 수가 4가지밖에 되지 않으므로 각각의 경우들에 대해 모두 알아보도록 합시다.

  • writable: false && configurable: false
    • 할당연산자를 이용한 값의 재할당이 불가능합니다.
    • value 외의 내부 설정의 재정의가 불가능합니다.
    • 이를 어떤 속성이 얼려진 상태라고 합니다.
  • writable: false && configurable: true
    • 할당연산자를 이용한 값의 재할당이 불가능합니다.
    • defineProperty 메서드를 이용해 값의 재할당이 가능합니다.
  • writable: true && configurable: false
    • 할당연산자를 이용해 값을 새롭게 할당할 수 있습니다.
    • writable을 false로 바꾸거나 value를 변경하는 것 외의 내부 설정의 재정의가 불가능합니다.
    • 값이 재할당되어도 내부속성은 그대로 유지됩니다.
    • 이를 어떤 속성이 봉인된 상태라고 합니다.
  • writable: true && configurable: true
    • 내부 설정의 재정의와 할당연산자를 통한 값의 재할당이 가능합니다.
    • 신규 속성을 리터럴 표기법 등 일반적 방법으로 추가시 갖게되는 내부 속성들입니다.