-
Notifications
You must be signed in to change notification settings - Fork 7
데코레이터를 아십니까?
- 자바스크립트의 함수는 일급객체이기 때문에 인자로 전달될 수 있고, 반환값으로 사용될 수도 있습니다!
- 유연하게 다룰 수 있음!
- 데코레이터를 이용해 기능을 추가 해 줄 수 있다!
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되도록 해야합니다!
- 함수에 전달된 인수들을 담고 있는 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();
}
더 자세한 내용은 타입스크립트 공식문서의 데코레이터 항목을 참고해주세요~