7. 객체 프로퍼티 설정

7.1 프로퍼티 플래그와 설명자


프로퍼티 플래그

  • 객체 프로퍼티는 값(value) 과 함께 플래그(flag)라 불리는 특별한 속성 세 가지를 갖습니다.
    - writabletrue이면 값을 수정할 수 있습니다. 그렇지 않다면 읽기만 가능합니다.
    - enumerabletrue이면 반복문을 사용해 나열할 수 있습니다. 그렇지 않다면 반복문을 사용해 나열할 수 없습니다.
    - configurabletrue이면 프로퍼티 삭제나 플래그 수정이 가능합니다. 그렇지 않다면 프로퍼티 삭제와 플래그 수정이 불가능합니다.

  • 지금까지 해왔던 '평범한 방식’으로 프로퍼티를 만들면 해당 프로퍼티의 플래그는 모두 true가 됩니다. 이렇게 true로 설정된 플래그는 언제든 수정할 수 있습니다.

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);

  • obj: 정보를 얻고자 하는 객체
  • propertyName: 정보를 얻고자 하는 객체 내 프로퍼티
let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/
  • 메서드 Object.defineProperty를 사용하면 플래그를 변경할 수 있습니다.
    Object.defineProperty(obj, propertyName, descriptor)
  • obj, propertyName: 설명자를 적용하고 싶은 객체와 객체 프로퍼티
  • descriptor: 적용하고자 하는 프로퍼티 설명자
  • defineProperty메서드는 객체에 해당 프로퍼티가 있으면 플래그를 원하는 대로 변경해줍니다. 프로퍼티가 없으면 인수로 넘겨받은 정보를 이용해 새로운 프로퍼티를 만듭니다. 이때 플래그 정보가 없으면 플래그 값은 자동으로 false가 됩니다.

writable 플래그

  • writable 플래그를 사용해 user.name에 값을 쓰지 못하도록(non-writable) 해봅시다.
let user = {
  name: "John"
};
Object.defineProperty(user, "name", {
  writable: false
});
user.name = "Pete"; // Error: Cannot assign to read only property 'name'
  • 이제 defineProperty를 사용해 writable 플래그를 true로 변경하지 않는 한 그 누구도 객체의 이름을 변경할 수 없게 되었습니다.

enumerable 플래그

  • 객체 내장 메서드 toString은 열거가 불가능(non-enumerable)하기 때문에 for..in 사용시 나타나지 않습니다. 하지만 커스텀 toString을 추가하면 아래와 같이 for..intoString이 나타납니다.
let user = {
  name: "John",
  toString() {
    return this.name;
  }
};
//커스텀 toString은 for...in을 사용해 열거할 수 있습니다.
for (let key in user) alert(key); // name, toString
  • 그런데 특정 프로퍼티의 enumerable 플래그 값을 false로 설정하면 for..in 반복문에 나타나지 않게 할 수 있습니다. 커스텀 toString도 열거가 불가능하게 할 수 있습니다.
let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

Object.defineProperty(user, "toString", {
  enumerable: false
});

// 이제 for...in을 사용해 toString을 열거할 수 없게 되었습니다.
for (let key in user) alert(key); // name
  • 열거가 불가능한 프로퍼티는 Object.keys에도 배제됩니다.
    alert(Object.keys(user)); // name

configurable 플래그

  • 어떤 프로퍼티의 configurable 플래그가 false로 설정되어 있다면 해당 프로퍼티는 객체에서 지우거나 변경하거나 덮어쓸 수 없습니다.

  • configurable 플래그를 false로 설정하면 돌이킬 방법이 없습니다. defineProperty를 써도 값을 true로 되돌릴 수 없죠.

  • configurable:false가 만들어내는 구체적인 제약사항은 아래와 같습니다.
    - configurable 플래그를 수정할 수 없음
    - enumerable 플래그를 수정할 수 없음.
    - writable: false의 값을 true로 바꿀 수 없음(truefalse로 변경하는 것은 가능함).
    - 접근자 프로퍼티 get/set을 변경할 수 없음(새롭게 만드는 것은 가능함).

  • “영원히 변경할 수 없는” 프로퍼티(user.name) 만들기
let user = { };

Object.defineProperty(user, "name", {
  value: "John",
  writable: false,
  configurable: false
});

// user.name 프로퍼티의 값이나 플래그를 변경할 수 없습니다.
// 아래와 같이 변경하려고 하면 에러가 발생합니다.
//   user.name = "Pete"
//   delete user.name
//   Object.defineProperty(user, "name", { value: "Pete" })
Object.defineProperty(user, "name", {writable: true}); // Error

"non-configurable"은 "non-writable"과 다릅니다.

configurable 플래그가 false이더라도 writable 플래그가 true이면 프로퍼티 값을 변경할 수 있습니다.

configurable: false는 플래그 값 변경이나 프로퍼티 삭제를 막기 위해 만들어졌지, 프로퍼티 값 변경을 막기 위해 만들어진 게 아닙니다.

Object.defineProperties

  • Object.defineProperties(obj, descriptors) 메서드를 사용하면 프로퍼티 여러 개를 한 번에 정의할 수 있습니다.
Object.defineProperties(obj, {
  prop1: descriptor1,
  prop2: descriptor2
  // ...
});

// ex)
Object.defineProperties(user, {
  name: { value: "John", writable: false },
  surname: { value: "Smith", writable: false },
  // ...
});

Object.getOwnPropertyDescriptors

  • Object.getOwnPropertyDescriptors(obj) 메서드를 사용하면 프로퍼티 설명자를 전부 한꺼번에 가져올 수 있습니다.
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
  • 할당 연산자를 사용해 프로퍼티를 복사하면 플래그정보, 심볼형 프로퍼티를 놓칠 수 있습니다. 하지만 Object.getOwnPropertyDescriptors는 심볼형 프로퍼티를 포함한 프로퍼티 설명자 전체를 반환합니다.

객체 수정을 막아주는 다양한 메서드(생략)



7.2 프로퍼티 getter와 setter


  • 객체의 프로퍼티는 두 종류로 나뉩니다.
  • 첫 번째 종류는 데이터 프로퍼티(data property) 입니다. 지금까지 사용한 모든 프로퍼티는 데이터 프로퍼티입니다. 데이터 프로퍼티 조작 방법에 대해선 모두 알고 계실 것이라 생각합니다.
  • 두 번째는 접근자 프로퍼티(accessor property) 라 불리는 새로운 종류의 프로퍼티입니다. 접근자 프로퍼티의 본질은 함수인데, 이 함수는 값을 획득(get)하고 설정(set)하는 역할을 담당합니다. 그런데 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보입니다.

getter와 setter

let obj = {
  get propName() {
    // getter, obj.propName을 실행할 때 실행되는 코드
  },

  set propName(value) {
    // setter, obj.propName = value를 실행할 때 실행되는 코드
  }
};
  • getter 메서드는 obj.propName을 사용해 프로퍼티를 읽으려고 할 때 실행되고, setter 메서드는 obj.propName = value으로 프로퍼티에 값을 할당하려 할 때 실행됩니다.
let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// 주어진 값을 사용해 set fullName이 실행됩니다.
user.fullName = "Alice Cooper";

alert(user.name); // Alice
alert(user.surname); // Cooper
  • 바깥 코드에선 접근자 프로퍼티를 일반 프로퍼티처럼 사용할 수 있습니다. 접근자 프로퍼티는 이런 아이디어에서 출발했습니다. 접근자 프로퍼티를 사용하면 함수처럼 호출 하지 않고, 일반 프로퍼티에서 값에 접근하는 것처럼 평범하게 user.fullName을 사용해 프로퍼티 값을 얻을 수 있습니다. 나머지 작업은 getter 메서드가 뒷단에서 처리해줍니다.
  • 이렇게 gettersetter 메서드를 구현하면 객체엔 fullName이라는 '가상’의 프로퍼티가 생깁니다. 가상의 프로퍼티는 읽고 쓸 순 있지만 실제로는 존재하지 않습니다.

접근자 프로퍼티 설명자

  • 접근자 프로퍼티엔 설명자 valuewritable가 없는 대신에 getset이라는 함수가 있습니다.
    - get – 인수가 없는 함수로, 프로퍼티를 읽을 때 동작함
    - set – 인수가 하나인 함수로, 프로퍼티에 값을 쓸 때 호출됨
    - enumerable – 데이터 프로퍼티와 동일함
    - configurable – 데이터 프로퍼티와 동일함
  • 프로퍼티는 접근자 프로퍼티(get/set 메서드를 가짐)나 데이터 프로퍼티(value를 가짐) 중 한 종류에만 속하고 둘 다에 속할 수 없다는 점을 항상 유의하시기 바랍니다.
  • 한 프로퍼티에 get과 value를 동시에 설정하면 에러가 발생합니다

getter와 setter 똑똑하게 활용하기

  • gettersetter를 ‘실제’ 프로퍼티 값을 감싸는 래퍼(wrapper)처럼 사용하면, 프로퍼티 값을 원하는 대로 통제할 수 있습니다.
  • 아래 예시에선 name을 위한 setter를 만들어 user의 이름이 너무 짧아지는 걸 방지하고 있습니다. 실제 값은 별도의 프로퍼티 _name에 저장됩니다.
let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("입력하신 값이 너무 짧습니다. 네 글자 이상으로 구성된 이름을 입력하세요.");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
alert(user.name); // Pete

user.name = ""; // 너무 짧은 이름을 할당하려 함

user의 이름은 _name에 저장되고, 프로퍼티에 접근하는 것은 gettersetter를 통해 이뤄집니다.

기술적으론 외부 코드에서 user._name을 사용해 이름에 바로 접근할 수 있습니다. 그러나 밑줄 "_" 로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습입니다.

호환성을 위해 사용하기

  • 접근자 프로퍼티는 언제 어느 때나 gettersetter를 사용해 데이터 프로퍼티의 행동과 값을 원하는 대로 조정할 수 있게 해준다는 점에서 유용합니다.
function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // age는 현재 날짜와 생일을 기준으로 계산됩니다.
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert( john.birthday ); // birthday를 사용할 수 있습니다.
alert( john.age );      // age 역시 사용할 수 있습니다.

📌 추가


객체의 상태가 변경되는 것은 객체 스스로의 행동에 의해서야 한다.

이렇게 설계될 때 객체는 자율적인 객체가 되고 외부의 영향을 받지 않음으로써 느슨한 결합과 유연한 협력을 이룰 수 있다.

getter, setter는 자신의 상태정보를 외부에 노출하고 이것은 외부의 영향으로 상태정보가 변할 수 있는 가능성을 열어두게된다. 한 객체의 변화가 여러 객체에게 영향을 주는 이러한 코드가 많아질수록 훗날 유지보수가 어려워질 것이다.
출처 : Getter, Setter 지양하기




📚 참고 : javascript.info

좋은 웹페이지 즐겨찾기