티스토리 뷰

현재 주로 사용하고 있는 웹 프레임워크는 Nest.js 라는 프레임워크이다. 자세한것은 생략하지만, Nest.js에서는 메타데이터 프로그래밍을 제공한다. 그리고 이를 위해 Setmetadata 혹은 createDecorator 등 사용자가 메타데이터에 대한 커스텀이 가능하도록 API를 제공하며, 사용되는 @Controller, @Service등 Nest.js의 리소스들도 모두 Metadata기반으로 DI, IoC를 동작시킨다.

 

이 모든것을 가능하게 해주는 핵심 라이브러리중 하나가 바로 Reflect-Metadata이다. 리플렉션이란 자기 자신을 프로그래밍 하는것을 의미한다. 쉽게 말하면 코드 내에 있는 메소드, 타입, 변수등에 대한 정보를 조작할 수 있는것을 의미한다.

 

https://www.npmjs.com/package/reflect-metadata

 

reflect-metadata

Polyfill for Metadata Reflection API. Latest version: 0.2.1, last published: a month ago. Start using reflect-metadata in your project by running `npm i reflect-metadata`. There are 18960 other projects in the npm registry using reflect-metadata.

www.npmjs.com

 

Nest.js를 조금 더 깊게 이해하기 위해 앞으로 이 패키지를 깊게 공부해보려고 한다. 이를 위해서 사전에 공부하는 내용들을 하나씩 학습하며 접근할 것이다.

 

우선 이번 포스팅에서는 자바스크립트에서 제공하는 Object Property Attribute에 대해 알아볼 것이다. 아래 포스팅을 볼때 ECMAScript 사양의 Property Attribute를 함께 참고하는것이 좋다.(https://tc39.es/ecma262/#sec-property-attributes)

 

Property Attribute와 Descriptor(서술자) 객체

JavaScript Object & Descriptor 출력해보기

자바스크립트의 객체는 아래와 같이 중괄호로 묶어주거나 Object생성자를 통해 간단히 선언이 가능하다.

const a = new Object();
const b = {};

console.log(typeof a); // object
console.log(typeof b); // object

그리고 객체는 Key:Value쌍을 값으로 가지게 된다. 그리고 이 각각의 구성 요소를 프로퍼티(Property, 속성) 이라고 부른다. 프로퍼티를 생성하는 방법은 간단하다.

const a = {};

a.key = "value";
a["key2"] = "value2";

console.log(a); // { key: 'value', key2: 'value2' }

자바스크립트 엔진은 프로퍼티를 생성할때 "프로퍼티의 상태"를 나타내는 프로퍼티 어트리뷰트(Property Attribute)를 기본값으로 자동 정의한다. 프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 슬롯들([[Value]],[[Writable]],[[Enumerable]],[[Configurable]])을 의미한다. 사용자는 이 내부 상태값에 직접 접근할 수 없지만, 프로퍼티 디스크립터(Property Descriptor)를 통해 간접적으로 확인할 수 있다(재정의는 가능하나, 수정은 불가능하다).

내부 슬롯과 내부 메소드
내부슬롯과 내부 메소드는 자바스크립트 엔진의 구현을 설명하기 위해 ECMAScript 사양에서 사용하는 Pseudo Property와 Pseudo Method이다. ECMAScript 표준서에서 사용하는 이중 대괄호가 내부 슬롯 혹은 내부 메소드를 의미한다.

ECMA 262: https://tc39.es/ecma262/
ECMA Property Attribute: https://tc39.es/ecma262/#sec-property-attributes

 

프로퍼티 상태라는것은 무엇일까? 우선 프로퍼티를 한번 출력해보자. 프로퍼티를 출력하기 위해서는 Object.getOwnPropertyDescriptor(<Object>,<Property Name>)를 사용하면 된다.

 

Object.getOwnPropertyDescriptor 메소드는 대상 Object의 특정 Property에 대한 Descriptor를 출력한다.혹은 Object Property 각각의 Descriptor를 반환하기 위해서는 Object.getOwnPropertyDescriptors(<Object>)를 사용하면 된다.(다만 이는 ES8부터 도입되었다).

const fruit = {
  apple: 3000,
  orange: 4000,
};

/**
{ value: 3000, writable: true, enumerable: true, configurable: true }
{ value: 4000, writable: true, enumerable: true, configurable: true }
 */
console.log(Object.getOwnPropertyDescriptor(fruit, "apple"));
console.log(Object.getOwnPropertyDescriptor(fruit, "orange"));
console.log(Object.getOwnPropertyDescriptor(fruit, "something")); // 존재하지 않으면 undefined

//   {
//     apple: { value: 3000, writable: true, enumerable: true, configurable: true },
//     orange: { value: 4000, writable: true, enumerable: true, configurable: true }
//   }
console.log(Object.getOwnPropertyDescriptors(fruit));

 

각각의 프로퍼티 별로 value, writable, enumerable, configurable 이라는 값이 담긴 객체를 가지고 있는 객체가 반환된다. 그리고 이것이 앞에서 말했던 Property Descriptor이다. 

Data Property & Accessor Property

객체에서 선언할 수 있는 프로퍼티는 두 종류로 나누어 진다. 

  • 데이터 프로퍼티:  값을 데이터로 가지고 있는 프로퍼티이다.(위에서 보았던 형태)
  • 접근자 프로퍼티: 값이 아닌 다른 데이터 프로퍼티의 값을 읽거나 저장할때 호출되는 접근자 함수로 구성된 프로퍼티이다. 흔히 Getter, Setter를 의미한다. Getter, Setter는 함수의 형태이나, 외부 코드에서는 일반적인 데이터 프로퍼티처럼 활용한다(쉽게 말하면 호출하는 형식의 사용 x).

데이터 프로퍼티와 접근자 프로퍼티가 가지는 Property Descriptor는 서로 다르다. 각각의 종류별로 어떤 값을 가지는지 살펴보자

 

데이터 프로퍼티

프로퍼티 어트리뷰트(ECMA 내부슬롯) 프로퍼티 디스크립터 프로퍼티 설명
[[Value]] value(any)  - 프로퍼티의 값을 의미한다
- 프로퍼티 값을 변경하면, value에 값을 재할당하는것이다
[[Writable]] writable(boolean) - 프로퍼티 값의 변경 가능 여부이다.
- Writable이 false인 경우, value의 값을 재할당할 수 없다
[[Enumerable]] enumerable(boolean) - 프로퍼티의 열거가능여부를 의미한다
- enumerable이 false인 경우에는 for...in 혹은 Object.keys를 통한 열거가 불가능하다.
[[Configurable]] configurable(boolean) - 프로퍼티 재정의 가능 여부를 나타낸다.
- configurable이 false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트의 변경이 불가능해진다.(설정에 대해 lock)
- 단 writable이 true인 경우 value의 값을 변경하거나, writable을
false로 변경하는것은 가능하다.

 

접근자 프로퍼티

프로퍼티 어트리뷰트(ECMA 내부슬롯) 프로퍼티 디스크립터 프로퍼티 설명
[[Get]] get - Getter를 의미한다. 접근자 프로퍼티 Key로 접근하면 Getter 함수가 호출된다
[[Set]] set - Setter를 의미한다. 접근자 프로퍼티 Key로 값을 대입하면(const a = b형태) Setter 함수가 호출된다.
[[Enumerable]] enumerable 데이터 프로퍼티 Enumerable과 동일
[[Configurable]] configurable 데이터 프로퍼티 Configurable과 동일

 

const person = {
  // Data Property
  firstName: "J",
  lastName: "Hoplin",

  // Accessor Property
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set fullName(name) {
    [this.firstName, this.lastName] = name;
  },
};

/**
 * 
{
  firstName: {
    value: 'Yoon',
    writable: true,
    enumerable: true,
    configurable: true
  },
  lastName: {
    value: 'Junho',
    writable: true,
    enumerable: true,
    configurable: true
  },
  fullName: {
    get: [Function: get fullName],
    set: [Function: set fullName],
    enumerable: true,
    configurable: true
  }
}
 * 
 */
console.log(Object.getOwnPropertyDescriptors(person));
console.log(person.fullName); // Getter
person.fullName = ["J", "someone"]; // Setter
console.log(person.fullName); // Getter

 

단순히 프로퍼티가 접근자 프로퍼티인지, 데이터 프로퍼티인지 구분하기 위해서는 Property Descriptor의 프로퍼티 값들로 구분하면 된다.

 

프로퍼티 정의하기

프로퍼티 정의란, 단순히 값을 추가하는것이 아닌, 값을 추가함과 동시에 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존의 프로퍼티를 재정의하는것이다. 프로퍼티 정의는 Object.defineProperty를 통해 정의할 수 있다. 프로퍼티 정의시, 어트리뷰트를 생략할 수 도 있는데, 이런 경우 각 어트리뷰트의 기본값으로 들어가게 된다.

프로퍼티 어트리뷰트(ECMA 내부슬롯) 프로퍼티 디스크립터 프로퍼티 기본값
[[Value]] value undefined
[[Get]] get undefined
[[Set]] set undefined
[[Writable]] writable false
[[Enumerable]] enumerable false
[[Configurable]] configurable false
const person = {};

Object.defineProperty(person, "firstName", {
  value: "J",
  writable: true,
  enumerable: true,
  configurable: true,
});

Object.defineProperty(person, "lastName", {
  value: "Hoplin",
});

Object.defineProperties를 사용하면 여러개의 프로퍼티 정의를 한번에 할 수 있다. 위 예시를 변경해본다.

const person = {};
/**
 * 
 * {
  firstName: { value: 'J', writable: true, enumerable: true, configurable: true },
  lastName: {
    value: 'Hoplin',
    writable: false,
    enumerable: false,
    configurable: false
  },
  fullName: {
    get: [Function: get],
    set: [Function: set],
    enumerable: true,
    configurable: true
  }
}
 */

Object.defineProperties(person, {
  firstName: {
    value: "J",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  lastName: {
    value: "Hoplin",
  },
  fullName: {
    get() {
      return `${this.firstName} ${this.lastName}`;
    },

    set(name) {
      [this.firstName, this.lastName] = name;
    },

    enumerable: true,
    configurable: true,
  },
});

console.log(Object.getOwnPropertyDescriptors(person));

 

객체 변경 방지

객체에 대한 조정을 알아보자. 객체에 프로퍼티를 추가하고 수정하고 삭제하는 등의 행위를 제한할 수 있다. 자바스크립트에서 객체의 변경을 방지하는 메소드들과 제한하는 범위는 아래와 같다.

 

구분 메소드 프로퍼티 추가 프로퍼티 삭제 프로퍼티 값 읽기 프로퍼티 값 쓰기 프로퍼티 어트리뷰터 정의
객체 확장 금지 Object.preventExtensions x o o o o
객체 밀봉 Object.seal x x o o x
객체 동결 Object.freeze x x o x x

 

객체 확장 금지

객체 확장은 새로운 프로퍼티의 추가만 금지(확장 금지)된다. 확장이 가능한지의 여부는 Object.isExtensible를 통해 확인이 가능하다.

"use strict";

const person = { name: "Hoplin" };

console.log(Object.isExtensible(person));

Object.preventExtensions(person);
console.log(Object.isExtensible(person));

person.age = 20; // Exception in strict mode

delete person.name;
Object.defineProperty(person, "age", { value: 20 }); // TypeError

객체 밀봉

객체 밀봉은 새로운 프로퍼티 추가, 기존 프로퍼티 삭제, 프로퍼티 어트리뷰트 재정의(정의 = 새로운 프로퍼티 추가)가 금지된다. 밀봉 상태의 여부는 Object.isSealed를 통해 확인이 가능하다.

"use strict";

const person = { name: "Hoplin" };

console.log(Object.isSealed(person));

Object.seal(person);

console.log(Object.isSealed(person));

person.age = 20; // Exception in Strict Mode

console.log(person);

delete person.name; // Exception in Strict Mode

console.log(person);

Object.defineProperty(person, "name", { configurable: true }); // TypeError

 

객체 동결

객체 동결은 값을 읽는것 외 모든 행동을 금지한다. 동결 여부는 Object.isFrozen을 통해 확인이 가능하다.

"use strict";

const person = {
  name: "yoon",
};

Object.freeze(person);

console.log(Object.isFrozen(person));

person.age = 20; // Exception in strict mode

delete person.name; // Exception in strict mode

// Type Error
Object.defineProperty(person, "name", {
  value: "It will be unchanged",
});

 

객체 변경 방지는 얕은 방지이다.

예를들어 아래와 같이 중첩 객체가 있다고 가정하자.

const person = {
  name: "yoon",
  address: {
    city: "seoul",
    inner: {
      test: "test",
    },
  },
};

person객체를 동결상태로 만들고, 중첩 객체인 address와 person 각각 동결 상태를 확인한다.

Object.freeze(person);

console.log(Object.isFrozen(person)); // true

console.log(Object.isFrozen(person.address)); // false

결과를 보면 중첩객체인 address는 동결되지 않은 상태임을 알 수 있다. 이는 Object.freeze 뿐만 아니라 preventExtensions, seal 모두 얕은 금지를 하기 때문이다. 이를 해결하기 위해서는 재귀적으로 객체 방지를 진행해 주어야 한다.

function deepFronzen(obj) {
  Object.freeze(obj);
  for (const key of Object.keys(obj)) {
    if (typeof obj[key] === "object") {
      deepFronzen(obj[key]);
    }
  }
}

deepFronzen(person);

console.log(Object.isFrozen(person));

console.log(Object.isFrozen(person.address));

 

다음 포스트에서는 JavaScript Proxy API와 Reflect API에 대해 다뤄볼 예정이다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함