Vue3의 Renderess Component / Compound Component 디자인 패턴에 대해 알아봅니다.
Vue3 디자인 패턴 시리즈를 정리하는 내용이 될 것 같습니다.
TL;DR
- renderess component는 마크업 없는 컴포넌트다
- 상태와 액션만을 추상화하는 컴포넌트다.
좋은 컴포넌트란 무엇인가?
- 결합도가 낮고 응집도가 높고 확장성 있는 컴포넌트
- SOLID 원칙을 잘 지키는 컴포넌트
- 개발 중에 의식해야 하는 부분 : 응집도와 결합도
좋은 컴포넌트 설계 방법론
- 나쁜 ui 컴포넌트는 너무 다양한 관심사가 섞여있음
- 비동기 데이터 페치 로직
- UI 업데이트
- 상태 관리
- 마크업 만들기
- 스타일 적용하기
- ...
- 좋은 컴포넌트란?
- 하나의 일만 잘하는 컴포넌트
- 상속보단 합성
- 단일 책임 원칙
- 하나의 일만 잘하는 컴포넌트
좋은 컴포넌트 만드는 방법
기본 사항
- 컴포넌트 디자인의 가장 구체적인 요소는 렌더링이다
- 즉 마크업 & 스타일 & 레이아웃이다.
- 뷰로 따지면 탬플릿 영역
- 버튼 / 인풋 같은 아톰 단위 요소가 가장 구체적이다.
- 구체적인 요소는 가장 재사용성이 떨어진다
- 슬롯을 통한 의존성 주입을 활용한다.
- 슬롯 외적인 부분의 재사용성을 높인다.
- 구체적인 요소를 다루는 기본적인 방법
- 인터페이스 분리 원칙(슬롯)
- 캡슐화
- 컴포넌트의 시각적 영역이 더 커질수록 오히려 추상적이어야 한다.
props 설계
- 상태와 옵션에 관한 내용만 프롭으로 받는다
- 렌더링에 쓰일 내용 프롭 X
- 레이아웃을 만들면 안된다.
- 마크업을 만들면 안된다. (즉 프롭으로 받은 데이터가 슬롯 영역에 나타나면 잘못 만든 것이다.)
- 렌더링에 쓰일 내용 프롭 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 메서드 공개
- SortableList
세 컴포넌트 전부 렌더리스 컴포넌트다.
기타
참고로 renderess component는 컴포저블로 분리할 수도 있지만,
renderess component 를 활용하면 상태와 액션을 상단 컴포넌트에서 관리하는 부담을 덜 수 있다.
또한 컴포넌트의 의존성이 좀 더 명확히 보인다.
리액트로도 해당 패턴과 유사하게 구현할 수 있지만 (훅 + 컨테이너 컴포넌트 패턴)
컨테이너 컴포넌트를 사용하면 마크업이 숨겨지는 단점이 있다.
또한 <script setup/>과 리액트 컴포넌트의 단점은
로직 영역과 마크업 영역의 대응 관계를 추적하는게 때때로 매우 복잡해 질 수 있다는 점인데
렌더리스 컴포넌트를 사용하면 해당 슬롯을 사용하는 데이터는 해당 탬플릿 내부에 있다는 것이 명확하여,
종속성 추적에도 이점이 있다.
렌더리스 컴포넌트, scoped slot과 같은 개념은 svelte에도 존재한다.
예제 링크
깃헙 : https://github.com/i0boy/vue3-renderless-component-example
참고
본문에서 다루는 내용은 tailwind css 창시자인 adam wathan에게 사사받은 내용임...
https://adamwathan.me/advanced-vue-component-design/
'FrontEnd' 카테고리의 다른 글
[Vue3] Vue3로 접근성 고려한 Form(양식) 개발하기 (0) | 2023.01.04 |
---|---|
[번역] layout 성능 정확하게 측정하기 (0) | 2023.01.01 |
[객체지향설계] 객체지향 설계 및 분석 : UML과 Use Case 다이어그램 (0) | 2022.12.30 |
[객체지향 설계] 객체지향 설계의 기초와 핵심개념 (0) | 2022.12.30 |
프론트엔드 성능 최적화 : layout thrashing 피하기 with requestAnimationFrame (0) | 2022.12.29 |