LHJ

I'm a FE developer.

21.2 객체 프로퍼티 속성

05 Jun 2020 » js_lj

21.2 객체 프로퍼티 속성

우리는 객체 프로퍼티에 대해 꽤 잘 알고 있습니다.
프로퍼티에는 항상 키가 있고, 키는 문자열이나 심볼일 수 있습니다.
프로퍼티의 값은 어떤 타입이든 괜찮습니다.
배열이나 과 달리 객체의 프로퍼티에는 순서가 없습니다.
객체 프로퍼티에 접근할 때점 연산자대괄호 연산자를 사용합니다.
마지막으로, 객체 프로퍼티식별자키로 사용하는 일반적인 프로퍼티, 심볼이나 표현식을 사용하는 계산된 프로퍼티, 메서드 단축 표기의 세 가지가 있습니다.

객체 프로퍼티

  • 프로퍼티에는 항상 키가 있고 키는 문자열이나 심볼일 수 있다.
  • 프로퍼티의 값은 어떤 타입이든 괜찮다.
  • 배열이나 맵과 달리 객체 프로퍼티에는 순서가 없다.
  • 객체 프로퍼티 접근 : . 연산자 or 대괄호 연산자
  • 객체 프로퍼티 식별자 종류 3가지 : 키(일반적 프로퍼티) / 심볼 또는 표현식(계산된 프로퍼티) / 메서드 단축표기

그 밖에도 프로퍼티에 대해 더 알아야 할 것이 있습니다.
프로퍼티에는 자신이 속한 객체 안에서 어떻게 동작할지 결정하는 속성(attribute)이 있습니다.
먼저 우리가 잘 알고 있는 방식으로 프로퍼티를 만들고, Object.getOwnPropertyDescriptor를 써서 속성을 알아봅시다.

const obj = {foo: "bar"};
Object.getOwnPropertyDescriptor(obj,'foo');

결과는 다음과 같습니다.

{value: "bar", writable: true, enumerable: true, configurable: true}

NOTE_
프로퍼티 속성, 프로퍼티 서술자(descripter), 프로퍼티 설정(configuration)은 모두 같은 것을 가리키는 용어이며 섞어 써도 무방합니다.

프로퍼티 속성 세 가지가 나타난 것을 볼 수 있습니다.

  1. 쓰기 가능한지(writable)
    프로퍼티 값을 바꿀 수 있는지 아닌지 판단합니다.
  2. 나열 가능한지(enumerable)
    for ... in 문이나 Object.keys, 확산 연산자에서 객체 프로퍼티를 나열할 때 해당 프로퍼티가 포함될지 아닌지 판단합니다.
  3. 설정 가능한지(configurable)
    프로퍼티를 객체에서 삭제하거나 속성을 수정할 수 있는지 아닌지 판단합니다.

Object.defineProperty로는 프로퍼티 속성을 컨트롤하거나(설정 가능한 경우) 새 프로퍼티를 만들 수 있습니다.
예를 들어 Object.defineProperty로 obj의 foo 프로퍼티를 읽기 전용으로 만들 수 있습니다.

const obj = {foo: "bar"};
Object.defineProperty(obj, 'foo', {writable: false})
Object.getOwnPropertyDescriptor(obj,'foo');

이제 foo에 값을 할당하려 하면 에러가 일어납니다.

obj.foo = 3;
// TypeError: Cannot assign to read only property 'foo' of [object Object]

CAUTION_
읽기 전용 프로퍼티에 값을 할당하려 할 때 에러가 발생하는 것은 스트릭트 모드에서뿐입니다.
스트릭트 모드가 아닐 때는 할당이 실패하지만 에러는 일어나지 않습니다.

(아..이래서 위에서 해볼 때 에러가 안났던거구나..)

Object.defineProperty를 써서 객체에 새 프로퍼티를 추가할 수도 있습니다.
일반적인 데이터 프로퍼티와 달리, 객체가 일단 생성된 뒤에는 접근자 프로퍼티를 추가할 다른 방법이 없고 Object.defineProperty를 쓰는 방법뿐입니다.
obj에 color 프로퍼티를 추가해봅시다.
이번에는 심볼을 쓰지 않고 유효성 검사도 하지 않습니다.

const obj = {foo: "bar"};
Object.defineProperty(obj, 'color', {
    get: function() {
        return this._color;
    },
    set: function(value) {
        this._color = value;
    },
})

Object.defineProperty로 데이터 프로퍼티를 추가할 때는 value 프로퍼티를 사용하면 됩니다.
obj에 name과 greet 프로퍼티를 추가해 봅시다.

const obj = {foo: "bar"};
Object.defineProperty(obj, 'name', {
    value: 'Cynthia',
});
Object.defineProperty(obj, 'greet', {
    value: function() {
        return `Hello, my name is ${this.name}!`
    }
})

역주_
이 예제를 실행한 후 obj.name을 콘솔에 써 보면 Cynthia가 나타나지만, obj를 콘솔에 써보면 name 프로퍼티가 나타나지 않습니다.
이는 enumerable 속성이 정의되지 않았기 때문입니다. (음..잘 나오는데…name 프로퍼티의 enuverable이 false로 되어있는 객체들을 말하는 거 같음..)

Object.defineProperty(obj, 'name', {
    value: 'Cynthia', 
    enumerable: true
})

이렇게 enumerable을 true로 명시하고 obj를 콘솔에 써 보면 프로퍼티가 나타납니다.
마찬가지로, 배열에 메서드를 추가하는 다음 예제에서도 defineProperty를 통해 메서드를 추가하는 두 번째 방법에서는 enumerable: false를 명시적으로 지정하지 않아도 arr을 콘솔에 쓰거나 for…in 문을 사용할 때 메서드가 노출되지 않습니다.

Object.defineProperty는 배열 프로퍼티를 나열할 수 없게 만들 때 주로 사용합니다.
배열은 원래 프로퍼티를 사용하지 않도록 설계됐으므로 문자열이나 심볼 프로퍼티는 사용하지 않는 게 좋다고 언급한 적이 있습니다만, 충분히 생각하고 조심스럽게 사용한다면 배열 프로퍼티도 유용하게 쓸 수 있습니다.
배열에서 for … in이나 Object.keys를 사용하는 것 역시 권장하지는 않지만 다른 개발자가 사용하는 걸 막을 수는 없습니다.
따라서 배열에 숫자형 프로퍼티가 아닌 프로퍼티를 추가한다면 다른 누군가가 그 배열에서 for…in이나 Object.keys를 사용했을 때 노출되지 않도록 나열할 수 없게 만들어야 합니다.

다음 코드는 배열에 sum과 avg 메서드를 추가합니다.

const arr = [3, 1.5, 9, 2, 5.2];
arr.sum = function() {
    return this.reduce((a, x) => a+x);
}
arr.avg = function() {
    return this.sum()/this.length;
}
Object.defineProperty(arr, 'sum', { enumerable: false });
Object.defineProperty(arr, 'avg', { enumerable: false });

다음과 같이 프로퍼티 하나를 문 하나로 완결하는 방법도 있습니다.

const arr = [3, 1.5, 9, 2, 5.2];
Object.defineProperty(arr, 'sum', {
    value: function() {
        return this.reduce((a, x) => a+x); 
    },
    enumerable: false
});
Object.defineProperty(arr, 'avg', {
    value: function() {
        return this.sum()/this.length;
    },
    enumerable: false
});

마지막으로, Object.defineProperties도 있습니다(복수형입니다).
이 메서드는 객체 프로퍼티 이름과 프로퍼티 정의를 서로 연결합니다.
바로 앞 예제를 Object.defineProperties를 써서 다음과 같이 고칠 수 있습니다.

const arr = [3, 1.5, 9, 2, 5.2];
Object.defineProperties(arr, {
    sum: {
        value: function() {
            return this.reduce((a, x) => a+x);
        },
        enumerable: false
    },
    avg: {
        value: function() {
            return this.sum() / this.length;
        },
        enumerable: false
    }
});