Home Modern Javascript Deepdive [ Chapter 16 프로퍼티 어트리뷰트 ]
Post
Cancel

Modern Javascript Deepdive [ Chapter 16 프로퍼티 어트리뷰트 ]

DesktopView

이 게시물은 SW마에스트로 연수생 간 진행한 Modern JavaScript DeepDive 스터디 개인 정리 내용입니다.
되도록 책의 내용을 그대로 적기보단 개인적으로 인상깊었던 부분을 구어체로 풀어쓰려 노력했습니다.

Modern JavaScript Deep Dive

Chapter _ 16 프로퍼티 어트리뷰트

JS 를 쓰면서 프로토 객체의 존재는 알고 있었으나, 프로토 객체 그 너머에 이런 개념이 있다는 사실은 저는 모르고 있었습니다. 조금 더 작동원리에 대해서 알 수 있는 챕터여서 흥미롭고 재밌게 정리할 수 있었던 것 같습니다.

내부 슬롯과 내부 메서드

내부 슬롯과 내브 메서드는 JS 엔진이 구현알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티의사 메서드 입니다. ECMAScript 사양에서의 이중 대괄호로 감싼 이름들이 내부 슬롯과 내부 메서드에 해당한다고 합니다. 각각의 요소들은 JS 엔진 내부에서만 사용 할 내부 로직이므로 이를 개발자가 직접적으로 접근하거나 호출 할 방법을 제공하지 않습니다. 하지만, 이를 간접적으로 접근하는 수단을 제공하기는 합니다.

JS 의 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 갖습니다. 이는 내부 로직이므로 직접 접근할 순 없지만 [[Prototype]] 내부 슬롯의 경우 __proto__ ( 찾아보니 프로토 라고 부른답니다 ) 를 통해 간접적으로 접근할 수 있습니다.

1
2
3
4
const o = {};

o.[[Prototype]] // Syntax Error -> 내부슬롯은 직접 접근 불가능!
o.__proto__ // Object.prototype => 접근 가능!

그래서 이게 왜 필요한 건가요?

이후에 나오는 프로퍼티 어트리뷰트와 프로퍼티 디스크립터의 개념을 설명하기에 앞서 그 배경을 알아보는 항목이었다고 이해하면 좋을 듯 합니다. 그냥 이런게 있구나 ~ 하고 알고만 있어도 좋을 것 같습니다.

프로퍼티 어트리뷰트와 프로퍼티 디스크립터

JS 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 정의합니다.

  • 프로퍼티의 상태란?
    • 프로퍼티의 값 ( value ) → 내부 슬롯 [[Value]]
    • 값의 갱신 여부 ( writable ) → 내부 슬롯 [[Writable]]
    • 열거 가능 여부 ( enumerable ) → 내부 슬롯 [[Enumerable]]
    • 재정의 가능 여부 ( configurable ) → 내부 슬롯 [[Configurable]]

를 말합니다. 여기서 프로퍼티 어트리뷰트 는 상태들을 관리하는 내부슬롯입니다.

앞서 내부 슬롯은 직접 접근하지 못한다는 것을 알 수 있었습니다. 이를 간접적으로 확인할 수 있게 해주는 메서드가 바로 Object.getOwnPropertyDescriptor 입니다.

Object.getOwnPropertyDescriptor 의 첫 번째 매개변수는 객체의 참조, 두 번째 매개변수는 프로퍼티 키를 문자열로 전달하여 프로퍼티 어트리뷰트들의 정보를 제공하는 프로퍼티 디스크립터 객체를 반환받습니다.

데이터 프로퍼티와 접근자 프로퍼티

프로퍼티들은 크게 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있습니다.

  • 데이터 프로퍼티 ⇒ 키와 값으로 구성된 일반적 프로퍼티로 지금까지 본 모든 프로퍼티들은 데이터 프로퍼티 입니다.
  • 접근자 프로퍼티 ⇒ 자체적인 값을 갖지 않고 다른 데이터의 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수만으로 구성된 프로퍼티.

데이터 프로퍼티

데이터 프로퍼티는 네 가지 프로퍼티 어트리뷰트 를 갖습니다.

  • [[Value]]
    • 프로퍼티 키를 통해 접근하면 반환되는 값
    • 프로퍼티 키를 통해 값을 변경하면 [[Value]] 어트리뷰트에 값을 재할당합니다.
    • 만약 프로퍼티가 없다면 프로퍼티를 동적으로 생성하고 생성된 프로퍼티의 [[Value]] 에 값을 저장합니다.
  • [[Writable]]
    • 프로퍼티 값의 변경 가능 여부를 불리언 값으로 나타냅니다.
  • [[Enumerable]]
    • 프로퍼티 값의 열거 가능 여부를 불리언 값으로 나타냅니다.
  • [[Configurable]]
    • 프로퍼티의 재정의 가능 여부를 불리언 값으로 나타냅니다.
    • 여기서 재정의는 프로퍼티 어트리뷰트 재정의를 말하는 것이고, 프로퍼티 값의 변경은 앞선 [[Writable]] 어트리뷰트의 값에 따릅니다.
1
2
3
4
5
6
7
8
9
10
11
const person = { 
	name :'Lee'
};
// 프로퍼티동적생성 
person.age = 20;

console.log(Object.getOwnPropertyDescriptors(person));
{
	name: {value: "Lee",writable: true,enumerable:true,configurable:true},
	age:{value: 20,writable: true,enumerable:true,configurable:true}
}

위 예제를 살펴보자면 name의 프로퍼티 어트리뷰트들은 따로 설명하지 않겠지만,

특이한 부분이라면 동적으로 생성 된 age 프로퍼티 어트리뷰트를 보면 name 의 프로퍼티 어트리뷰트들과 동일한 것을 확인할 수 있습니다.

접근자 프로퍼티

접근자 프로퍼티는 자체적인 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용되는 접근자 함수들로 구성된 프로퍼티입니다.

접근자 프로퍼티들은 다음과 같은 프로퍼티 어트리뷰트를 갖습니다.

  • [[Get]]
    • 접근자 프로퍼티를 통해 프로퍼티의 값을 읽을 때 호출되는 접근자 함수입니다.
    • 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 어트리뷰트 [[Get]] 의 값인 getter 함수가 호출되고, 프로퍼티 값을 반환해줍니다.
  • [[Set]]
    • 접근자 프로퍼티를 통해 프로퍼티의 값을 저장할 때 호출되는 접근자 함수입니다.
    • 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 어트리뷰트 [[Set]] 의 값인 setter 함수가 호출되고, 프로퍼티 값을 저장해줍니다.
  • [[Enumerable]] : 똑같습니다.
  • [[Configurable]] : 똑같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const person = { // 데이터 프로퍼티
	firstName:'Ungmo',
	lastName : 'Lee'
	//fullName은 접근자함수로 구성된 접근자 프로퍼티다.
	//getter 함수
	get fulName() {
	return (`${this.firstName}${this.lastName)`)
	},
	// setter함수
	set fullName(name) {
	[this.firstName, this.lastName] =name.split(' ');
	}
}

// 접근자 프로퍼티를 통한 프로퍼티 값 저장. 여기선 setter 함수가 호출됩니다.
person.fullName = 'CS KIM'
console.log(person) // firstName : "CS", lastName : "KIM"

// 접근자 프로퍼티를 통한 프로퍼티 값 참조. 여기선 getter 함수가 호출됩니다.
console.log(person.fullName) // CSKIM

접근자 프로퍼티를 사용한 간단한 예제입니다. 재미있는 부분은 fullName 을 사용한 방법은 다르지 않으나, 사용된 함수는 상황에 맞게 getter,setter 로 분기되었다는 점입니다. ( 사실 이 부분은 “상황에 맞다” 라는 인식이 이미 JS 에 익숙해져서일지도 모르겠습니다. )

우선 설명을 이어가자면 예제 코드 안에 getter 함수와 setter 함수가 위치하고 그 뒤어 이어오는 함수의 이름 fullName 이 접근자 프로퍼티 입니다.

fullName 프로퍼티는 Value 어트리뷰트를 가지지 않으며, 데이터 프로퍼티의 값을 읽거나 저장할 때 관여할 뿐입니다.

이를 내부 슬롯, 내부 메서드 관점에서 설명하면 다음과 같은 순서로 동작합니다.

  1. 프로퍼티 키가 유효한지 확인합니다.
    1. 프로퍼티 키는 문자열 혹은 심벌이어야 합니다.
  2. 프로토타입 체인에서 프로퍼티를 검색합니다.
  3. 검색된 fullName 프로퍼티가 데이터 프로퍼티인지 접근자 프로퍼티인지 확인합니다.
  4. 접근자 프로퍼티 fullName의 프로퍼티 어트리뷰트 getter 함수를 호출하여 그 결과를 반환합니다.

프로퍼티 정의

새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 기본값이 아닌 명시적인 값으로 정의하거나 기존의 프로퍼티 어트리뷰트를 재정의 하는 것을 말합니다. 이는 Object.defineProperty 메서드를 사용해 재정의 할 수 있고 인수로는 객체의 참조, 데이터 프로퍼티의 키, 프로퍼티 디스크립터의 객체를 전달해야 합니다.

예를 들어 [[Writable]] ,[[Enumerable]] ,[[Configurable]] 를 재정의하여 갱신, 열거, 재정의 가능 여부를 명시하여 정의할 수 있습니다.

  • 만약 프로퍼티 디스크립터 객체를 누락한다면 기본값으로 undefined 혹은 false 가 들어갑니다.
  • [[Writable]]
    • 값이 false 일 경우 해당 프로퍼티의 Value 값을 변경할 수 없습니다.
    • 변경하더라도 에러가 발생하지 않고 무시 됩니다.
  • [[Enumerable]]
    • 값이 false 일 경우 for … in 이나 forEach 등을 통해 열거되지 않습니다.
    • 단순히 불가능이 아니라 다른 항목이 열거될 때 해당 항목은 열거되지 않아 보이지 않습니다.
  • [[Configurable]]
    • 마찬가지로 값이 false라면 프로퍼티를 삭제하려고 할 때 해당 요청은 무시됩니다.
    • 하지만 특징적으로 해당 프로퍼티를 재정의 하려고 할 땐 TypeError (Cannot redefine) 를 반환합니다.

객체 변경 방지

객체는 변경이 가능한 값이어서 재할당 과정 없이 직접 변경할 수 있습니다.

또한 Object.defineProperty 또는 Object.defineProperties 메서드를 사용하여 어트리뷰트 재정의도 가능합니다.

이러한 변경을 방지하기 위해서 JS는 세 가지 메서드를 제공합니다.

  • 객체 확장 금지 - Object.preventExtensions
    • 확장이 금지된 객체는 프로퍼티의 추가가 금지됩니다.
    • 객체의 확장 가능 여부는 Object.isExtensible 메서드를 통해 알 수 있습니다.
  • 객체 밀봉 - Object.seal
    • 밀봉된 객체는 읽기와 쓰기만 가능합니다.
    • 하지만 아시죠? 객체는 쓰기가 가능하다면 값의 갱신도 가능하다는 점.
    • 실질적으론 프로퍼티의 추가, 삭제, 재정의가 금지된다고 이해하시면 빠를 것 같습니다.
    • 객체의 밀봉 여부는 Object.isSealed 메서드를 통해 알 수 있습니다.
  • 객체 동결 - Object.freeze
    • 동결된 객체는 읽기만 가능합니다.
    • 동결된 객체는 읽기를 제외한 모든 요청이 불가능합니다.
    • 동결 객체 여부는 Object.isFrozen 메서드로 확인 가능합니다.
  • 불변 객체
    • 객체 동결을 사용하더라도 중첩객체는 동결이 불가능합니다.
    • 이 경우 재귀를 통해 freeze 메서드를 요청해야 하위 객체들도 동결된 불변객체 생성이 가능합니다.
This post is licensed under CC BY 4.0 by the author.