자바스크립트 데코레이터 살펴보기

sejin kim·2022년 6월 4일
0
post-thumbnail

Decorator?

Decorator란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 디자인 패턴을 말합니다(위키백과). 코드를 수정하지 않고도 수평적으로 기능을 확장하거나 동작을 추가할 수 있고, 중복 코드도 줄일 수 있는 유용한 방법입니다. 프레임워크 등을 통해 유사한 기능을 사용/구현해본 적이 있다면 익숙하게 와닿는 개념일 수 있습니다.

자바스크립트에서도 사용할 수는 있지만 글 작성 시점 기준 현재 아직 표준은 아니며, 표준 제정을 위한 ES proposal stage 3 까지 올라온 상황입니다. 만약 본격적으로 사용하고자 한다면 babel과 같이 트랜스파일러의 도움이 필요할 수 있습니다.

반면 자바스크립트 대비 더 과감하고 적극적인 신규 문법 도입이 가능한 타입스크립트 진영에서는 이미 적극적으로 도입하여 사용하고 있었기도 한데, 대표적인 예로 Nest.jsTypeORM 같은 백엔드 영역을 들 수 있겠습니다.

아래와 같이 함수 호출시 다른 함수를 래핑하는 방법으로 Decorator를 구현할 수 있기도 합니다. 어디까지나 '패턴'에 관한 토픽이기 때문입니다. 기본적으로 새로운 함수를 반환하여 전달된 함수의 동작을 수정하는 방식이라고 볼 수 있습니다.


function doSomething(message) {
    console.log(`${message}`);
}

function loggingDecorator(wrapper) {
    return function() {
        console.log('logging start');
        const result = wrapper.apply(this, arguments);
        console.log('logging finished');
        return result;
    }
}

const wrapper = loggingDecorator(doSomething);

wrapper('Hello!');
// 'logging start'
// 'Hello!'
// 'logging finished'





How to use

Python과 유사하게, @expression 과 같은 형태로 적용 대상의 바로 위에 추가하여 사용합니다. 현재 제안/설계된 스펙에 따르면, 아래와 같은 대상에만 적용할 수 있으며 추후 후속 작업을 통해 확장될 가능성이 있습니다.


  • 클래스
  • 클래스 필드(프로퍼티)
  • 클래스 메소드
  • 클래스 접근자

클래스의 필드를 읽기 전용으로 만드는 예제 코드를 인용해 보겠습니다.


function readonly(target, key, descriptor) {
    return {
        ...descriptor,
        writable: false,
    };
}

class Data {
    @readonly
    password = 'pwd';
}

const data = new Data();
data.password = 'newpwd'; // Error: Cannot assign to read only property 'password' of object '#<Data>'

데코레이터 함수는 인자로 target, key, descriptor를 전달받고 있습니다. target은 데코레이팅 대상, key는 대상의 속성 이름, descriptor는 데코레이팅되는 속성을 포함한 대상의 모든 속성들입니다. 여기서 주목해 보아야 할 부분은 설명자 객체, descriptor입니다.


const person = {
    name: 'sj',
    level: 99
};

console.log(Object.getOwnPropertyDescriptor(person, 'level'));

/*
{
    configurable: true
    enumerable: true
    value: 99
    writable: true
}
*/

자바스크립트 객체의 프로퍼티에는 속성 설명자, propertyDescriptor 라는 정보가 존재합니다. 위 예제에서는 이것을 변경한 것으로, 각 속성이 의미하는 바는 아래와 같습니다. MDN 문서 링크


  • configurable: 객체에서 해당 속성을 제거할 수 있는지, 속성의 특성/유형을 변경할 수 있는지 여부 (기본 값은 false)
  • enumerable: 객체를 열거할 때 key를 노출할 것인지, 전개 연산자가 key를 볼 수 있는지 여부 (기본 값은 false)
  • value: 속성의 값 (기본 값은 undefined)
  • writable: 할당 연산자로 값을 변경할 수 있는지 여부
  • get: 접근자로 사용할 함수, 없으면 undefined
  • set: 설정자로 사용할 함수, 없으면 undefined

계속해서 클래스 메소드에 데코레이터를 적용하는 예시를 인용해 보겠습니다. 위에서 함수를 래핑하여 명령형으로 데코레이터를 구현한 방법과 유사한 형태입니다.


function logging(target, name, descriptor) {
    const originalValue = descriptor.value;
    const wrapper = function(...arguments) {
        try {
            return originalValue.apply(this, arguments);
        } catch (error) {
            console.error(`Error: ${error}`);
            throw error;
        }
    };
    return { ...descriptor, wrapper };
}

class Data {
    @logging
    getData = () => {
        throw new Error('no data');
    };
}

const data = new Data();
data.getData(); // 'Error: no data'





정리

이미 데코레이터를 프로덕션 코드에서도 실제로 사용하고 있는 케이스가 존재할 것이지만, 아직 현재 시점에서도 제안 단계인지라 추후 명세는 변경될 수 있는 상황입니다. 다만 그렇더라도 구체적인 구현 방법, 코드가 변화할 뿐 기본적인 의도나 패턴 자체가 변화되는 것은 아니니 얼마든지 적용해 볼 수 있는 부분입니다.

간단하고 편리하게 반복되는 공통 코드를 다른 코드에 횡단하여 적용할 수 있다는 점은 매력적입니다. 위의 예제에서처럼 로깅, 에러 핸들링, 유효성 검사, DB 커넥션 등 상황에 따라 여러 형태로 유용하게 사용할 수 있을 것입니다.

profile
퇴고를 좋아하는 주니어 웹 개발자입니다.

0개의 댓글