[모던JS: Core] 객체 프로퍼티 설정

KG·2021년 5월 19일
0

모던JS

목록 보기
11/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

프로퍼티 플래그

자바스크립트의 객체는 프로퍼티를 저장하고, 이 프로퍼티는 키-값 쌍 구조를 가진다. 그러나 사실 객체의 프로퍼티는 그 자체로 더 유연하고 강력한 자료구조이다.

통상적으로 선언한 객체의 프로퍼티는 값(value)을 가지지만, 이 외에 플래그(flag)라고 불리는 특별한 속성 3가지를 더 가질 수 있다.

  1. writable : true라면 값을 수정 가능. 그렇지 않다면 읽기만 가능.
  2. enumerable : true이면 반복문을 사용해 나열 가능. 그렇지 않다면 반복문을 사용해 나열 불가
  3. configurable : true이면 프로퍼티 삭제나 플래그 수정 가능. 그렇지 않다면 프로퍼티 삭제와 플래그 수정이 불가

앞서 객체를 다루었던 일반적인 방식으로 객체를 선언하게 되는 경우 프로퍼티 플래그는 모두 true가 된다. 프로퍼티 플래그는 객체 내장 메서드를 통해 접근 및 수정이 가능하다.

1) 플래그 접근 메서드

Object.getOwnPropertyDescriptor 메서드를 사용하면 특정 프로퍼티에 대한 정보를 모두 얻을 수 있다.

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

// obj는 정보를 얻고자 하는 객체
// propertyName은 정보를 얻고자 하는 객체 내 프로퍼티

해당 메서드를 호출하면 프로퍼티 설명자(descriptor)라고 불리는 객체가 반환되고, 해당 객체는 프로퍼티의 값과 세가지 플래그에 대한 정보를 모두 담고 있다.

let user = {
  name: "KG"
};

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

console.log(JSON.stringify(descriptor, null, 2));
/*
{
    "value": "KG",
    "writable": true,
    "enumerable": true,
    "configurable": true
}
*/

2) 플래그 변경 메서드

Object.defineProperty 메서드를 사용해서 특정 프로퍼티에 대한 플래그를 변경할 수 있다.

Object.defineProperty(obj, propertyName, descriptor);

// obj는 설명자를 적용하고 싶은 객체
// propertyName은 설명자를 적용하고 싶은 객체 프로퍼티
// descriptor는 적용하고자 하는 프로퍼티 설명자

defineProperty는 일반 객체 선언과 달리 넘겨받는 정보(descriptor)가 없다면 모든 플래그 값을 false로 선언한다. 특정 플래그 값을 true로 설정하기 위해서는 descriptor안에 원하는 플래그 값을 true로 명시해주어야 한다.

let user = {};

// 일반 객체 선언 방식이 아닌
// defineProperty를 사용해 선언 (value만 설정)
Object.defineProperty(user, "name", {
  value: "KG"
});

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

console.log(JSON.stringify(descriptor, null, 2));

/*
{
    "value: "KG",
    "writable": false,
    "enumerable": false,
    "configurable": false,
}
*/

3) writable 플래그

writable 플래그를 통해 객체의 특정 프로퍼티를 읽기 전용으로 만들 수 있다.

let user = {
  name: "KG"
};

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

user.name = "SJ";	// Error: ... read only ...

// 또는 처음부터 defineProperty를 통해 선언할 수 있다.
// 아래의 결과는 위와 동일하다.

let user = {};

Object.defineProperty(user, "name", {
  value: "KG",
  // writable은 명시하지 않음으로 false로 초기화
  enumerable: true,
  configurable: true
});

user.name = "SJ";	// Error: ... read only ...

에러는 엄격모드(use strict)에서만 발생한다. 비엄격모드에서는 에러는 발생하지 않지만 값을 변경하려는 시도가 무시된다.

4) enumerable 플래그

모든 객체에는 객체가 텍스트 값으로 표시되거나 객체가 문자열이 예상되는 방식으로 참조 될 때 자동으로 호출되는 toString() 메서드가 있다. 그러나 이 메서드는 기본적으로 열거 불가(non-enumerable)하기 때문에 for...in 사용 시 나타나지 않는다. 그렇지만 일반 방식으로 커스텀 toString() 메서드 선언 시에는 enumerable 플래그 값이 true로 설정되기 때문에 for...in 사용 시 해당 값을 출력할 수 있다.

let userA = {
  name: "KG"
};

let userB = {
  name: "SJ",
  toString() {
    return this.name;
  }
};

for(let key in userA)
  console.log(key);	// name만 출력

for(let key in userB) 
  console.log(key);	// name, toString 출력

만일 커스텀 toString 메서드 역시 열거 불가능 속성으로 지정하고 싶다면 defineProperty 메서드를 이용하여 enumerable값을 false로 지정해주면 된다.

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

for(let key in userB) 
  console.log(key);	// name만 출력

열거 불가능으로 지정된 프로퍼티는 Object.keys() 메서드에서도 배제된다.

5) configurable 플래그

configurable 플래그는 false로 지정되면 해당 프로퍼티를 객체에서 지울 수 없는 것은 무론, 쓰기와 열거 모두 불가하다. 즉 재구성(non-configurable)이 불가능하다. 자바스크립트에서는 Math.PI와 같은 프로퍼티가 대표적으로 기본값이 configurable: false로 설정되어 있다.

configurable 값을 false로 설정하면 다시 돌이킬 수 있는 방법이 없다. 선언 시점에서부터 재구성이 불가능하도록 설정되기 때문이다. 해당 플래그가 만들어내는 구체적인 제약사항은 다음과 같다.

  • configurable 플래그 수정 불가
  • enumerable 플래그 수정 불가
  • writable: false의 값을 true로 변경 불가 (그러나 반대는 가능: true -> false)
  • 접근자 프로퍼티 get/set 변경 불가 (새로 생성은 가능)

non-configurable 과 non-writable은 다르다.
configurablefalse라도 writabletrue라면 프로퍼티 값 변경이 가능하다.
configurable: false는 플래그 값 변경 또는 프로퍼티 삭제를 위해 만들어진 플래그이다.
그러나 두 플래그가 모두 false라면 프로퍼티 값 변경 역시 불가하고 다시 true로 돌아갈 수 없다.

6) Object.defineProperties

Object.defineProperties 메서드를 사용하면 프로퍼티 여러개를 한번에 정의할 수 있다.

Object.defineProperties(user, {
  name: { value: "KG", writable: false },
  surname: { value: "LEE", writable: false }
});

7) Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors(obj) 메서드를 사용하면 해당 객체의 모든 프로퍼티 설명자를 전부 가져올 수 있다. 해당 메서드를 사용해서 객체를 복사하면 객체의 값 뿐만이 아니라 플래그까지 모두 복사가 가능하다.

또한 기본적으로 숨김 처리된 심볼(Symbol)형으로 선언된 프로퍼티까지 포함한 프로퍼티 설명자 전체를 반환한다.

const clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

// for...in을 통한 일반적인 방식
for (const key of obj) {
  clone[key] = obj[key];
}

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

프로퍼티 설명자는 특정 프로퍼티 하나를 대상으로 한다. 아래의 메서드는 한 객체 내 전체 프로퍼티를 대상으로 하는 제약사항을 적용할 수 있다.

  1. Object.preventExtensions(obj)
    • 객체에 새로운 프로퍼티를 추가할 수 없도록 설정
  1. Object.seal(obj)
    • 새로운 프로퍼티 추가 또는 기존 프로퍼티 삭제할 수 없도록 설정
    • configuralbe: false와 같은 효과
  1. Object.freeze(obj)
    • 새로운 프로퍼티 추가나 기존 프로퍼티 삭제, 수정할 수 없도록 설정
    • configurable: false, writable: false와 같은 효과

프로퍼티 getter와 setter

객체의 프로퍼티는 두 가지로 구분할 수 있다.

  1. 데이터 프로퍼티(data property)

    • 지금까지 사용한 모든 프로퍼티는 데이터 프로퍼티
    • 키-값의 구조로 저장되는 프로퍼티
  2. 접근자 프로퍼티(accessor property)

    • 본질은 함수와 같음: 값을 획득(get)하고 설정(set)하는 역할의 함수
    • 그러나 외부에서는 함수가 아닌 일반적인 프로퍼티처럼 보임

접근자 프로퍼티는 객체 지향형 프로그래밍 언어에서 보이는 gettersetter와 매우 유사하다.

1) getter & setter

접근자 프로퍼티는 gettersetter 메서드로 표현되고, 이는 객체 리터럴 안에서 각각 getset으로 나타낼 수 있다.

let user = {
  get propName() {
    // getter, obj.propName 반환
  },
  
  set propName(value) {
    // setter, obj.propName = value로 설정
  }
}
let user = {
  name: "KG",
  surname: "LEE",
  
  // getter
  get fullName() {
    return `${this.name} ${this.surname}`;
  },
  
  // setter
  set fullName() {
    [this.name, this.surname] = value.split(" ");
  }
}

console.log(user.fullName);	// KG LEE

user.fullName ="SJ WOO";	// setter(value) 사용
console.log(user.fullName);	// SJ WOO

위와 같이 gettersetter 메서드를 구현하면 객체엔 fullName이라는 가상의 프로퍼티가 생긴다. 가상의 프로퍼티는 분명 읽고 쓸 수는 있지만 가상이기 때문에 실제로 존재하지는 않는다.

2) 접근자 프로퍼티 설명자

데이터 프로퍼티와 설명자 프로퍼티의 설명자는 살짝 다르다. 접근자 프로퍼티는 그 값을 가상으로 읽고 쓰기 때문에 valuewritable 플래그가 존재하지 않는다. 대신 getset이라는 함수를 가진다. 따라서 Object.defineProperty를 통해서도 gettersetter를 설정할 수 있다.

  • get: 인수가 없는 함수로 프로퍼티를 읽을 때 동작
  • set: 인수가 하나인 함수로 프로퍼티를 쓸 때 동작
  • enumerable: 데이터 프로퍼티와 동일
  • configurable: 데이터 프로퍼티와 동일
let user = {
  name: 'KG',
  surname: 'LEE',
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },
  
  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

데이터 프로퍼티와 접근자 프로퍼티는 반드시 한 종류에만 속해야 한다. 따라서 get/set 플래그와 value/writable 플래그를 동시에 설정하면 에러가 발생한다.

3) getter와 setter 활용

gettersetter를 실제 프로퍼티 값을 감싸는 래퍼(wrapper)처럼 활용한다면, 프로퍼티 값을 원하는 대로 통제할 수 있다.

let user = {
  get name() {
    return this._name;
  },
  
  // 4글자 미만으로 입력한 이름은 처리하지 않도록 설정
  set name(value) {
    if (value.length < 4) {
      alert("입력한 값이 너무 짧습니다. 네 글자 이상으로 입력해주세요.");
      return;
    }
    
    this._name = value;
  }
};

user의 이름은 실제 프로퍼티 _name에 저장되고, 이에 접근하고 수정하는 것은 오직 gettersetter에 의해 이루어진다. 문법적으로는 user._name으로 바로 이름에 접근할 수는 있지만, 관습적으로 언더바(_)로 시작하는 프로퍼티는 오직 객체 내부에서만 활용하고 외부에서는 건드리지 않는다.

4) 호환성을 위한 getter와 setter

접근자 프로퍼티는 언제 어느 때나 gettersetter를 사용해 데이터 프로퍼티의 행동과 값을 원하는 대로 조정할 수 있게 한다는 점에서 유용하다.

만약 어떤 객체의 데이터 프로퍼티가 다음과 같이 선언되어 있었다고 가정하자.

function User(name, age) {
  this.name = name;
  this.age = age;
}

let KG = new User("KG", 20);

이때 만약 해당 객체의 age 프로퍼티 대신 birthday 프로퍼티가 저장되어야 하는 상황이 발생하면, 기존 코드의 age 프로퍼티를 모두 수정해주어야 한다. 즉 age 프로퍼티를 사용하고 있는 이미 기존에 선언된 객체 역시 일일이 찾아 수정해야 하는데 이는 매우 번거로울 뿐더러 시간이 오래 소요되는 작업이다.

만약 age 프로퍼티 처럼 굳이 제거될 필요가 없는 값이라면 굳이 이를 제거하지 말고 그대로 두되, getter를 이용하여 age에 대해 계속 접근할 수 있도록 설정할 수 있다. 이처럼 설정하면 기존에 생성된 객체 역시 age 프로퍼티를 계속 유지할 수 있다.

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
  
  // 객체 리터럴 방식의 객체 생성 방식이 아니기 때문에
  // Object.defineProperty 메세드를 이용해 getter 선언
  // 이때 전달하는 obj는 함수 성질에 맞춰 this를 전달한다.
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let kg = new User('KG', new Date(1995, 04, 27));

// birthday와 age 모두 사용 가능
console.log(kg.birthday);
console.log(kg.age);

References

  1. https://ko.javascript.info/object-properties
  2. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
profile
개발잘하고싶다

0개의 댓글