Skip to content

데코레이터를 아십니까?

Chanhee Kim edited this page Dec 5, 2021 · 3 revisions

데코레이터 패턴

  • 자바스크립트의 함수는 일급객체이기 때문에 인자로 전달될 수 있고, 반환값으로 사용될 수도 있습니다!
    • 유연하게 다룰 수 있음!
    • 데코레이터를 이용해 기능을 추가 해 줄 수 있다!
function slow(x) {
  // CPU 집약적인 작업이 여기에 올 수 있습니다.
  alert(`slow(${x})을/를 호출함`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // cache에 해당 키가 있으면
      return cache.get(x); // 대응하는 값을 cache에서 읽어옵니다.
    }

    let result = func(x);  // 그렇지 않은 경우엔 func를 호출하고,

    cache.set(x, result);  // 그 결과를 캐싱(저장)합니다.
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1)이 저장되었습니다.
alert( "다시 호출: " + slow(1) ); // 동일한 결과

alert( slow(2) ); // slow(2)가 저장되었습니다.
alert( "다시 호출: " + slow(2) ); // 윗줄과 동일한 결과

// 출처 : https://ko.javascript.info/call-apply-decorators
  • 위 코드에서 cachingDecorator는 인자로 주어진 함수에 캐시 기능을 추가해줍니다!
  • 인자로 주어진 함수를 데코레이터 함수가 래핑하고 클로저로 맵 객체인 cache를 넘겨줍니다.
    • cache에 해당 키가 없을 때만 함수를 실행하고 있을 때는 캐시된 값을 반환!
  • 함수에 유연하게 캐시 기능을 추가할 수 있습니다!
    • 캐시 기능이 필요한 함수마다 캐시 기능을 위한 코드를 작성할 필요가 없어집니다.
    • 캐싱 기능의 코드를 분리하여 간결하게 함수의 핵심 로직에 대한 코드만 볼 수 있게됨 ⇒ 가독성 향상

메서드 데코레이터

  • 아래와 같이 this를 사용하는 객체 메서드의 경우 위의 예제와 같이 쓰면 this가 원래 메서드를 갖고 있던 객체가 아니게 되어 에러가 발생
// worker.slow에 캐싱 기능을 추가해봅시다.
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // CPU 집약적인 작업이라 가정
    alert(`slow(${x})을/를 호출함`);
    return x * this.someMethod(); // (*)
  }
};

// 이전과 동일한 코드
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // 기존 메서드는 잘 동작합니다.

worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용

alert( worker.slow(2) ); // 에러 발생!, Error: Cannot read property 'someMethod' of undefined
  • func(x) 부분을 func.call(this, x) 또는 func.apply(this, x)로 바꾸어 this가 worker 객체로 bind되도록 해야합니다!

arguments 객체

  • 함수에 전달된 인수들을 담고 있는 Array형태의 객체
function func1(a, b, c) {
  console.log(arguments[0]);
  // expected output: 1

  console.log(arguments[1]);
  // expected output: 2

  console.log(arguments[2]);
  // expected output: 3
}

func1(1, 2, 3);

function func2() {
  console.log(arguments[0]);
  // expected output: 1

  console.log(arguments[1]);
  // expected output: 2

  console.log(arguments[2]);
  // expected output: 3
}

func2(1, 2, 3);
  • 모든 함수 내에서 이용가능한 지역 변수.
  • 함수 선언에 매개변수 선언해주지 않아도 arguments 객체에는 전달됩니다!
    • 이를 이용해 매개변수 갯수가 유동적인 함수들도 데코레이팅 할 수 있습니다!
let worker = {
  slow(min, max) {
    alert(`slow(${min},${max})을/를 호출함`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = decoB(decoA(cachingDecorator(worker.slow, hash)));

alert( worker.slow(3, 5) ); // 제대로 동작합니다.
alert( "다시 호출: " + worker.slow(3, 5) ); // 동일한 결과 출력(캐시된 결과)

데코레이터 문법

데코레이터 패턴을 더 쉽고 가독성 좋게 적용하기 위해 ES7에서 제안된 문법입니다.

// 데코레이터 문법 미사용
foo = f(g(originalFoo));

// 데코레이터 문법 사용
@f
@g
foo(){
	...
}

아직 최종적으로 채택된 문법이 아니기 때문에 사용하기 위해서는 Babel이 필요합니다.@babel/plugin-proposal-decorators 플러그인을 설치하고 설정에 추가해주세요.

legacy(stage 1) 문법을 사용하기 위해서는 legacy옵션을 true로 설정해야합니다.

  • typescript, nest.js 등 대부분의 프레임워크들에서 stage 1 문법을 사용하고 있고, 참고할만한 자료들도 대부분 stage 1 문법에 기반하는 경우가 많아 legacy 옵션을 true로 사용하는 것을 추천합니다.
"plugins": [["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]]
"plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]

vscode 사용시 에러메시지가 나타날 수 있는데, JavaScript > Implicit Project Config: Experimental Decorators 옵션을 체크하면 메시지가 더이상 나타나지 않습니다.

데코레이터 문법을 사용하기 위한 설정을 알아보았으니 이제 데코레이터 문법을 살펴보겠습니다.

데코레이터는 함수이며 @decorator 와 같이 데코레이터 함수 이름 앞에 at 기호를 붙여 사용합니다.클래스에 프로퍼티를 추가/오버라이드 하거나, 메서드에 동작을 추가해주거나, 프로퍼티의 값을 변경하는 다양한 데코레이터를 만들고 사용할 수 있습니다.

아래 예시처럼 메서드를 꾸며주는 메서드 데코레이터를 작성하고 사용할 수 있습니다.

// Stage1 Syntax
// 매개변수를 이용해 값에 따라 다르게 꾸며주고 싶다면 데코레이터 팩토리 형태로 만들어 사용하면 됩니다
// 메서드에 render 동작을 추가하는 메서드 데코레이터
function reRender(logging) {
	return function (target, propertyKey, description) {
	  const fn = description.value;

	  description.value = function (...args) {
            if(logging) console.log('rerender!');
	    fn.apply(this, args);
	    this.render();
	  };
	}
}

// 매개변수에 따른 동작이 필요 없을 때는 팩토리 형태로 작성하지 않아도 됩니다
function reRender(target, propertyKey, description) {
  const fn = description.value;

  description.value = function (...args) {
    fn.apply(this, args);
    this.render();
  };
}

// Stage2 Syntax
function reRender({ descriptor, kind }) {
  if (kind === 'method') {
    const fn = descriptor.value;
    descriptor.value = function (...args) {
      fn.apply(this, args);
      this.render();
    };
  }
}
//팩토리 형태로 작성했을 때
@reRender(true)
onResetPeriod() {
  this.searchConditionModel.resetPeriod();
}
//팩토리 형태가 아닐때
@reRender
onResetPeriod() {
  this.searchConditionModel.resetPeriod();
}

타입스크립트에서의 데코레이터

타입스크립트에서도 자바스크립트에서와 마찬가지로 데코레이터 문법을 사용할 수 있습니다.

커맨드라인에 아래와 같은 명령어를 입력하거나,

tsc --target ES5 --experimentalDecorators

tsconfig.json의 컴파일러 옵션에 "experimentalDecorators": true 를 추가해주세요.

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

작성 방법은 자바스크립트와 같습니다.

아래의 코드는 저희 백엔드에서 미들웨어 메서드를 try/catch 문으로 감싸주는 CatchError 데코레이터입니다.

function CatchError(target, propertyKey: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;
  descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
    try {
      await method(req, res, next);
    } catch (error) {
      next(error);
    }
  };
}

이 데코레이터를 이용해 비동기 작업을 하는 미들웨어에서 try/catch 구문을 반복적으로 작성하지 않게 되었고,

try/catch 구문이 미들웨어 메서드 코드에서 사라짐으로써 핵심 로직에 대한 코드들만 남게되어 가독성도 더 높아졌습니다.

데코레이터 적용 전

export const updateUserValidator = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { userID } = req.session;
    const userdata = req.body;
    const newUserData = new UpdateUserData(userdata);

    const errors = await validate(newUserData, VALIDATE_OPTIONS);
    if (errors.length) return res.status(400).json(errors);

    const user = await userRepository.findOne(userID);
    if (!user) return res.status(400).send(updateUserDataMSG.userNotFound);

    next();
  } catch (e) {
    next(e);
  }
};

데코레이터 적용 후

  @CatchError
  async updateUserValidator(req: Request, res: Response, next: NextFunction) {
    const { userID } = req.session;
    const userdata = req.body;

    const newUserData = new UpdateUserData(userdata);
    const errors = await validate(newUserData, VALIDATE_OPTIONS);
    if (errors.length) throw new CustomError({ message: errors, status: 400 });
    const user = await userRepository.findOne(userID);
    if (!user) throw new CustomError({ message: updateUserDataMSG.userNotFound, status: 400 });

    next();
  }

더 자세한 내용은 타입스크립트 공식문서의 데코레이터 항목을 참고해주세요~

타입스크립트 공식문서 - 데코레이터

Clone this wiki locally