Skip to main content

Command Palette

Search for a command to run...

Decorator

Updated
6 min readView as Markdown
Decorator

Decorator는 아직 정식으로 추가된 JS의 feture가 아닙니다.

그럼에도 사람들에게 널리 유용하게 쓰이는 기능입니다! 연구도 활발히 진행되고 있구요!

대체 뭐길래? 그럴까요?

Decorator의 사용

보통 babel이나 TypeScript를 통해 사용되고 있습니다.

TypeScript config중에 "experimentalDecorators" 를 true로 해주면 사용이 가능하죠.

Decorator란?

ES7 스펙 명세에는 Decorator를 아래와 같이 설명하고 있습니다.

선언된 클래스와 그 프로퍼티들을 디자인 시간에 변경할 수 있는 편리한 문법

그럼 어떤 기능인지 한 번 살펴보시죠!

Decorator를 왜, 어디에 사용하나요?

Decorator는 일종의 wrapper처럼 활용 될 수 있습니다! Util함수 처럼 여기저기서 많이 활용되어야 하는 함수에 적합하죠!

클래스의 프로퍼티를 읽기 전용으로 만든다던지 열거 대상에서 제외할 수 있습니다.

Decorator의 활용은 여기서 끝나지 않습니다. 메모이제이션을 하는 메서드를 만들수 있고 클래스에 자동으로 바인드된 메서드로 만들 수도 있습니다.

React에선 무려 HOC처럼 활용할 수도 있죠...!!

Descriptor(설명자)

Decorator를 이해하기 전에 Descriptor(설명자)에 대해 알아야 하지만 이것은 ref로 붙여놓고 Decorator 자체에 좀 더 집중해보겠습니다!

https://wonism.github.io/what-is-decorator/

https://kelly-kh-woo.medium.com/javascript-decorator-descriptor-fd7e982a64ee

https://ui.toast.com/weekly-pick/ko_20200102

MetaProgramming

Decorator를 알아보는데 갑자기 뜬금없이 웬 MetaProgramming인가 싶을 것입니다.

눈치 채셨겠지만, Decorator와 같은 프로그래밍 방법이 MetaProgramming이기 때문입니다.

그래서 개념을 한 번 알아보고 넘어가겠습니다!

“Metaprogramming” is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze, or transform other programs, and even modify itself while running.

너무 기네요;;; 아찔하군요 😵 😵

정리하자면 한 프로그램이 다른 프로그램을 읽고 만들고 분석하고 수정할 수 있도록 하는 프로그래밍 테크닉 이며,

다른 프로그램에 영향을 주는 프로그램을 "MetaProgram"이라하고

이런 Program을 만들어 사용하는 것을 "MetaProgramming"이라고 합니다.

넓은 의미로는, 런타임에 수행해야 할 작업의 일부를 컴파일 타임 동안 수행하는 프로그램을 말하기도 한다고 하네요. (ref. wiki백과)

Decorator는 두가지 개념들이 모두 해당됩니다.

MetaLanguage

Decorator와는 관계없는 내용이지만, 추가해보았습니다.

완전히 다른 언어로 된 프로그램을 조작하는 프로그램 언어를 "MetaLanguage"라고 합니다.

TypeScript는 JavaScript로 된 프로그램을 만들어내기에 일종의 "MetaLanguage"이며,

Java도 bytecode로 된 프로그램을 만들어내기 때문에 일종의 "MetaLanguage"라고 할 수 있습니다.

반대로 같은 언어로 된 프로그램을 조작하는 프로그램 언어를 "homoiconic language"라고 합니다.("Lisp"가 여기에 해당하고 희귀하다고 합니다.)

ref

https://medium.com/jspoint/a-brief-introduction-to-metaprogramming-in-javascript-88d13ed407b5

https://ko.wikipedia.org/wiki/메타프로그래밍

Decorator

데코레이터는 "데코레이터 함수"(또는 메서드)의 약자입니다.

데코레이터의 모체는 함수라는 이야기이죠~!

그리고 새 함수를 반환하여 전달 된 함수 또는 메서드의 동작을 수정하는 함수입니다.

함수를 1급 시민으로서의 기능을 지원하는 모든 언어는 데코레이터를 구현할 수 있다고 합니다.

파이썬이나 자바에서도 Decorator 기능이 있지요.

그럼 우린 TypeScript와 JavaScript의 Decorator기능을 알아봅시다.

아래의 예시는 Person class에 Logger라는 function을 Decorator로 사용한 예시입니다.

function Logger(constructor: Function) {
    console.log("Loggeing.....");
    console.log(constructor);
}

// Logger function을 사용한 Decorator
@Logger
class Person {
    name = "Jun";

    constructor() {
        console.log("Creating Person Object...");
    }
}

const pers = new Person();
console.log(pers);

![decoratorExample]({{ "../assets/img/TypeScript/decorator/decoratorExample.png" | relative_url }})

console의 결과를 보셨듯, 기존 Person class대로라면 "Creating Person Object..."와 Person instance만 출력되어야 했으나,

그 위에 Logger function의 내용이 추가 되었습니다.

Decorator의 정의대로 Person class에 Logger function의 코드가 추가 되어 함께 작동하고 있는 것을 확인 할 수 있습니다.

Decorator Factory

기존의 Decorator function를 한번 감싸서 다른 데이터를 받아들인 뒤 Decorate해줄 수도 있습니다.

이 Decorator function을 감싸는 함수를 "Decorator Factory"라고 합니다.

이렇게 함으로써, Decorator function 외부의 다른 정보를 받아올 수 있게 되었습니다!

아래의 코드는 위의 코드를 Decorator Factory형태로 개조한 것입니다.

function Logger(logString: string) {
    // Decorator function을 return한다.
  return function(constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

// Decorator function을 한번 감쌋으므로 실행해야한다.
@Logger("Logging - Person")
class Person {
  name = "Max";

  constructor() {
    console.log("Creating person object...");
  }
}

const pers = new Person();
console.log(pers);

![decoratorFactoryExample]({{ "../assets/img/TypeScript/decorator/decoratorFactoryExample.png" | relative_url }})

console 맨 윗줄에 Decorator Factory의 parameter가 출력된 것을 확인 할 수 있습니다!

Decorator의 실행 순서

Decorator는 일반 프로그래밍과 비교하면 실행 순서도 다르고 그 특성도 다르고, 언뜻 봐서는 뭐가 어떻게 실행되는 건지 알 수 없습니다.

따라서 헷갈릴 수 있는 Decorator의 실행 순서를 한 번 짚고 넘어가 보겠습니다!

Instance화 되기 전에 실행 된다.

가장 중요한 Decorator의 특성은 Decorator가 걸린 function 혹은 class가 instance화 될 때, 실행 되는 것이 아닌 선언 되었을 때 실행 된다는 것입니다.

즉, JS가 class 혹은 function의 정의를 읽을 때, 실행된다는 것입니다.

// constructor는 class를 본따 만든 객체 원형이 들어가 있다. (Prototype 개념 참고)
function Logger(constructor: Function) {
    console.log("Loggeing.....");
    console.log(constructor);
}

@Logger
class Person {
    name = "Jun";

    constructor() {
        console.log("Creating Person Object.....");
    }
}

// 여기서 멈춤!
debugger;
const pers = new Person();
debugger;
console.log(pers);

![decorator_beforeRunning]({{ "../assets/img/TypeScript/decorator/decorator_beforeRunning.png" | relative_url }})

위에서 확인 할 수 있듯이 instance화 되기 전에 Decorator가 먼저 실행됩니다.

Decorator가 여러개인 경우

위에서부터 @1, @2가 있다고 가정했을 때, 실행순서는 아래와 같습니다.

@1 Factory function → @2 Factory function → @2 Decorator function → @1 Decorator function

즉, Factory는 평상시처럼 위에서 아래로, Decorator는 아래서 위로 적용됩니다.

아래와 같은 코드가 있다고 했을때, 실행하면 다음과 같은 결과가 나옵니다.

function Logger(logString: string) {
  console.log("Logger Factory");
  return function(constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

function WithTemplate(template: string, hookId: string) {
  console.log("Template Factory");
  return function(constructor: any) {
    console.log("!!Rendering Template");
    const hookEl = document.getElementById(hookId)!;
    const p = new constructor();
    if (hookEl) {
      hookEl.innerHTML = template;
      hookEl.querySelector("h1")!.textContent = p.name;
    }
  }
}

// @1 = Logger, @2 = WithTemplate
@Logger("Logging - Person")
@WithTemplate("<h2>Rendered!!</h2><h1></h1>", "root")
class Person {
  name = "Jun";

  constructor() {
    console.log("Creating person object...");
  }
}

// 여기서 멈춤!
debugger;
const pers = new Person();
debugger;
console.log(pers);

![decorators]({{ "../assets/img/TypeScript/decorator/decorators.png" | relative_url }})

위에서 나열된 순서대로 실행되었습니다!

여러 형태의 Decorator

class나 function 그 자체에 붙이는 것 이외에도 아래와 같은 곳에도 가능합니다.

  • property
  • parameter
  • accessor
  • method

나열하고 보니 생각할 수 있는 거의 모든 곳에 사용할 수 있네요...!

실행 순서는 class Decorator와 같이 instance화 되기 전에 미리 실행이 됩니다.

사용시, 주의해야 할 점은!

어디에 Decorator를 붙여주냐에 따라 Decorator function에서 받는 parameter들이 조금씩 다릅니다.

아래는 적용 예시입니다.

// property Decorator
function Log(target: any, propertyName: string | Symbol) {
  console.log('Property decorator!');
  console.log(target, propertyName);
}

// accessor Decorator
function Log2(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log('Accessor decorator!');
  console.log(target);
  console.log(name);
  console.log(descriptor);
}

// method Decorator
function Log3(
  target: any,
  name: string | Symbol,
  descriptor: PropertyDescriptor
) {
  console.log('Method decorator!');
  console.log(target);
  console.log(name);
  console.log(descriptor);
}

// parameter Decorator
function Log4(target: any, name: string | Symbol, position: number) {
  console.log('Parameter decorator!');
  console.log(target);
  console.log(name);
  console.log(position);
}

class Product {
    // property Decorator
  @Log
  title: string;
  private _price: number;

    // accessor Decorator
  @Log2
  set price(val: number) {
    if (val > 0) {
      this._price = val;
    } else {
      throw new Error('Invalid price - should be positive!');
    }
  }

  constructor(t: string, p: number) {
    this.title = t;
    this._price = p;
  }

    // method, parameter Decorator
  @Log3
  getPriceWithTax(@Log4 tax: number) {
    return this._price * (1 + tax);
  }
}

const p1 = new Product('Book', 19);
const p2 = new Product('Book 2', 29);

Decorator로 기존의 logic을 수정

지금까지는 Decorator의 logic을 추가하는 것만 확인해보았습니다.

Decorator function에 return을 주지 않고 있죠...!

하지만 이번엔 Decorator로 기존 logic을 수정해보도록 하겠습니다...!

위에서 사용했던 로직을 재활용하여 아래 함수를 읽기 전용으로 바꾸어 보겠습니다.

function Log3(
  target: any,
  name: string | Symbol,
  descriptor: PropertyDescriptor
) {
  console.log('Method decorator!');
  console.log(target);
  console.log(name);
  console.log(descriptor);
    // 이 Decorator function이 적용된 것은 readonly가 됩니다.
    return ({
        writable: false
    });
}

class Product {
  title: string;
  private _price: number;

  @Log3
  getPriceWithTax(@Log4 tax: number) {
    return this._price * (1 + tax);
  }
}

const p1 = new Product('Book', 19);
// p1.getPriceWithTax = "sdsd"; X error

More from this blog

SSM으로 private DB에 안전하게 연결

SSE를 통해 private DB를 외부에 노출하지 않고 접근할 수 있는 방법을 알아보겠습니다. private Instance에 접근하기 중요한 데이터와 비즈니스 로직이 들어있는 Instance들의 경우에는 보안을 위해 인터넷에 노출시키지 않고 사설망에 두고 운영하는 경우가 많습니다. 사설망에 있는 Instance들을 private Instance라고 합니다. 인터넷에 노출되지 않아 보안이 강력해지지만 관리자들도 접근할 수 없게되어 privat...

Sep 12, 20254 min read
SSM으로 private DB에 안전하게 연결

OIDC(OpenID Connect)

Google 로그인 등에 사용되는 OIDC(OpenID Connect)는 OAuth2.0 프로토콜을 기반으로 하고 있습니다. OAuth 2.0이 인증만 제공하는 반면, OIDC는 인증 기능을 추가하여 사용자 인증 및 인증 시나리오에 대한 보다 표준화된 솔루션을 제공합니다. 간단히 말해: OIDC = 인가 프로토콜 + 신원 인증 프로토콜 입니다. OIDC의 등장 배경 여러 서비스가 생겨나고 각 서비스는 각자 사용자 정보를 관리하게 됩니다. 하지만...

Sep 11, 20254 min read
OIDC(OpenID Connect)

PKCE & OAuth 2.1

기존의 OAuth 2.0을 보완하기 위한 여러 시도가 있습니다. 그 중 PKCE 그리고 더 발전된 형태인 OAuth 2.1에 대해 간략히 알아보겠습니다. PKCE (Proof Key for Code Exchange) PKCE는 악의적인 애플리케이션이 인증 코드를 가로채는 것을 방지하는 방법에 대한 내용입니다. OAuth 2.0 인증과정 중 Authorization code가 탈취 OAuth 2.0의 흐름 중 다음과 같은 과정이 있습니다. Re...

Sep 11, 20252 min read
PKCE & OAuth 2.1

OAuth 2.0

OAuth 2.0 프로토콜은 외부 서비스의 권한 위임에 위해 사용되는 프로토콜입니다. OAuth 2.0 RFC 공식 문서 OAuth2.0은 왜 필요했을까? 일반 사용자들이 앱을 통해서 Google이나 제 3자 서비스들에 저장된 자기 정보에 접근하기 위해서 가장 간단한 방법은 제 3자 서비스의 비밀번호를 앱에 공유하는 것입니다. 하지만, 이러면 "앱"은 사용자들의 제 3자 서비스 계정의 비밀번호를 알게 되고 관리해야하며, 이는 보안상 중요한 정보...

Sep 11, 20253 min read
OAuth 2.0

관찰 가능성 시스템 구현기 2 - 구축과 적용

이번 포스팅에선 정의한 문제를 해결하기 위해 관찰 가능성 시스템을 구축하고 적용하기 위한 과정과 어떤 효과가 있었는지에 대해 공유하고자 합니다. 서론 일전엔 러프하게 Grafana 생태계를 고려한다고 했지만, 구체적으로 무엇을 어떻게 구현할지는 아직 정하지 않았으며 어떤 인프라를 이용해 구현할지도 아직 정하지 않았습니다. 구체적인 선택의 과정과 구현 과정을 공유하고자 합니다. 또한, 이 완성된 시스템이 조직의 논의를 통해 점진적으로 적용되는 과...

Sep 9, 20255 min read
관찰 가능성 시스템 구현기 2 - 구축과 적용
K

KyungJun Woo | Software Engineer

30 posts