TypeScript 데코레이터 | Decorators 완벽 가이드
이 글의 핵심
TypeScript 데코레이터: Decorators 클래스 데코레이터·메서드 데코레이터.
들어가며
데코레이터란?
데코레이터(Decorator)는 클래스·메서드·프로퍼티에 추가 정보를 붙이거나, 선언 시점에 공통 로직을 감싸 넣는 특별한 문법입니다. 횡단 관심(로깅, 권한 등)을 본문과 분리할 때 사용됩니다.
JavaScript에서 TypeScript로의 전환
“런타임 에러는 프로덕션에서 터진다”는 말이 있습니다. JavaScript로 개발하던 시절, 이 말을 뼈저리게 체감했습니다. 테스트는 다 통과했는데 배포 후 사용자가 이상한 데이터를 입력하면 앱이 터지는 거습니다. 특히 팀 프로젝트에서 다른 사람이 작성한 함수의 반환 타입을 잘못 이해해서 버그가 발생하는 일이 잦았습니다. TypeScript를 도입한 후 가장 먼저 느낀 건 안심이었습니다. IDE가 자동완성을 정확히 해주으며, 타입이 맞지 않으면 빨간 줄로 미리 알려줍니다. 리팩토링할 때도 “이 함수를 바꾸면 어디가 깨질까?” 걱정할 필요가 없어졌습니다. 물론 처음엔 타입 정의 작성이 번거로웠지만, 몇 달 후 그 타입 정의 덕분에 버그를 미리 잡는 경험을 하고 나니 돌아갈 수 없었습니다.
1. 설정
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
2. 클래스 데코레이터
기본 사용
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
실전 예제: 로깅
function logger<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log(`${constructor.name} 인스턴스 생성됨`);
}
};
}
@logger
class User {
constructor(public name: string) {}
}
const user = new User("홍길동");
// 출력: User 인스턴스 생성됨
3. 메서드 데코레이터
기본 사용
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`${propertyKey} 호출됨:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} 결과:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(10, 20);
// 출력:
// add 호출됨: [10, 20]
// add 결과: 30
실전 예제: 성능 측정
function measure(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} 실행 시간: ${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class DataService {
@measure
async fetchData() {
await new Promise(resolve => setTimeout(resolve, 1000));
return { data: "결과" };
}
}
const service = new DataService();
await service.fetchData();
// 출력: fetchData 실행 시간: 1001.23ms
4. 프로퍼티 데코레이터
기본 사용
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class User {
@readonly
id: string = "U001";
name: string = "홍길동";
}
const user = new User();
console.log(user.id); // U001
// user.id = "U002"; // 에러 (strict 모드)
실전 예제: 검증
function validate(validationFn: (value: any) => boolean) {
return function(target: any, propertyKey: string) {
let value: any;
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: any) {
if (!validationFn(newValue)) {
throw new Error(`${propertyKey} 검증 실패`);
}
value = newValue;
}
});
};
}
class User {
@validate((value) => value.length >= 2)
name!: string;
@validate((value) => value >= 0 && value <= 150)
age!: number;
}
const user = new User();
user.name = "홍길동"; // ✅
user.age = 25; // ✅
// user.name = "a"; // ❌ 에러
// user.age = 200; // ❌ 에러
5. 매개변수 데코레이터
기본 사용
function required(
target: any,
propertyKey: string,
parameterIndex: number
) {
console.log(`${propertyKey}의 ${parameterIndex}번째 매개변수는 필수입니다`);
}
class User {
greet(@required name: string) {
console.log(`안녕하세요, ${name}님!`);
}
}
6. 데코레이터 팩토리
개념
매개변수를 받는 데코레이터를 만듭니다.
function log(prefix: string) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[${prefix}] ${propertyKey} 호출됨`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class UserService {
@log("USER")
createUser(name: string) {
console.log(`사용자 생성: ${name}`);
}
@log("AUTH")
login(email: string) {
console.log(`로그인: ${email}`);
}
}
const service = new UserService();
service.createUser("홍길동");
// 출력:
// [USER] createUser 호출됨
// 사용자 생성: 홍길동
service.login("hong@test.com");
// 출력:
// [AUTH] login 호출됨
// 로그인: hong@test.com
7. 실전 예제
예제 1: 권한 체크
function authorize(roles: string[]) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const userRole = getCurrentUserRole(); // 현재 사용자 역할
if (!roles.includes(userRole)) {
throw new Error("권한이 없습니다");
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
function getCurrentUserRole(): string {
return "admin"; // 실제로는 세션에서 가져옴
}
class AdminService {
@authorize([admin])
deleteUser(id: string) {
console.log(`사용자 삭제: ${id}`);
}
@authorize(["admin", "moderator"])
banUser(id: string) {
console.log(`사용자 차단: ${id}`);
}
}
const service = new AdminService();
service.deleteUser("U001"); // ✅ admin이므로 성공
예제 2: 캐싱
function cache(ttl: number = 60000) {
const cacheStore = new Map<string, { value: any; expiry: number }>();
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const key = `${propertyKey}_${JSON.stringify(args)}`;
const cached = cacheStore.get(key);
if (cached && Date.now() < cached.expiry) {
console.log("캐시에서 반환");
return cached.value;
}
console.log("새로 계산");
const result = await originalMethod.apply(this, args);
cacheStore.set(key, { value: result, expiry: Date.now() + ttl });
return result;
};
return descriptor;
};
}
class DataService {
@cache(5000) // 5초 캐싱
async fetchUser(id: string) {
await new Promise(resolve => setTimeout(resolve, 1000));
return { id, name: "홍길동" };
}
}
const service = new DataService();
await service.fetchUser("U001"); // 새로 계산
await service.fetchUser("U001"); // 캐시에서 반환
정리
핵심 요약
- 클래스 데코레이터: 클래스 수정
- 메서드 데코레이터: 메서드 동작 수정
- 프로퍼티 데코레이터: 프로퍼티 제어
- 매개변수 데코레이터: 매개변수 메타데이터
- 데코레이터 팩토리: 매개변수 받기
다음 단계
- TypeScript 고급 패턴
- TypeScript 실전 프로젝트
- NestJS 데코레이터
관련 글
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. TypeScript 데코레이터: Decorators 완벽 가이드. 클래스 데코레이터·메서드 데코레이터로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. TypeScript·Decorators·데코레이터 중심으… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- TypeScript 유틸리티 타입 | Partial, Pick, Omit, Record
- TypeScript 고급 패턴 | 조건부 타입, 템플릿 리터럴 타입
- TypeScript 실전 프로젝트 | REST API 서버 만들기
이 글에서 다루는 키워드 (관련 검색어)
TypeScript, Decorators, 데코레이터, 메타프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.