객체 프로퍼티는 어떻게 제어할까?

hyoribogo·2023년 6월 21일
2

데브코스

목록 보기
5/10
post-thumbnail

📒 자바스크립트

모던 자바스크립트 Deep Dive 16장 스터디를 통해 정리한 글 😚
퀴즈의 출처는 스터디원들입니다 💛


const zzang = 'hyori'

const amazingPerson = {
  zzang: 'hyori'
}

해당 코드를 보면 zzang 변수 값은 재할당이 불가능하지만, amazingPerson 객체에 있는 zzang 프로퍼티 값은 얼마든지 바꿀 수 있다. 따라서 짱은 바뀔 수 있다.

과연 객체 내의 짱이 바뀌지 않게 하려면 어떻게 해야할까?
이를 위해 프로퍼티 어트리뷰트를 이해해야 한다.
이 글은 자바스크립트의 프로퍼티 어트리뷰트에 대해 정리한 글이다.


✔️ 미리 푸는 테스트

프로퍼티 어트리뷰트에 대해 얼마나 답할 수 있는지 체크해보자!

키와 값으로 구성된 일반적인 프로퍼티를 무엇이라 부르는지 알고 있다.
자체적으로 값은 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출하는 접근자 함수로 구성된 프로퍼티를 무엇이라 부르는지 알고 있다.
데이터 프로퍼티의 프로퍼티 어트리뷰트로 어떤 것들이 있는지 알고 있다.
접근자 프로퍼티의 프로퍼티 어트리뷰트로 어떤 것들이 있는지 알고 있다.
객체의 [[prototype]] 내부 슬롯에 간접적으로 접근하는 방법을 알고 있다.
프로퍼티 디스크립터 객체를 반환하는 함수를 알고 있다.
기본적으로 프로퍼티를 동적으로 생성했을 때의 프로퍼티 어트리뷰트 값을 알고 있다.
프로퍼티의 어트리뷰트를 직접 정의할 수 있다.
프로퍼티 추가, 삭제, 값 쓰기, 어트리뷰트 재정의를 제어하는 함수들을 알고 있다.


코드의 실행결과는?

Q1. (난이도 ⭐⭐)

const person = { lastName: 'Hong' }
Object.seal(person)

person.age = 26
console.log(person)     // (1)

delete person.lastName 
console.log(person)     // (2)

person.lastName= 'Park'
console.log(person)     // (3)

Object.defineProperty(person, "lastName", {configurable: true})   // (4)
퀴즈 정답 Object.seal 메서드는 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미한다.
밀봉된 객체는 읽기와 쓰기만 가능하다.

(1) { lastName: 'Hong' }
설명 : 프로퍼티 추가 금지
(2) { lastName: 'Hong' }
설명 : 프로퍼티 삭제 금지
(3) { lastName: 'Park' }
설명 : 프로퍼티 값 갱신은 가능
(4) TypeError : Cannot redefine property : lastName
설명 : 프로퍼티 어트리뷰트 재정의 금지


Q2. (난이도 ⭐⭐⭐)

const animal = { name: "nana" }

Object.freeze(animal)

console.log(Object.getOwnPropertyDescriptors(animal))
// {value: "nana", writable: ?, enumerable: ?, configurable: ?}
퀴즈 정답 {value: "nana", writable: false, enumerable: true, configurable: false}

Q2. (난이도 ⭐⭐⭐⭐)

const study = {}

Object.defineProperties(study, {
  index: {
    value: '16장'
  },
  title: {
    value: '프로퍼티 어트리뷰트',
    writable: true,
    enumerable: true,
    configurable: true
  },
  chapter: {
    get() {
      return `${this.index}.${this.title}`;
    },
    set(string) {
      [this.index, this.title] = string.split('.')
    },
    enumerable: true,
    configurable: true
  }
})

study.chapter = '17장.생성자 함수에 의한 객체 생성'
console.log(study.chapter)
console.log(Object.keys(study))
퀴즈 정답 16장.생성자 함수에 의한 객체 생성
['title', 'chapter']

index는 writable 값을 할당하지 않았기 때문에 암묵적으로 false가 기본값이 된다.
따라서 title 값만 변경이 되기 때문에 답은 '16장.생성자 함수에 의한 객체 생성'이 되는 것이다.

만약 모든 체크박스에 ✔️ 표시를 하고, 문제 3개를 다 맞혔다면,
이 글에 있는 내용은 다 알고 있다는 뜻이기 때문에 당당하게 뒤로 가기를 눌러도 된다! 😎




프로퍼티 어트리뷰트

🤔 프로퍼티 어트리뷰트란?
➞ 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯, 내부 메서드이다.

🤔 그럼 내부 슬롯과 내부 메서드는 뭔데?
➞ 내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드이다. [[...]] 처럼 이중 대괄호로 감싼 형태로 되어 있다. 하지만 자바스크립트 엔진의 내부 로직이므로 일부 내부 슬롯과 메서드를 제외하면 개발자가 직접 접근하거나 호출할 수는 없다.

예외 중 하나는 프로토타입이다. 모든 객체는 [[Prototype]] 내부 슬롯을 갖는다. 이 내부 슬롯은 __proto__를 통해 접근할 수 있다.

const o = {}

o.__proto__ // Object.prototype

자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

프로퍼티의 구분

프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.

✳️ 데이터 프로퍼티
키와 값으로 구성된 일반적인 프로퍼티를 말한다.
프로퍼티 어트리뷰트로는 [[Value]] [[Writable]] [[Enumerable]] [[Configurable]]를 갖고 있다.

  • [[Value]]: 프로퍼티 값을 저장한다.
  • [[Writable]]: 프로퍼티 값의 변경 여부를 나타내며 불리언 값을 갖는다.
  • [[Enumerable]]: 프로퍼티의 열거 가능 여부를 나타내며 불리언 값을 갖는다. false인 경우 for...in 문이나 Object.keys 메서드 등으로 열거할 수 없다.
  • [[Configurable]]: 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값을 갖는다. 단, [[Writable]]이 true라면 프로퍼티 값 변경과 [[Writable]] 값을 false로 변경하는 것은 가능하다.
const person = {
  name: 'Lee'
}

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

Object.getOwnPropertyDescriptor 메서드를 사용하면 프로퍼티 어트리뷰트를 간접적으로 확인할 수있다.
❗️ 이 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 것이 아니다. 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환하는 것이다. (굉장히 헷갈린다.)


🤔 프로퍼티 디스크립터는 또 뭐야?
[[Value]]의 디스크립터 프로퍼티는 value, [[Writable]]의 디스크립터 프로퍼티는 writable다. 쉽게 말해 직접적으로 접근하지 않고 이렇게 간접적으로 접근할 수 있는 것이다.

즉 이 메서드를 호출하면 디스크립터 객체를 반환하기 때문에,
{[[Value]]: "lee", [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true} 가 아닌
{value: "lee", writable: true, enumerable: true, configurable: true} 값을 확인할 수 있다.


✳️ 접근자 프로퍼티
자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티다.
프로퍼티 어트리뷰트로는 [[Get]] [[Set]] [[Enumerable]] [[Configurable]]를 갖고 있다.

  • [[Get]]: 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수다. 즉, getter 함수가 호출되고 결과가 프로퍼티 값으로 반환된다.
  • [[Set]]: 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수다. 즉, setter 함수가 호출되고 결과가 프로퍼티 값으로 저장된다.
const person = {
  firstName: 'Haha',
  lastName: 'Lee',
  
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  },
  
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(' ')
  }
}

// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
person.fullName = 'Wow Lee'
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(person.fullName)

접근자 프로퍼티는 데이터 프로퍼티와 다르게 get, set 이름으로 메서드를 생성하면 이 메서드들은 getter/setter 함수의 역할을 한다. 여기서 함수의 이름인 fullName이 바로 접근자 프로퍼티가 된다.


프로퍼티 정의

객체에 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존의 프로퍼티의 프로퍼티 어트리뷰트를 재정의할 수 있다.
즉, [[Value]] [[Writable]] [[Enumerable]] [[Configurable]] 값을 변경할 수 있는 것이다. 물론, 직접적으로 접근은 안된다 했으니 o.[[Value]] = 'hi' 처럼 변경하는 것은 당연히 아니다.

✳️ Object.defineProperty

const person = {}

Object.defineProperty(person, 'firstName', {
  value: 'Ho',
  writable: true,
  enumerable: true,
  configurable: true
})

Object.defineProperty(person, 'lastName', {
  value: 'Lee'
})

이렇게 defineProperty 메서드의 인수로 객체의 참조, 데이터 프로퍼티 키, 프로퍼티 디스크립터 객체를 전달해서 어트리뷰트를 직접적으로 정의할 수 있다.
기본적으로 프로퍼티 디스크립터 객체에서 생략된 어트리뷰트는 기본값이 적용된다.

✳️ 어트리뷰트 기본값

  • [[Value]]: undefined
  • [[Get]]: undefined
  • [[Set]]: undefined
  • [[Writable]]: false
  • [[Enumerable]]: false
  • [[Configurable]]: false

위의 코드에서 'lastName' 키는 value 값만 정의했으므로, writable, enumerable, configurable 값은 모두 false가 기본값이 되었을 것이다.

💡 참고로 프로퍼티 디스크립터 객체는 여러 개를 호출할 수 있고, 정의할 수 있다.
Object.getOwnPropertyDescriptors(프로퍼티 키)
Object.defineProperties(프로퍼티 키, 디스크립터 객체들)


객체 변경 방지

Object.defineProperty 메서드를 사용해 명시적으로 프로퍼티 어트리뷰트 값을 변경해줄 수 있지만, 기본적으로 자바스크립트는 객체의 변경을 방지하는 메서드인 Object.preventExtensions, Object.seal, Object.freeze를 제공해주고 있다.


✳️ Object.preventExtensions

객체의 확장을 금지한다. 즉, 해당 객체에 프로퍼티 추가가 금지되는 것이다. 프로퍼티를 추가하는 방법은 동적 추가와 Object.defineProperty 메서드를 사용하는 방법이 있는데, 이 방법이 모두 금지된다.
확장이 가능한 객체인지 여부는 Object.isExtensible 메서드로 확인할 수 있다.

const person = {name: 'hyori'}
// 확장 금지
Object.preventExtensions(person)
Object.isExtensible(person)
// false 반환

Object.getOwnPropertyDescriptor(person, 'name')
// {value: 'hyori', writable: true, enumerable: true, configurable: true} 반환

이 메서드는 writable, enumerable, configurable이 모두 true로 유지된다.


✳️ Object.seal

객체를 밀봉한다. 밀봉된 객체는 읽기와 쓰기만 가능하다.
밀봉된 객체인지 여부는 Object.isSealed 메서드로 확인할 수 있다.

const person = {name: 'hyori'}
// 프로퍼티 추가, 삭제, 재정의 금지
Object.seal(person)
Object.isSealed(person)
// true 반환

Object.getOwnPropertyDescriptor(person, 'name')
// {value: 'hyori', writable: true, enumerable: true, configurable: false} 반환

이 메서드는 configurable 값만 false가 된다.


✳️ Object.freeze

객체를 동결한다. 동결된 객체는 읽기만 가능하다.
동결된 객체인지 여부는 Object.isFrozen 메서드로 확인할 수 있다.

const person = {name: 'hyori'}
// 프로퍼티 추가, 삭제, 재정의, 쓰기 금지
Object.freeze(person)
Object.isFrozen(person)
// true 반환

Object.getOwnPropertyDescriptor(person, 'name')
// {value: 'hyori', writable: false, enumerable: true, configurable: false} 반환

이 메서드는 writable과 configurable 값이 false가 되는 것을 확인할 수 있다.

💡 참고로 중첩 객체를 동결하려면 재귀적으로 Object.freeze 메서드를 호출해야 한다.



요약

용어도 생소하고, 한 번에 이해하기는 어려우니 요약으로 완벽 이해를 하고 넘어가자.

  • [[Value]] [[Writable]] [[Get]]과 같은 내부 슬롯, 내부 메서드를 프로퍼티 어트리뷰트라고 부름
  • 우리는 프로퍼티 어트리뷰트에 직접 접근할 수 없음
  • 프로퍼티 어트리뷰트에 대한 정보를 제공하는 프로퍼티 디스크립터를 통해 간접적으로 접근할 수 있음
  • 바로 Object.getOwnPropertyDescriptor 메서드를 이용하는 방법
  • 프로퍼티 디스크립터 값을 변경해 프로퍼티 어트리뷰트를 정의할 수도 있음
  • 바로 Object.defineProperty 메서드를 이용하는 방법
  • 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 나뉨
  • 데이터 프로퍼티의 디스크립터 프로퍼티는 value, writable, enumerable, configurable이 있음
  • 접근자 프로퍼티의 디스크립터 프로퍼티는 get, set, enumerable, configurable이 있음
  • 디스크립터 프로퍼티를 생략하면 기본값이 undefined와 false로 적용됨
  • Object.preventExtensions 메서드는 프로퍼티 추가 금지, 하지만 기존 값 변경 가능. 어트리뷰트 재정의 가능
  • Object.seal 메서드는 프로퍼티 삭제와 어트리뷰트 재정의 금지, 하지만 기존 값 변경 가능.
  • Object.freeze 메서드는 프로퍼티 추가, 삭제, 어트리뷰트 재정의, 기존 값 변경 모두 불가능, 하지만 값 읽기는 가능
  • 🥶 실무에서는 잘 사용하지 않음

const amazingPerson = {
  zzang: 'hyori'
}
  
Object.freeze(amazingPerson)

이제 짱은 바뀌지 않는다.

profile
FE 개발자

0개의 댓글