Skip to main content

Command Palette

Search for a command to run...

[Series. 3] 데코레이터 패턴과 프론트엔드 개발

웁니다

Updated
5 min read
[Series. 3] 데코레이터 패턴과 프론트엔드 개발


어제보다 더 나은 서비스를 만들어내는 사람이 되고자 노력하며, 내일의 나를 위해 기록합니다.

Nest.js의 문법을 가볍게 학습하면서 문득 데코레이터 패턴에 대해서 궁금해졌다. @ 를 통해 마법처럼 특정 기능을 동작케 하는 이 패턴은 대체 어떻게 작성이 되는 걸까? 그리고 정체가 뭘까?

그런 이유로 리액트 컴포넌트 패턴 시리즈의 3번째는 데코레이터 패턴 탐구로 서두를 연다.

데코레이터 패턴이란?

데코레이터 패턴은 “코드재사용을 목표로 하는 구조 패턴”이라고 한다. 그렇다면 여기에서 언급된 구조 패턴은 무엇일까?

💡
구조 패턴(Structural Pattern) 이란 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴을 총칭한다

위 정의에 따르면 데코레이터 패턴은 조합을 통해 기능을 확장하여 코드 재사용을 가능케하는 패턴으로 정리할 수 있다.
즉, 데코레이터 패턴은 독립적인 기능을 캡슐화하여 기존 코드에 데코레이터를 추가하는 것만으로 기능을 확장시키는 역할을 한다고 볼 수 있다.

이와 더불어 데코레이터 패턴은 객체지향의 SOLID 원칙 전반을 준수하는 패턴이기도 한데,

  1. 각 데코레이터는 하나의 기능을 가지고, (단일책임원칙)

  2. implements 등을 통해 기본 인터페이스는 수정하지 않고 확장을 하며, (개방폐쇄원칙)

  3. 부모를 자식으로 교체해도 정확성을 보장해야하며, (리스코프치환원칙)

  4. 인터페이스를 분리하여 필요한 기능만 구현해야하고, (인터페이스분리원칙)

  5. 구체적인 구현이 아닌 추상화에 의존한다.(의존성역전원칙)

와 같이 SOLID 원칙을 준수해 작성된다.

데코레이터 패턴 작성 방법

일반적인 클래스 문법을 통한 데코레이터 패턴

데코레이터 패턴에서는 기본 객체를 담당하는 클래스와 추가 기능을 위한 데코레이터 클래스로 구조가 나뉘어 진다.

// Hamburger 기본 객체 클래스
class Hamburger {
    price(){
        return 5500;
    }

    description(){
        return "기본 햄버거"
    }
}

// 데코레이터의 기본 클래스
class HamburgerDecorator {
    constructor(hamburger){
        this.hamburger = hamburger
    }

    price(){
        return this.hamburger.price();
    }

    description(){
        return this.hamburger.description();
    }
}

이렇게 구조화된 기본 객체와 기본 데코레이터 객체를 통해 구체적인 데코레이터들을 생성해나가면 되는데,

// 치즈 추가 데코레이터
class CheeseDecorator extends HamburgerDecorator {
    price(){
        return this.hamburger.price() + 1000
    }

    description(){
        return this.hamburger.description() + " + 치즈 추가"
    }
}

// 패티 추가 데코레이터
class PattyDecorator extends HamburgerDecorator {
    price(){
        return this.hamburger.price() + 2000
    }

    description(){
        return this.hamburger.description() + " + 패티 추가"
    }
}

위의 데코레이터를 활용하게 되면 아래와 같다.

let hamburger = new Hamburger();
console.log(hamburger.price()); // 5500
console.log(hamburger.description()); // 기본 햄버거

hamburger = new CheeseDecorator(hamburger);
console.log(hamburger.price()); // 6500
console.log(hamburger.description()); // 기본 햄버거 + 치즈 추가

hamburger = new PattyDecorator(hamburger);
console.log(hamburger.price()); // 8500
console.log(hamburger.description()); // 기본 햄버거 + 치즈 추가 + 패티 추가

@ 문법을 활용한 데코레이터 패턴

위에 작성한 문법을 보았을 때는 Nest.js에서 확인가능한 @ 데코레이터가 아닌 것을 확인할 수 있다. 하지만 이 데코레이터 문법은 JS의 24년도 1월에 들어온 최신의 문법으로 크롬, 파이어폭스, 사파리 브라우저는 물론 Node.js 에서도 2023.12~2024.03월 사이 업데이트 버전에서 지원하기 시작했으므로 사용에 주의가 필요하다. 물론 Babel 등을 통해 활용 가능하나 기본지원이 아니라는 점에서 주의가 필요하다.

특징

우선 @ 데코레이터 문법은 함수를 정의해서 활용하게 된다. 기존에 클래스로 정의한 데코레이터는 모두 함수로 변경해야한다는 말이기도 하다. 그렇기에 사용성이 편리하다고 해서 기존 클래스로 작성된 데코레이터 패턴을 모두 @ 문법으로 옮길 필요는 없다.

또한 클래스 기반과의 큰 차이로는 구현 방식의 차이가 있는데, 클래스 기반에서는 런타임에 유연하게 기능이 추가/제거가 되지만 @문법에서는 클래스 정의 시점에 기능이 결정되어 컴파일 타임에 적용된다.

사용 방식에는 중간 데코레이터 없이 데코레이터 함수로 직접 확장이 되는 형태이기 때문에 기본 객체 클래스를 만들어 두는 것 외에는 클래스 기반 데코레이터와 문법적으로 차이가 있다.

// Hamburger 기본 객체 클래스
class Hamburger {
    price(){
        return 5500;
    }

    description(){
        return "기본 햄버거"
    }
}
// 치즈 데코레이터 함수
// value는 데코레이터가 적용될 클래스(기본 객체 클래스)이며 context는 데코레이터의 컨텍스트 정보이다.
function withCheese(value, context) {
    if (context.kind === "class") {
        // 새로운 클래스를 반환하여 기존 클래스를 확장
        return class extends value {
            price() {
                // super를 통해 기존 가격에 치즈 가격을 추가
                return super.price() + 1000;
            }

            description() {
                // super를 통해 기존 설명에 치즈 설명을 추가
                return super.description() + " + 치즈 추가";
            }
        };
    }
}

// 패티 데코레이터 함수
function withPatty(value, context) {
    if (context.kind === "class") {
        return class extends value {
            price() {
                return super.price() + 2000;
            }

            description() {
                return super.description() + " + 패티 추가";
            }
        };
    }
}

위와 같이 작성된 데코레이터 함수는 사용 시 @ 문법을 통해 활용되게 된다.

// 치즈 버거
@withCheese
class CheeseHamburger extends Hamburger {}

// 더블패티 치즈버거
@withPatty
@withCheese
class DoublePattyCheeseHamburger extends Hamburger {}
const basic = new Hamburger();
console.log(basic.price());        // 5500
console.log(basic.description());  // "기본 햄버거"

const cheese = new CheeseHamburger();
console.log(cheese.price());       // 6500
console.log(cheese.description()); // "기본 햄버거 + 치즈 추가"

const double = new DoublePattyCheeseHamburger();
console.log(double.price());       // 8500
console.log(double.description()); // "기본 햄버거 + 치즈 추가 + 패티 추가"

데코레이터를 프론트엔드 개발에 활용할 수는 없을까?

문득 이런 데코레이터 패턴의 편의를 보고나니 리액트 컴포넌트를 개발할 때 활용할 수 없을까? 라는 생각이 들었다. 공통적으로 활용되어야하는 에러로깅이나 스타일 등을 데코레이터 함수로 빼서 활용하면 편하지 않을까?

결과적으론 가능하다. 하지만 실제 사용할 일은 거의 없다고 본다. (Angular에서나TS 문법으로는 데코레이터 패턴이 적극적으로 활용된다.)
이유는 데코레이터 명세에 있는 적용 대상에 있다.

데코레이터는

  1. 클래스 선언

  2. 클래스 메서드

  3. 클래스 필드

  4. 클래스 접근자(getter/setter)

  5. 클래스 private 요소

에 대해서 적용가능하기 때문인데, 현재 리액트를 개발할 때 대부분 함수형 컴포넌트를 활용하고 있기 때문에 현실적으로는 데코레이터 패턴 자체를 프론트엔드 개발에 활용하기엔 무리가 있다.

하지만 현재 리액트 시대에는 데코레이터의 이점을 가진 대안이 있다.

프론트엔드 개발에서의 데코레이터 대안

HoC 패턴이 주요한 대안인데 데코레이터 패턴의 기능 재사용, 코드 확장, 관심사 분리 등의 목적을 수행할 수 있다.

HoC (High Order Components)

정의

💡
컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수로 기존 컴포넌트에 추가 기능이나 속성을 부여한다

규칙

  1. 순수함수로 작성한다.

  2. 원본 컴포넌트를 수정하지 않는다.

  3. 모든 props를 전달한다.

구현

// 기본 컴포넌트
function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

// HOC
function withLogging(WrappedComponent) {
  return function WithLoggingComponent(props) {
    const handleClick = () => {
      console.log('Button clicked');
      props.onClick();
    }
    return <WrappedComponent {...props} onClick={handleClick} />;
  }
}

// 사용
const LoggedButton = withLogging(Button);

사실 상 데코레이터의 리액트 버전이라고 볼 수 있는 HoC 패턴의 경우 기본 컴포넌트를 감싸 새로운 기능을 추가하며, 둘 다 원본을 수정하지 않고 기능을 확장한다. 그리고 여러 개의 래퍼(데코레이터)를 활용해 조합이 가능하다.

기본 객체 클래스와 데코레이터 클래스로 나뉘는 데코레이터 패턴과 유사성이 짙으며 역할에서도 유사성이 짙다. 다만 리액트 개발을 할 경우 HoC는 컴포넌트 자체를 감싸 새 기능을 추가하는 역할로 활용되기 떄문에 렌더링 로직에 한정되어 사용되며 단순한 로직을 재사용할 경우에는 Hooks를 활용하게 된다.

마치며

유명한 디자인 패턴들이 많이 있다. 이름은 대부분 들어봤지만 프론트엔드 개발 시에 어떻게 적용할 수 있는 지 파악하기 어렵고 찾아보지 않으면 빈번하게 사용하는 것들과 이러한 패턴들의 유사성 또한 알아보기 어렵다. 그런 점에서 프론트엔드 개발에서 사용되는 패턴들의 출처를 찾아 근원으로 돌아가 학습해보는 것은 유의미 한 것 같다. 아무튼 다음에는 어떤 걸 찾아볼 지 고민이 된다.

Frontend Tech

Part 1 of 3

회고와 기술을 분리해 작성하고자 합니다. 리액트를 중심으로 Front-End 관련 기술 아티클을 작성합니다.

Up next

[Series. 2] Headless Component 패턴

싹뚝...