GODSENAL

Javascript - Decorator 문법

# javascript
2018-10-22 · 4

Javascript가 정말 빨리 바뀌고 있다. 하루가 멀다 하고 새로운 문법들이 추가되고, proposal 들이 추가되고 있다. 이번에는 그 중에서도 많이 쓰이고 있는 Decorator(현재 2018/10/22 기준 state-2)에 대해 알아보자.

Decorator란?

Decorator는 이름과 같이 decorating을 해주는 문법이다. higher-order function과 같이 어떤 코드를 다른 코드로 감싸준다.

function work() {
  console.log('Some work...');
}
function helloWrapper(wrapped) {
  return function(...args) {
    else {
      console.log('Hello!');
      wrapped.apply(this, args);
    }
  }
}

const wrapped = helloWrapper(work);

위 코드는 새로운 함수 wrapped를 반환해준다. 이 함수는 helloWrapper 라는 higher-order function으로 work를 감싸주어 일하기 전에 인사를 추가해주는 함수이다.

work();
// Some work...

wrapped();
// Hello!
// Some work...

위와 같은 higher-order function의 가장 큰 목적은 재사용성일 것이다. 위와 같은 방법으로, hello를 출력하고 싶은 함수마다 console.log('Hello');를 작성해야 하는 번거로운 일은 없어진다.

Decorator도 마찬가지이다. 코드의 재사용성을 높여줄 수 있다.

왜 Decorator를 사용하는가?

higher-order function으로 충분히 가능하지 않은가? 라고 말할 수 있지만, 몇 가지 장점이 있다.

  • Higher-order function보다 적용하기 쉽다. 특히, Class나 Class member들에 적용할 때는 훨씬 편하다.
  • 읽기 쉽고 코드를 조금 더 깔끔하게 할 수 있다.
  • Javascript의 새로운 문법이니 만큼 써보고 싶은 마음이 생긴다..

아직은 Class나 Class mebmer들에게만 적용할 수 있지만, 이 부분에서 higher-order function보다는 조금 더 쉽게 다가온다.

어떻게 사용하는가?

여러 언어에 Decorator가 있다. 기본적으로 어떤 함수를 인자로 받아서 그 함수의 기능을 좀 더 확장시켜 반환하는 함수이다. Javascript에서 Decorator는 타입에 따른 인자를 받아 클래스나 그 멤버들을 변경,확장 시켜주는 함수이다.

Decorator도 함수이므로, 함수를 선언하고 Decorator하면 자주 봤을 @를 사용해 Class 등을 꾸민다.

Class

먼저 Class 자체에 적용하는 decorator를 한 번 만들어 보며 어떻게 적용되는지 보자. Class의 Decorator는 타겟 Class의 생성자를 인자로 받는다.

// 아까 말했듯이, 그냥 함수다.
function whiteColor(target) {
  target.prototype.color = "white";
}

@whiteColor
class Paint {}

const snow = new Paint();
console.log(snow.color);

위의 코드는 편한 설명을 위한 코드다. class의 prototype을 저렇게 설정해주는게 좋은 방법은 아니다. 위 코드를 실행시키면 'white'가 출력될 것이다.

조금 더 응용해보자.

function withColor(color) {
  return function(target) {
    target.prototype.color = color;
  };
}

@withColor("blue")
class Paint {}

const sky = new Paint();
console.log(sky.color);

위와 같이 팩토리 함수처럼 인자를 받은 후 그 인자를 사용하는 함수를 반환할 수도 있다.

Class 멤버

Class 멤버 대해서도 적용할 수 있다. class에서 받는 target 인자와 더불어, 그 멤버의 이름인 name 그리고 멤버의 descriptor를 받는다. descriptor는 object의 property가 기본적으로 가지고 있는 속성으로 자세한 내용은 여기서 알아보자. 기본적으로 javascript에서 property는 string-valued name(key)와 property descriptor로 이루어진다.

먼저 property descriptor을 사용하는 예를 보자. 그 속성 중 writable속성을 건드리면 간단하게 클래스 멤버를 readonly로 만들 수 있다.

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Example {
  @readonly
  readonlyFunc() {
    console.log("This method is readonly!");
  }
}

const example = new Example();
example.readonlyFunc = () => console.log("change!");
example.readonlyFunc(); // This method is readonly!

writable은 속성은 그 속성의 value가 바뀔 수 있는지 없는지를 나타낸다. 이를 false로 바꿈으로서 바꿀 수 없도록 해준다. 따라서 이 decorator를 사용한 멤버는 바뀔 수 없다!

다음에는 클래스에서 본 것 처럼 클로져를 이용하여 보자.

function deprecate(message) {
  return (target, name, descriptor) => {
    const origin = descriptor.value; // property의 value
    descriptor.value = function(...args) {
      console.log(`Deprecate warning[${name}]: ${message}`);
      return origin.apply(this, args);
    };
    return descriptor;
  };
}

class Example {
  @deprecate('Consider to use "newFunc" instead')
  oldFunc() {
    console.log("This is old function.");
  }
  newFunc() {
    console.log("This is new function.");
  }
}

const example = new Example();
example.oldFunc();
// Deprecate warning[oldFunc]: Consider to use "newFunc" instead
// This is old function.

멤버 함수에 데코레이터를 사용하여 deprecate 메시지를 출력하는 예제이다. descriptor의 value인 oldFunc를 따로 저장해두고, console을 출력한 후 저장해준 원 함수를 호출한다. 이 때 주의할 점은, descriptor value를 인라인 함수로 정의하지 않아야 한다는 점이다. 이는 this가 원하는 참조를 하기 위해서 이다.

현재 ECMAScript stage-3에 있는 class의 instance field에도 사용 가능하다. 위에서 사용한 readonly를 이용하여

class Proposal {
  @readonly decorator = "stage-2";
  /*
    ...
  */
}

이런식으로 사용 가능하다.

번외- React에서 사용하기

React의 class 컴포넌트에서도 물론 decorator를 사용할 수 있다. 특히, Higher Order Component를 구현할 때, 꽤 편하게 사용할 수 있다. React Component도 당연히 클래스(stateful 일 때)이기 때문에 그 클래스를 인자로 받아와 원하는 동작을 해줄 수 있다.

function withName(name) {
  return Comp => () => <Comp name={name} />;
}

@withName("app")
class App extends React.Component {
  render() {
    return <div>{this.props.name}</div>;
  }
}

위 코드는 실용성도 없고 의미도 없지만, 이런식으로 inject props를 하거나, 감싸줄 수 있다. 조금 더 실용적인 예제로는 react-redux의 connect가 있다.

const mapStateToProps = state => ({
  todos: state.todos
});

@connect(mapStateToProps)
class App extends React.Component {
  render() {
    return (
      <ul>
        {todos.map((todo, i) => (
          <li key={i}>{todo}</li>
        ))}
      </ul>
    );
  }
}

connect를 위와 같이 사용할 수 있다. 보통 쓰는 것 처럼 connect(mapStateToProps)(App); 과 같이 할 수 있고 이런식으로도 사용 가능하다.

» List

Godsenal

안녕하세요. 개발 공부 중인 학생입니다.
개발하고 공부하며 배우는 내용들을 정리하고 있습니다.
의견, 조언 감사합니다.

» Mail» Github
« Graphql 2 - N+1 문제와 Dataloader
Graphql 1 - REST의 대안 GraphQL »