자바스크립트 완벽 가이드 14장 정리 ( Meta Programming )
포스트
취소

자바스크립트 완벽 가이드 14장 정리 ( Meta Programming )

해당 포스트는 자바스크립트 완벽 가이드라는 교재로 스터디를 하면서 14장을 정리한 포스트입니다.
주관적으로 해석한 내용이 들어가 있어서 잘못된 내용이 포함될 수 있습니다.
또한 교재의 모든 내용을 정리하지 않고 주관적인 판단에 의해 필요한 내용만 작성했습니다.

메타 프로그래밍이란 코드를 조작하는 코드를 작성하는 것을 의미합니다.

🌱 프로퍼티 속성

객체의 프로퍼티에는 키와 값이 있지만, 내부적으로 숨겨진 세 가지 속성이 있습니다.
기본적으로는 생성 시 세 가지 속성은 모두 열려있습니다.

0️⃣ 값 ( value )

1️⃣ 쓰기 가능 ( writtable )

프로퍼티 값을 바꿀 수 있는지에 대한 속성입니다.

2️⃣ 열거 가능 ( enumable )

프로퍼티들을 열거할 수 있는지에 대한 속성입니다.

3️⃣ 변경 가능 ( enumable )

프로퍼티를 삭제 혹은 변경 가능한지에 대한 속성입니다.

4️⃣ 프로퍼티 속성 확인

정적 메서드인 Object.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptors()를 사용하면 됩니다.
메서드 이름에서 추론이 가능하듯이 본인이 가지고 있는 프로퍼티만 검색이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const person = { name: "alice", age: 26 };

console.log(Object.getOwnPropertyDescriptor(person, "name"));
/**
 * {
 *   value: 'alice',
 *   writable: true,
 *   enumerable: true,
 *   configurable: true
 * }
 */
console.log(Object.getOwnPropertyDescriptors(person));
/**
 * {
 *   name: {
 *     value: 'alice',
 *     writable: true,
 *     enumerable: true,
 *     configurable: true
 *   },
 *   age: { value: 26, writable: true, enumerable: true, configurable: true }
 * }
 */

5️⃣ 프로퍼티 정의

Object.defineProperty()Object.defineProperties()를 사용할 수 있습니다.
value의 기본 값은 undefined이고 writable, enumerable, configurable의 기본 값은 false입니다.

1
2
3
4
5
6
7
const person = {};

Object.defineProperty(person, "name", {
  value: "alice",
});

console.log(Object.keys(person)); // []
1
2
3
4
5
6
7
8
9
10
11
12
13
const person = {};

Object.defineProperties(person, {
  name: {
    value: "alice",
  },
  age: {
    value: 26,
    enumerable: true,
  },
});

console.log(Object.keys(person)); // ["age"]

🥠 객체 확장성

객체에 새로운 프로퍼티를 추가할 수 있는지를 결정하는 속성입니다.
기본적으로는 확장 가능합니다.

0️⃣ 객체 확장 가능성 확인

Object.isExtensible()을 이용해서 확인합니다.

Object.isSealed()로 확인이 가능합니다.

Object.isFrozen()로 확인이 가능합니다.

1️⃣ 객체 확장 불가능으로 만들기

Object.extensible()을 이용하면 객체의 확장성을 막을 수 있습니다.
하지만 프로토타입에는 프로퍼티를 추가할 수 있습니다.

Object.seal()은 확장 불가능 + 자체 프로퍼티 변경 불가능하게 만듭니다.

Object.freeze()는 확장 불가능 + 프로퍼티 변경 불가능하게 만듭니다.

1
2
3
4
5
6
7
8
const person = { name: "alice" };

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

// Object.extensible(), Object.freeze() 모두 사용 가능하고, 상황에 맞게 판단해서 사용하면 됨
Object.seal(person);

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

🪜 프로토타입 속성

객체에서 프로토타입 프로퍼티에 접근할 수 있는 방법이 두 가지 있습니다.

0️⃣ Object.getPrototypeOf()와 Object.setPrototypeOf()

특정 객체의 프로토타입 프로퍼티를 가져오기 및 수정하는 방법입니다.

1
2
3
const person = {};

console.log(Object.getPrototypeOf(person) === Object.prototype); // true

1️⃣ __proto__

특정 객체의 프로토타입 프로퍼티를 가져오기 및 수정하는 방법입니다.
( __proto__를 사용하는 것을 권장하지 않습니다. )

1
2
3
const person = {};

console.log(Object.getPrototypeOf(person) === Object.prototype); // true

📲 잘 알려진 심벌

심벌 타입의 등장 목적은 이미 배포된 코드의 호환성을 유지하면서 안전하게 확장하기 위함입니다.

0️⃣ Symbol.iterator

이터러블을 위해 사용되는 심벌입니다.

1️⃣ Symbol.hasInstance

만약 정의되어 있다면 instanceof 연산자의 연산에 사용됩니다.

l instanceof r이라고 가정한다면 r[Symbol.hasInstance](instance){}가 정의되어 있다면 해당 메서드를 사용합니다.
매개변수인 instance의 자리에는 l이 들어갑니다.

1
2
3
4
5
6
7
8
const person = {
  [Symbol.hasInstance](instance) {
    return instance.length >= 5;
  },
};

console.log("4444" instanceof person); // false
console.log("55555" instanceof person); // true

2️⃣ Symbol.toStringTag

만약 정의되어 있다면 toString() 메서드의 반환 값으로 사용합니다.

1
2
3
4
5
6
7
8
9
const person1 = {};
const person2 = {
  get [Symbol.toStringTag]() {
    return "내가 정의한 이름으로 반환";
  },
};

console.log(person1.toString()); // [object Object]
console.log(person2.toString()); // [object 내가 정의한 이름으로 반환]

3️⃣ Symbol.species

Array.prototype.map()과 같은 메서드는 새로운 배열(Array)를 반환하는 메서드입니다.

일반적으로 Array를 상속받은 클래스를 통해 만든 인스턴스에서 호출한다면 호출한 본인의 인스턴스를 반환하게 됩니다.

이렇게 동작하는 이유는 새로운 인스턴스를 생성할 때는 new this.constructor[Symbol.species]()를 호출한 것과 같은 것을 생성합니다.

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
class MyArray1 extends Array {
  get first() {
    return this[0];
  }
}

const arr11 = new MyArray1(1, 2, 3);
const arr12 = arr11.map((v) => v);

console.log(arr11.first); // 1
console.log(arr12.first); // 1

class MyArray2 extends Array {
  static get [Symbol.species]() {
    return Array;
  }

  get first() {
    return this[0];
  }
}

const arr21 = new MyArray2(1, 2, 3);
const arr22 = arr21.map((v) => v);

console.log(arr21.first); // 1
console.log(arr22.first); // undefined

console.log(arr21 instanceof MyArray2); // true
console.log(arr22 instanceof MyArray2); // false

4️⃣ Symbol.isConcatSpreadable

TODO:

5️⃣ 패턴 매칭 심벌

TODO:

6️⃣ Symbol.toPrimitive

TODO:

7️⃣ Symbol.unscopables

TODO:

🩻 템플릿 태그

템플릿 리터럴과 태그된 템플릿 리터럴이 있습니다.

0️⃣ 템플릿 리터럴

백틱으로 감싸고 ${v}를 찾으면 내부의 v를 변수로 생각하고 평가한 결과를 사용합니다.

1
2
3
const name = "Aatrox";

console.log(`내 이름은 "${name}"`); // 내 이름은 "Aatrox"

1️⃣ 태그된 템플릿 리터럴

graphQL, styled-components에서 주로 사용합니다.

아래 코드는 태그된 템플릿 리터럴을 사용해서 styled-components를 흉내낸 예시입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const func = (strings, ...args) => {
  const result = strings.map((string, i) => {
    if (typeof args[i] === "function") {
      return string + args[i]({ size: 10, color: "red" });
    }

    return string;
  });

  return result.join("");
};

const result = func`
  font-size: ${(props) => props.size}px;
  backgroundColor: ${(props) => props.color};
`;

console.log(result);
/**
 *  font-size: 10px;
 *  backgroundColor: red;
 */

🧩 Reflect API

생성자 함수가 아닌 특별한 함수들을 모아놓은 객체면서, 함수와 그 프로퍼티를 반영하는 API입니다.

0️⃣ Reflect.apply(f, o, args)

Function.prototype.apply(o, args)와 동등합니다.

1️⃣ Reflect.construct(c, args[, newTarget])

생성자 함수를 호출한 것과 거의 동일합니다.
newTarget이 있다면 생성자에서 new.target의 값이 됩니다.

2️⃣ Reflect.defineProperty(o, name, descriptor)

Object.defindProperty(name, descriptor)와 거의 동일합니다.
성공 여부에 따라 boolean을 반환하는 점만 다릅니다.

3️⃣ Reflect.deleteProperty(o, name)

delete o[name]과 거의 동일합니다.

4️⃣ Reflect.get(o, name[, receiver])

o[name]과 거의 동일합니다.

1
2
3
4
5
6
7
8
9
const person = {
  get name() {
    console.log(this); // { x: 10 }

    return "alice";
  },
};

console.log(Reflect.get(person, "name", { x: 10 }));

5️⃣ Reflect.getOwnPropertyDescriptor(o, name)

Object.getOwnPropertyDescriptor(o, name)와 거의 일치합니다.
첫 번째 인자가 객체가 아니라면 TypeError가 발생합니다.

6️⃣ Reflect.getPrototypeOf(o)

Object.getPrototypeOf(o)와 거의 일치합니다.
첫 번째 인자가 기본 값이면 TypeError를 발생합니다.

7️⃣ Reflect.has(o, name)

name in o와 거의 일치합니다.

8️⃣ Reflect.isExtensible(o, name)

Object.isExtensible(o)와 거의 일치합니다.
첫 번째 인자가 객체가 아니라면 false를 반환합니다.

9️⃣ Reflect.ownKeys(o)

객체 본인의 key들을 모은 배열입니다. ( Symbol 포함 )

1️⃣0️⃣ Reflect.perventExtensions(o)

Object.perventExtensions(o)와 거의 일치합니다.
첫 번째 인자가 객체가 아니라도 TypeError를 발생하지 않고 boolean값을 반환합니다.

1️⃣1️⃣ Reflect.set(o, name, value, receiver)

o[name] = value과 거의 동일합니다.

1
2
3
4
5
6
7
8
9
const person = {
  set name() {
    console.log(this); // { x: 10 }

    return "alice";
  },
};

console.log(Reflect.get(person, "name", "Aatrox", { x: 10 }));

1️⃣2️⃣ Reflect.setPrototypeOf(o, p)

Object.setPrototypeOf(o, p)와 거의 동일합니다.
프로토타입을 변경하는 행동은 하지 않는 것이 좋습니다.

🎲 Proxy 객체

객체의 기본적인 동작을 직접 구현하는데 사용합니다.

핸들러 객체에 행위가 정의되어 있다면 정의된 행위를 실행하고, 그게 아니라면 대상 객체에서 직접 동작을 수행하는 방식으로 동작합니다.

객체의 기본 동작 자체를 막아버리고 다른 행동을 할 수도 있고, 객체의 기본 동작에 추가적인 기능을 부여할 수도 있습니다.

아래는 ProxyReflect를 이용해서 프로퍼티에 접근하는 경우 로그를 찍는 추가적인 기능을 적용한 예시입니다.

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
32
33
34
35
36
37
38
39
const person = {
  name: "Aatrox",
  age: 26,
  gender: true,
};

const proxyPerson = new Proxy(person, {
  get(target, key, receiver) {
    console.log(`get: ${key.toString()} 호출`);

    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`set: ${key.toString()} => "${value}" 호출`);

    return Reflect.set(target, key, value, receiver);
  },
});

console.log(proxyPerson.name);

proxyPerson.name = "Puppy";

console.log(proxyPerson.name);

/**
 * get: name 호출
 * Aatrox
 * set: name => "Puppy" 호출
 * get: name 호출
 * Puppy
 */

({ ...proxyPerson });
/**
 * get: name 호출
 * get: age 호출
 * get: gender 호출
 */

기능만 보면 활용하면 많은 것을 할 수 있을 것 같은데 아직 사용 경험이 없고 어떤 식으로 사용해야 할지 감이 잘 안 잡혀서 실제로 사용해보게 되고 난 뒤에 추가적인 내용을 작성하겠습니다.

📮 레퍼런스

  1. « 자바스크립트 완벽 가이드 14장 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )
  2. 1-blue - 이터러블
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

스토리북 기본 사용법 ( Storybook + webpack )

styled-components + TypeScript 세팅 및 사용 예시