티스토리 뷰

Backend

Component 와 Transclude

지마켓 선현상 2021. 4. 12. 09:34

Web Component

이 글은 Web Component 에 대한 소개의 글은 아닙니다. 이를 위한 Custom Element 나 Shadow DOM 의 기술을 설명하는 글도 아닙니다. 이 글에서는 transclude 한 컴포넌트 디자인이 어떤 의미를 지닐 수 있는지를 논의해보고 싶습니다. 자바스크립트나 함수적인 프로그래밍에 익숙한 분들은 클로져나 람다 등에 익숙하실 것 같습니다. 제가 느끼기에 transclude 는 이런 기술들의 근본과 차이가 크지 않으며 이를 통한 활용이 좀 더 높은 추상화를 가능하게 도와주는 도구라고 생각합니다.

Web 에서 가장 많이 사용되는 element 는 <div> 컴포넌트 일 것입니다. 이 요소는 매우 단순한 요소인데 어떻게 많이 쓰이면서도 요소가 제공하는 요구사항을 더 추가하거나 수정하지 않고 쓰일 수 있을까요? 그것에는 두 가지 이유가 있다고 생각합니다. 가장 중요한 것은 작은 기능만을 제공한다는 점입니다. 너무 많은 기능이 아니라 매우 효과적으로 제공하는 적은 기능에 집중합니다. 그리고 다른 하나는 제공하는 기능 외에는 모두 자식 요소의 책임으로 미룹니다. 이 원칙은 좀 더 구체적이고 큰 컴포넌트를 만드는 경우에도 중요합니다. 엄밀히 말하면 될 수록 큰 컴포넌트를 만들지 않을 방법을 찾는 편이 더 좋을 것이라는 이야기가 될 것이기도 합니다.

함수에서 활용 가능한 패턴

생뚱맞지만 잠깐 함수 이야기를 해보려고 합니다. 아래와 같은 함수를 생각해보겠습니다. 간단히 카드를 그리는 함수입니다. 카드 이름과 번호를 받아 출력합니다.

function printCard(name, number) {
  console.log(name);
  printNumber(number);
}

function printNumber(number) {
  console.log(number);
}

printCard("Check Card", "0101-2020-2121-4423");

이를 조금 다른 방식으로 작성할 수도 있습니다. 카드 번호를 출력하는 기능을 contentPrinter 에 위임하는 점 외에는 두 함수 사이에 큰 차이는 없어 보입니다.

function printCard(name, contentPrinter) {
  console.log(name);
  contentPrinter();
}

function printNumber(number) {
  console.log(number);
}

printCard("Check Card", () => printNumber("0101-2020-2121-4423"));

이제 두 코드에 변경사항을 적용해보겠습니다. 카드 번호를 출력하는 곳에 유효기간을 추가로 출력해야 합니다. 첫 코드에서는 printNumber 함수에 expireDate 를 받아 출력합니다. printCard 또한 printNumber 에 전달하기 위해 expireDate 를 받도록 수정합니다.

function printCard(name, number, expireDate) {
  console.log(name);
  printNumber(number, expireDate);
}

function printNumber(number, expireDate) {
  console.log(number);
  console.log(expireDate);
}

printCard("Check Card", "0101-2020-2121-4423", "25/06");

이에 비해 두 번째 코드에서는 printNumber 에 대한 수정이면 충분합니다.

function printNumber(number, expireDate) {
  console.log(number);
  console.log(expireDate);
}

printCard("Check Card", () => printNumber("0101-2020-2121-4423", "25/06"));

이번에 또 두 코드에 변경사항을 적용해보겠습니다. 이번엔 바코드를 그려야 하는 포인트 카드 출력을 지원해야 합니다. 일단 printBarcode 함수를 만들고 printCard 에 기능을 확장합니다. 기존 기능과 구분이 필요하기에 isPointCard 를 추가로 필요할 것 같습니다. printBarcode 에 전달할 인수들도 printCard 에 추가합니다. 이를 실행하는 코드도 조금은 번잡하지만 이제 printCard 는 포인트 카드를 그리는 기능을 지원합니다.

function printCard(isPointCard, name, number, expireDate, barcode) {
  console.log(name);
  if (isPointCard) {
    printBarcode(barcode);
  } else {
    printNumber(number, expireDate);
  }
}

function printBarcode(barcode) {
  console.log(barcode);
}

printCard(false, "Check Card", "0101-2020-2121-4423", "25/06", null);
printCard(true, "Membership Card", null, null, "8888 2222 5555");

그럼 이번엔 두 번째 코드에 변경사항을 적용해보겠습니다. printBarcode 를 만들면 끝입니다. 다른 코드를 변경하지 않아도 됩니다. 필요한 분기는 printCard 내에서가 아니라 실행코드에서 이뤄집니다. contentPrinterprintNumber 로 구성할지 printBarcode 구성할지를 통해 이미 분기가 결정되기 때문입니다.

function printBarcode(barcode) {
  console.log(barcode);
}

printCard("Check Card", () => printNumber("0101-2020-2121-4423", "25/06"));
printCard("Membership Card", () => printBarcode("8888 2222 5555"));

두 스타일의 코드를 어떻게 보셨나요? 작은 차이점이지만 코드에 변경사항을 추가하는 과정이 이어지면서 꽤 많은 차이로 벌어졌습니다. 각자 선호하거나 익숙한 스타일이 있겠지만 변경사항을 좀 더 유연하게 대응하기엔 두 번째 스타일이 더 나아보입니다.

컴포넌트에서 활용 가능한 패턴

이제 컴포넌트로 돌아와 보겠습니다. 구현은 편의를 위해 조금 생략하고 필요한 골격을 위주로 표현하였습니다. 이번에도 두 스타일의 컴포넌트가 있습니다. 먼저 아래와 같이 간단한 컴포넌트를 작성합니다.

<!-- Card -->
<div>
  <span>{name}</span>
  <CardNumber number={number}/>
</div>

<!-- document -->
<Card name="Check Card" number="0101-2020-2121-4423"/>

그리고 이번엔 다음과 같이 작성합니다. children 을 사용한 것 외에는 큰 차이는 없습니다.

<!-- Card -->
<div>
  <span>{name}</span>
  {children}
</div>

<!-- document -->
<Card name="Check Card">
  <CardNumber number="0101-2020-2121-4423"/>
</Card>

이번에도 변경사항이 있습니다. expireDate 를 추가로 표시해야 합니다. 첫 컴포넌트는 다음처럼 수정합니다. <CardNumber> 뿐 아니라 <Card> 에도 수정이 필요합니다. expireDate 를 전달하기 위해서 입니다.

<!-- Card -->
<div>
  <span>{name}</span>
  <CardNumber number={number} expire-date={expireDate}/>
</div>

<!-- document -->
<Card name="Check Card" number="0101-2020-2121-4423" expire-date="25/06"/>

두 번째 컴포넌트는 어떨까요? <CardNumber> 에는 역시 수정이 필요하지만 <Card> 는 그대로 쓰면 됩니다.

<!-- document -->
<Card name="Check Card">
  <CardNumber number="0101-2020-2121-4423" expire-date="25/06"/>
</Card>

다음 변경사항은 포인트 카드를 지원하기입니다. 함수의 예시와 마찬가지로 <Card> 내부에 분기를 결정하기 위한 인수를 받아야 하고 지원해야 하는 모든 경우의 인수를 위한 설계도 필요합니다. 그것이 <Card> 에서 쓰이는 값이 아니더라도 자식 컴포넌트들에 전달하기 위해서 말이죠.

<!-- Card -->
<div>
  <span>{name}</span>
  { isPointCard
      ? <Barcode barcode={barcode}/>
      : <CardNumber number={number} expire-date={expireDate}/> }
</div>

<!-- document -->
<Card is-point-card={false} name="Check Card" number="0101-2020-2121-4423" expire-date="25/06"/>
<Card is-point-card={true} name="Membership Card" barcode="8888 2222 5555"/>

그러면 두 번째 컴포넌트도 살펴볼까요? 다음과 같습니다.

<!-- document -->
<Card name="Check Card">
  <CardNumber number="0101-2020-2121-4423" expire-date="25/06"/>
</Card>
<Card name="Membership Card">
  <Barcode barcode="8888 2222 5555"/>
</Card>

수정이 필요한 컴포넌트는 없습니다. <Card>name 외에는 다른 불필요한 정보를 알 필요가 없습니다. 포인트 카드를 그리면서 <Card> 컴포넌트를 훌륭하게 재사용하고 있습니다. 물론 기존의 카드를 그리는데에도 마찬가지입니다. 이 두 스타일은 어떤가요? 위에 예시한 함수와 뭔가 비슷하지 않나요? 서두에 transclude 는 함수적 표현들과 근본적인 부분이 유사하다는 의견을 적었습니다. 그리고 그것은 함수적인 표현이 추상을 다루는 패턴과 흡사합니다.
이 패턴은 유연하고 재사용성이 높은 컴포넌트를 설계하는데 주요합니다. 두 번째에서의 <Card> 컴포넌트는 작은 기능(name 을 출력하는) 만을 훌륭히 해내고 유연하며 충분히 재사용되고 있습니다. 그것은 자식 컴포넌트의 요구사항들을 전달하는 통로가 되는 대신 transclude 하게 자식 컴포넌트의 요구사항들을 직접 알아서 확인하도록 열어두었기 때문입니다. 마치 <div> 가 그런것처럼 말이죠.

정리

Web 을 개발하는 영역은 많은 변경과 추가기능에 대비 해야합니다. 여타의 다른 부분보다 더 디테일하고 잦은 변경을 마주하기 때문입니다. 또한 언뜻 비슷해 보이면서 세부적으로 조금씩 다른 표현들도 자주 등장합니다. 어느 순간은 이런 세부적인 차이들 사이에서 공통된 부분을 추상화하는 것이 어렵게 느껴질 때가 있습니다. 그래서 결국 세부적인 요구사항의 개수만큼 다양한 컴포넌트를 개발하곤 합니다. 이는 관리 역량을 더 필요하게 하는 결정이지만 흔히 하는 결정이기도 합니다. 이 때, 위에 소개한 transclude 를 잘 사용하는 패턴은 세부적인 요구사항들로부터 추상화가 가능한 부분들을 더 찾을 수 있게 도울 수 있습니다.

마치 <div> 가 그런것 처럼 말이죠.

긴 글 읽어주셔서 감사합니다.

댓글