Slot을 이용한 Presentational Component 모듈화

Presentation 컴포넌트 제작과정에서 다음과 같은 상황을 흔히 만나게 됩니다.

  • 컴포넌트를 논리적으로 작게 쪼개서 만든다.

  • 이를 조합해서 좀더 큰 컴포넌트를 만들고 나면 그 컴포넌트가 너무 커진다.

이런 문제를 slot을 이용해 컴포넌트를 적절한 크기로 분리하는 방법에 대해 다뤄봅니다.

Presentational Component

어플리케이션의 UI를 구성하는 프레젠테이션 컴포넌트는 보통 더 작게 단일 기능만 수행하도록 만들어진 컴포넌트들로 구성된 중첩된(Nested) 구조를 가집니다.

예를 들어 다음 구조의 Left Menu 프레젠테이션 컴포넌트가 있다고 해봅시다.

diag 3071cb4cd8bd4339b917b98f504a69ec
Figure 1. Example

이런 구조의 복잡한 프레젠테이션 컴포넌트를 단일 컴포넌트로 만드는것은 컴포넌트 기반 개발의 아이디어에서 크게 어긋나는 것 일 겁니다.

당연히 각 구성을 작게 쪼개서 컴포넌트를 만들겠죠.

그리고 만들고 나면 이것을 합쳐야 하는데..

LeftMenu.vue
<template>
  <ul class="leftMenu"  :class="{ [$style.leftMenu]: isOpen }">
    <li is="menu-header" v-on:closeClicked="closedClicked"/>
    <li is="main-logo" :mainLogoType="mainLogo" v-on:click.stop="mainLogoClicked"/>
    <li is="sub-brand-logo" v-if="isSubBrand" :selectedBrand="selectedBrand"/>
    <li is="upper-brand-link" v-if="hasUpperBrand" :upperBrand="upperBrand"/>
    <ul class="category">
      <div class="scroller">
        <div class="scrollWrapper">
          <li is="category-list" v-for="cateogry in topCategorie" :category="category"/>
          <li is="brand-list" v-for="cateogry in topCategorie" :category="category"/>
        </div>
      </div>
    </ul>
    <ul class="brandNew">
      <li is="brand-new-list" v-for="goods in newGoodsList" :goods="goods"/>
    </ul>
  </ul>
</template>

음 이게 정말 프레젠테이션 컴포넌트일까요? 저는 아니라고 봅니다.

그냥 divul등 태그 안에 컨텐츠의 위치를 잡아주기만 해야 하는 프레젠테이션 컴포넌트가 각종 이벤트를 받아서 처리 하고, 데이터도 넘겨주어야 하는등 해야 할 일이 너무 많습니다.

물론 UI만을 위한 이벤트는 프레젠테이션 컴포넌트에서 하는게 맞습니다.

사실 저런 일들은 컨테이너 컴포넌트에서 해야 하는 일 들이거든요. 지금은 프레젠테이션 컴포넌트가 불필요한 책임을 가지고 있는 것입니다.

With Slot

위 코드를 slot을 이용하여 프레젠테이션에 관한 책임만 가지게 변경해 봅시다.

LeftMenu.vue
<template>
  <ul class="leftMenu"  :class="{ [$style.leftMenu]: isOpen }">
    <slot name="menu-header"></slot>
    <slot name="main-logo"></slot>
    <slot name="sub-brand-logo"></slot>
    <slot name="upper-brand-link"></slot>
    <ul class="category">
      <div class="scroller">
        <div class="scrollWrapper">
          <slot name="category-list></slot>
          <slot name="brand-list></slot>
        </div>
      </div>
    </ul>
    <ul class="brandNew">
      <slot name="brand-new-list"></slot>
    </ul>
  </ul>
</template>

이렇게 레이아웃과 표현에 관련된 내용만 남기게 됩니다.

이제 이 프레렌테이션에 대응하는 컨테이너 컴포넌트는 다음처럼 구성 될 겁니다.

LeftMenuContainer.vue
<template>
  <left-menu>
    <menu-header
      slot="menu-header"
      v-on:closeClicked="closedClicked"/>

    <main-logo
      slot="main-logo"
      :mainLogoType="mainLogo"
      v-on:click.stop="mainLogoClicked"/>

    <sub-brand-logo
      slot="sub-brand-logo"
      v-if="isSubBrand"
      :selectedBrand="selectedBrand"/>

    <upper-brand-link-container
      slot="upper-brand-link"
      v-if="hasUpperBrand"
      :upperBrand="upperBrand"/>

    <category-list
      slot="category-list"
      v-for="cateogry in topCategorie"
      :category="category"/>

    <brand-list
      slot="brand-list"
      v-for="cateogry in topCategorie"
      :category="category"/>

    <brand-new-list
      slot="brand-new-list"
      v-for="goods in newGoodsList"
      :goods="goods"/>
  </left-menu>
</template>

이렇게 컨테이너 컴포넌트의 경우 여러 프레젠테이션에 대해 일괄적인 통제가 가능해야 하는 경우가 많기 때문에 slot을 이용해 각 개별 프레젠테이션 컴포넌트에 대한 설정을 분리해 낼수 있습니다. 각 개별 프레젠테이션 컴포넌트에 데이터를 넘기고 이벤트 핸들러를 연결하는 등 일반적인 컨테이너 역활을 수행 할 수 있게 됩니다. 거대한 데이터 꾸러미를 한번에 넘기는게 아니고 말이죠.

또한 각 슬롯에 넘겨주는게 단순 프레젠테이션 컴포넌트일수도 있고, 컨테이너까지 딸린 컴포넌트일수도 있습니다. 선택할수 있게 되는것이죠.

이 방식이 좋은 방식인지는 아직 모르겠습니다만, 컨테이너와 프레젠테이션을 분리하기 위한 방법을 고민하다 떠오른 방법 이였습니다.

참고가 되셨기를.

Tags: javascript  vue  component  vuejs