본문 바로가기

FrontEnd

Vue3 컴포넌트 디자인 패턴 : Renderess Component / Compound Component

반응형

Vue3의 Renderess Component / Compound Component 디자인 패턴에 대해 알아봅니다.

Vue3 디자인 패턴 시리즈를 정리하는 내용이 될 것 같습니다.

TL;DR

  • renderess component는 마크업 없는 컴포넌트다
  • 상태와 액션만을 추상화하는 컴포넌트다.

좋은 컴포넌트란 무엇인가?

  • 결합도가 낮고 응집도가 높고 확장성 있는 컴포넌트
  • SOLID 원칙을 잘 지키는 컴포넌트
    • 개발 중에 의식해야 하는 부분 : 응집도와 결합도

좋은 컴포넌트 설계 방법론

  • 나쁜 ui 컴포넌트는 너무 다양한 관심사가 섞여있음
    • 비동기 데이터 페치 로직
    • UI 업데이트
    • 상태 관리
    • 마크업 만들기
    • 스타일 적용하기
    • ...
  • 좋은 컴포넌트란?
    • 하나의 일만 잘하는 컴포넌트
      • 상속보단 합성
      • 단일 책임 원칙

좋은 컴포넌트 만드는 방법

기본 사항

  • 컴포넌트 디자인의 가장 구체적인 요소는 렌더링이다
    • 즉 마크업 & 스타일 & 레이아웃이다.
    • 뷰로 따지면 탬플릿 영역
    • 버튼 / 인풋 같은 아톰 단위 요소가 가장 구체적이다.
  • 구체적인 요소는 가장 재사용성이 떨어진다
    • 슬롯을 통한 의존성 주입을 활용한다.
    • 슬롯 외적인 부분의 재사용성을 높인다.
  • 구체적인 요소를 다루는 기본적인 방법
    • 인터페이스 분리 원칙(슬롯)
    • 캡슐화
  • 컴포넌트의 시각적 영역이 더 커질수록 오히려 추상적이어야 한다.

props 설계

  • 상태와 옵션에 관한 내용만 프롭으로 받는다
    • 렌더링에 쓰일 내용 프롭 X 
      • 레이아웃을 만들면 안된다. 
      • 마크업을 만들면 안된다. (즉 프롭으로 받은 데이터가 슬롯 영역에 나타나면 잘못 만든 것이다.)

state 설계

  • ui적인 상태는 최대한 상단 컴포넌트에서 관리하면 좋음
    • 사이드 이펙트는 상단 컴포넌트로 빼내면 좋다 (Container Component)
    • 퓨어한 자식 컴포넌트들을 합성할 수 있다.
  • ui적인 상태 또한 컴포넌트 단위로 추상화 할 수 있음
  • ui적인 상태를 다루는 로직을 컴포넌트 단위로 추상화할 수 있음
  • ui적인 상태 + 로직만 포함한 마크업 없는 컴포넌트 => renderess component

event/emit 설계

  • HTML 이벤트 인터페이스야 말로 가장 잘 알려지고 재사용성 높은 인터페이스다.
    • HTML element의 이벤트를 최대한 직접 이용한다.
  • event는 최대한 feature 단위로 설계하는 것이 좋다.
    • ex) 뒤에 나올 "sortable:stop" 이벤트

Renderess Component

가장 쉽게 생각할 수 있는 렌더리스 컴포넌트는 데이터 페치 컴포넌트다.

프롭으로 url을 제공하면 하단 컴포넌트에 데이터를 제공한다.

해당 데이터는 슬롯을 이용해 활용 가능하다

<data-fetcher url="/johndoe" v-slot="{test}">
	{{test}}
</data-fetcher>

이런 컴포넌트의 탬플릿 영역 구현은 아래처럼 될 것이다.

한 마디로 마크업을 전혀 만들지 않으며 렌더링에 활용할 수 있는 (데이터, 상태)/ 메서드를 제공한다

<template>
   <slot :test="data"/>
</template>

이를 활용하면 v-model을 이용해 렌더링할 데이터를 조작하는 것도 가능하다.


Compound Component

때로 하나의 컴포넌트 만으로 하나의 UI의 기능을 달성하기 어려울 수 있다.

예를 들어 정렬 기능이 있는 리스트가 있다고 생각해보자

리스트를 위해선 최소한 ul/ol, li 두 개의 태그가 필요하다.

 

이 경우 각 태그에 상응하는 컴포넌트 간의 통신 수단이 필요하다.

(ex 정렬 상태 공유)

provide / inject를 통해 props이 아닌 수단으로 데이터 / 상태 / 메서드를 공유할 수 있다.

 

provide / inject는 국지적인 전역 객체 의존성/ 전역상태 처럼 동작한다.

즉 해당 provide 컨텍스트를 활용하는 컴포넌트들은 높은 결합도를 갖게 된다.

이는 어떻게 보면 클래스들을 재활용하기 어려운 스프링 앱이라 생각할 수 있다.

 

하지만 반대로 생각하면 해당 컴포넌트들의 응집도를 높게 유지한다면,

우리는 언제든 붙였다 떼어낼 수 있는 여러개의 스프링 애플리케이션을 통합하여 사용하고 있다고 생각할 수 있다.

또한 잘 설계된 REST API가 충분히 활용될 수 있는 만큼,

이처럼 컴포넌트들을 여러 곳에서 재사용 할 수 있다.

 

prop 전달 시 provide / inject를 사용해야 하는 이유는,

사용하지도 않는 prop들을 더 하위 컴포넌트들로 전달하기 위한 불필요한 의존성이 생기기 때문이다.

이는 인터페이스 분리 법칙을 쉽게 위반하게 할 수 있다.

또한 분리된 API 각각에 컨슈머가 상태를 직접 전달하게 하는 것도 이치에 맞지 않는다.

 

이 개념을 renderess component와 결합하면 매우 재사용성 높은 렌더링 로직만 따로 분리할 수 있다


예제 : Sortable list

위의 원칙을 활용하여 정렬 가능한 리스트를 구현해 보았다.

실행해보기 : codesandbox.io

만들어볼 리스트

  • 정렬 기능은 @Shopify/draggable 기능을 통해 구현하였다.
    • 해당 라이브러리를 사용한 특별한 이유는 없다. 참조한 예제가 해당 라이브러리를 사용하였기 때문
    • 해당 라이브러리는 셀렉터로 CSS 클래스를 사용한다
      • 리스트 아이템 클래스
      • 핸들 클래스
  • Sortable list 컴포넌트는 3개 컴포넌트로 구성되어 있다.
    • SortableList
      • @Shopify/draggable 인스턴스 초기화
      • 자식 컴포넌트를 위한 provide context 초기화
      • 정렬 로직 캡슐화
      • 부모 model 업데이트
        • 부모 모델을 변경하는 대신에 부모 모델을 프롭으로 받아, 정렬된 데이터만 제공할 수도 있음. 이 경우 정렬 데이터 공개
    • SortableItem
      • context를 inject 하여 자식 돔에 클래스 제공
      • 자식 돔을 해당 컴포넌트에 연결할 수 있는 setRef 메서드 공개
    • SortableItem
      • context를 inject 하여 자식 돔에 클래스 제공
      • 자식 돔을 해당 컴포넌트에 연결할 수 있는 setRef 메서드 공개

세 컴포넌트 전부 렌더리스 컴포넌트다.


기타

참고로 renderess component는 컴포저블로 분리할 수도 있지만,

renderess component 를 활용하면 상태와 액션을 상단 컴포넌트에서 관리하는 부담을 덜 수 있다.

또한 컴포넌트의 의존성이 좀 더 명확히 보인다.

 

리액트로도 해당 패턴과 유사하게 구현할 수 있지만 (훅 + 컨테이너 컴포넌트 패턴)

컨테이너 컴포넌트를 사용하면 마크업이 숨겨지는 단점이 있다.

 

또한 <script setup/>과 리액트 컴포넌트의 단점은

로직 영역과 마크업 영역의 대응 관계를 추적하는게 때때로 매우 복잡해 질 수 있다는 점인데

 

렌더리스 컴포넌트를 사용하면 해당 슬롯을 사용하는 데이터는 해당 탬플릿 내부에 있다는 것이 명확하여,

종속성 추적에도 이점이 있다.

 

렌더리스 컴포넌트, scoped slot과 같은 개념은 svelte에도 존재한다.


예제 링크

깃헙 : https://github.com/i0boy/vue3-renderless-component-example

 

GitHub - i0boy/vue3-renderless-component-example: poc for vue3 with renderess component

poc for vue3 with renderess component. Contribute to i0boy/vue3-renderless-component-example development by creating an account on GitHub.

github.com

참고

본문에서 다루는 내용은 tailwind css 창시자인 adam wathan에게 사사받은 내용임...

https://adamwathan.me/advanced-vue-component-design/

 

Advanced Vue Component Design

“The best part of Adam’s course is not only does he show you more advanced patterns for building Vue components, but he also walks you through the evolution of each piece, so you get a sense of why things work the way they do. This approach to teaching

adamwathan.me

 

반응형