글의 목적
- 원래는 클래스에 대해서 글을 작성해야 하는데 몸살기운이 있어서 집중이 안되기 때문에 조금은 간단한 실험이자 nestjs를 공부하면서 nestjs를 잘 다루기 위해서는 decorator를 잘 다뤄야 한다는 느낌을 받았기 때문에 간단한 실험을 기록하려고 한다.
- 초보자도 복붙하고 명령어를 따라하면 바로 실행해볼 수 있도록 모든 명령어와 코드를 제공할 예정.
본론
1. 폴더 생성 후 npm init, ts 초기 셋업
- vscode를 열어서 아무 폴더를 생성해줍니다.
- 저는 test라는 폴더를 만들었습니다. test 폴더로 들어가줍니다.
- npm 패키지관리자를 설치해줍니다. (node는 설치가 되어 있다는 가정입니다.)
- 타입스크립트도 설치해줍니다.
npm init -y
npm i typescript, ts-node
- deco.ts 파일을 만들어 줍니다.
- tsconfig.json 파일을 만들고 아래 내용을 넣어줍니다.
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}
- .ts파일을 테스트를 하기 위한 사전 준비를 모두 마쳤습니다.
2. decorator 생성
- 일단 간단하게 decorator가 어떻게 동작하는지 부터 알아보겠습니다.
// deco.ts
function deco(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("데커레이터가 평가됨.");
}
class TestClass {
@deco
test() {
console.log("함수가 호출됨");
}
}
const t = new TestClass();
t.test();
- 위에 코드를 붙여넣기 하시고 아래 명령어를 실행해줍니다.(node환경에서 .ts 파일이 실행됩니다.)
ts-node deco.ts
// 출력
데커레이터가 평가됨
함수가 호출됨
- 지금까지 클래스 안에서 사용되는 deco decorator를 만들었습니다
2.1 decorator에 인수를 넘겨서 decorator의 동작을 변경하기
- 간단하게 활용을 해보겠습니다.
- ts파일 확인 명령어는 아래 명령어로 통일입니다!
ts-node deco.ts
function deco(value: string) {
console.log("데커레이터가 평가됨.");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(value);
};
}
class TestClass {
@deco("Hi")
test() {
console.log("인수에게 먼저 인사한 함수가 호출됨");
}
}
const t = new TestClass();
t.test();
// 출력
데커레이터가 평가됨.
Hi
인수에게 먼저 인사한 함수가 호출됨
- decorator에 인수를 넣어서 decorator의 동작을 변경하는것을 실헙해봤습니다.
3. decorator 합성
- decorator의 합성은 간단합니다.
@a
@b
method()
- 이런 decorator의 연속 사용은 수학적으로 a(b())와 동일합니다.
- 이때 평가방식과 호출방식도 동일하게 진행됩니다. (평가는 위에서부터, 호출은 아래서부터)
- 따라해봅시다
function first() {
console.log("1. first decorator factory가 평가됨.");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("4. first decorator가 호출됨.");
};
}
function second() {
console.log("2. second decorator factory가 평가됨.");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("3. second decorator가 호출됨.");
};
}
class ExampleClass {
@first()
@second()
method() {
console.log("5. 모든 decorator를 통과한 후 메서드가 호출됨.");
}
}
const t = new ExampleClass();
t.method();
// 출력
1. first decorator factory가 평가됨.
2. second decorator factory가 평가됨.
3. second decorator가 호출됨.
4. first decorator가 호출됨.
5. 모든 decorator를 통과한 후 메서드가 호출됨.
4. 타입스크립트에서 지원하는 5가지 decorator
4.1 클래스 decorator
- 클래스 decorator는 이름 그대로 클래스 바로 앞에 선언됨.
- 클래스 decorator는 생성자에 적용되어 클래스 정의를 읽거나 수정할 수 있음.
- 단, 선언 파일과 선언 클래스 내에서는 사용할 수 없음.
- 클래스 decorator는 생성자를 리턴하는 함수여야 함.
function userClassDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
profileImageURL = "http://www.example.com";
};
}
@userClassDecorator
class User {
type = "User";
name: string;
constructor(t: string) {
this.name = t;
}
}
const user = new User("Decorator");
console.log(user);
// 출력
User {
type: 'User',
name: 'Decorator',
profileImageURL: 'http://www.example.com'
}
- User 클래스에 선언되지 않았던 새로운 속성이 추가되었습니다.
- 단 클래스의 타입이 변경되는것은 아님. 타입시스템은 profileImageURL을 인식하지 못하기 때문에 user.profileImageURL을 사용할 수 없음.
4.2 메서드 decorator
- 메서드 decorator는 메서드 바로 앞에 선언됨.
- 메서드의 속성 설명자(property descriptor)에 적용되고 메서드의 정의를 읽거나 수정할 수 있음.
- 선언파일, 오버로드 메서드, 선언 클래스에 사용 못함.
- decorator를 만드는 함수는 3가지 인자가 필수적으로 들어감
- targer : 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
- propertyKey: 멤버의 이름
- descriptor: 멤버의 속성 설명자. PropertyDescriptor 타입을 가짐
- 만약에 method decorator가 값을 반환한다면, 해당 method의 descriptor임.
- 함수를 실행할 때 에러가 발생하면 에러를 핸들링하는 예시로 확인해보겠습니다!
function ErrorHandler() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("target :", target);
console.log("propertyKey :", propertyKey);
console.log("descriptor :", descriptor);
const method = descriptor.value;
descriptor.value = function () {
try {
method();
} catch (error) {
console.log(error);
}
};
};
}
class User {
@ErrorHandler()
error() {
throw new Error("method decorator에서 에러를 핸들링하였습니다.");
}
}
const user = new User();
user.error();
// 출력
target : {}
propertyKey : error
descriptor : {
value: [Function: error],
writable: true,
enumerable: false,
configurable: true
}
Error: method decorator에서 에러를 핸들링하였습니다.
4.2.1 PropertyDescriptor
- 메서드 decorator가 가져야 하는 3개의 인수
interface PropertyDescriptor {
configurable?: boolean; // 속성의 정의를 수정할 수 있는지 여부
enumerable?: boolean; // 열거형인지 여부
value?: any; // 속성 값
writable?: boolean; // 수정 가능 여부
get?(): any; // getter
set?(v: any): void; // setter
}
4.3 접근자 decorator
- 접근자 decorator는 접근자(accessor)바로 앞에 선언
- 접근자의 정의를 읽거나 수정할 수 있음.
- 선언파일과 선언클래스에는 사용할 수 없음.
- 접근자 decorator가 반환하는 값이 있다면 해당 멤버의 속성 설명자가 됨.
- 특정 멤버가 열거가 가능한지 결정하는 예시로 확인해보겠습니다!
function Enumerable(enumerable: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = enumerable;
};
}
class User {
constructor(private name: string) {}
@Enumerable(true)
get getName() {
return this.name;
}
@Enumerable(false)
set setName(name: string) {
this.name = name;
}
}
const user = new User("test");
for (let key in user) {
console.log(`${key}: ${user[key]}`);
}
- getName은 출력이 되지만 setName은 출력이 안된것을 확인할 수 있음.
// 출력
name: test
getName: test
4.4 속성 decorator
- 속성 decorator는 클래스의 속성 바로 앞에 선언됨.
- 선언파일과 선언클래스에는 사용할 수 없음.
- 속성 decorator는 2개의 인자를 가짐
- target : 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
- propertyKey : 멤버의 이름
- 속성 설명자가 없고 반환값도 무시됨.
- 속성을 읽을때 getter를 호출하여 속성을 바꿔서 출력되는 decorator로 설명하겠습니다!
function format(formatString: string) {
return function (target: any, propertyKey: string): any {
let value = target[propertyKey];
function getter() {
return `${formatString} ${value}`;
}
function setter(newVal: string) {
value = newVal;
}
return {
get: getter,
set: setter,
enumerable: true,
configurable: true,
};
};
}
class Greeter {
@format("Hello")
greeting: string;
}
const t = new Greeter();
t.greeting = "World";
console.log(t.greeting);
// 출력
Hello World
4.5 매개변수 decorator
- 매개변수 decorator는 생성자나 메서드의 매개변수에 선언되어 사용됨.
- 선언파일과 선언클래스에는 사용할 수 없음.
- 매개변수 decorator는 3개의 인자를 가짐, 단 반환값은 무시됨.
- target : 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
- propertyKey : 멤버의 이름
- parameterIndex : 매개변수가 함수에서 몇 번째 위치에 선언되었는지를 나타내는 인덱스
- 매개변수가 제대로 전달되었는지 검사하는 decorator로 설명하겠습니다! (일반적으로 함수 decorator와 같이 사용됨)
- 해당 패키지를 설치해주시고 코드를 복사하셔서 ts-node deco.ts를 입력해주세요
npm i @nestjs/common
import { BadRequestException } from "@nestjs/common";
function MinLength(min: number) {
//파라미터의 최소값을 검사하는 파라미터 데코레이터
return function (target: any, propertyKey: string, parameterIndex: number) {
target.validators = {
// validators 속성에 유효성을 검사하는 함수 할당
minLength: function (args: string[]) {
return args[parameterIndex].length >= min;
},
};
};
}
//유효성 검사 : parameterIndex에 위치한 인자의 길이가 최소값보다 같거나 큰지 검사
function Validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value;
descriptor.value = function (...args) {
Object.keys(target.validators).forEach((key) => {
if (!target.validators[key](args)) {
throw new BadRequestException();
}
});
method.apply(this, args);
};
}
class User {
private password: string;
@Validate
setPassword(@MinLength(8) password: string) {
this.password = password;
}
}
const t = new User();
t.setPassword("password"); // 길이가 8이상이라 문제없음
console.log("이 선을 넘으면 통과!");
t.setPassword("pass"); //길이가 8보다 작기 때문에 BadRequestException 발생
// 출력
이 선은 넘으면 통과!
BadRequestException: Bad Request
4.6 요약
데코레이터[@] | 역할 | 호출시 전달되는 인자 | 선언 불가능한 위치 |
클래스 데코레이터 | 클래스의 정의를 읽거나 수정 | (constructor) | d.ts 파일, declare 클래스 |
메서드 데코레이터 | 메서드의 정의를 읽거나 수정 | (target, propertyKey, propertyDescriptor) | d.ts 파일, declare 클래스, 오버로드 메서드 |
접근자 데코레이터 | 접근자의 정의를 읽거나 수정 | (target, propertyKey, propertyDescriptor) | d.ts 파일, declare 클래스 |
속성 데코레이터 | 속성의 정의를 읽음 | (target, propertyKey) | d.ts 파일, declare 클래스 |
매개변수 데코레이터 | 매개변수의 정의를 읽음 | (target, proeprtyKey, parameterIndex) | d.ts 파일, declare 클래스 |
결론
- 몸이 안좋아서 클래스 정리를 미루고 가볍게 시작했는데 예기치 못한 에러를 만나서 3시간을 내리 날려버렸다....
2023.10.22 - [에러] - [23.10.22] .ts를 js로 컴파일하는것과 ts-node
[23.10.22] .ts를 js로 컴파일하는것과 ts-node
에러 나는 이 둘이 같은것인줄 알았다. 하지만 너무 다르다. 단순히 동작이 1가지에서 2가지로 바뀌는게 아니고 이 둘은 완전히 다르다. 본론 nestjs코드를 확인하기 위해서 간단하게 프로젝트를
paikpaik.tistory.com
- decorator는 그냥 class-validator를 많이 쓰지만 이제부터 커스텀해서 코드를 리팩토링도 해야겠다.
'데일리' 카테고리의 다른 글
[23.10.29] nodejs, DI, Ioc, nestjs (67) | 2023.10.29 |
---|---|
[23.10.25] Promise 함수 합성(Monad, Kleisli Composition) (54) | 2023.10.25 |
[23.10.20] 프로토타입 (27) | 2023.10.19 |
[23.10.19] 생성자 함수에 의한 객체 생성 (29) | 2023.10.19 |
[23.10.18] 원시 값과 객체 (24) | 2023.10.19 |