컴포넌트 디자인 패턴은
컴포넌트를 개발하면서 마주치게 되는 특정 문제를 해결하면서
소프트웨어의 유지보수성 / 재사용성을 높이는 개발을 위한 도구다.
이전 시리즈 링크 :
2022.12.21 - [분류 전체보기] - Vue3 컴포넌트 디자인 패턴 소개
TL;DR
props는 컴포넌트의 일관성(오케스트레이션)을 위해 사용하면 좋다.
(특히 마크업 스트럭처 컨트롤은 X)
하위 컴포넌트나 htmlElement에 전달하는 경우는 직접 element를 노출하는 방법(slot)을 쓰는 것이 좋다
우리는 보통 컴포넌트를 설계할 때, props 데이터를 중심으로 사고한다.
이는 입출력이 기본인 함수의 멘탈 모델에 익숙하기 때문이다.
Props 정의 : 모범 사례
배열 문법
<script>
export default {
name: 'Movie',
props: ['title', 'length', 'watched']
}
</script>
<template>
<section>
<h1>{{ title }}</h1>
<p>{{ length }} <span v-if="watched">✅</span><p>
</section>
</template>
- prop 명은 처음엔 직관적이지만 종종 잘못 해석하면 버그로 이어질 수 있다.
- 만약 필수로 제공해야 하는 속성을 제공하지 않는다면?
- 각 prop의 정의가 애매하다
- length는 숫자여야 할까? string이어야 할까?
- 어떤 포맷의 string이어야 할까? (05:23 / 5hour 23min)
- 감상여부(watched)는 boolean인가? Y/N인가?
이 컴포넌트는 prop 렌더링만 담당한다
더 복잡한 논리가 관련된 경우 어떤 종류의 버그가 나타날지 알 수 없다.
Prop에 대한 좀 더 명확한 설명을 제공하면서, 개발자가 이해하기 쉬운 형태를 알아보자.
객체 문법
대부분의 시나리오에서 배열 대신 객체 구문을 사용하여 prop을 정의해야 한다.
객체 구문을 사용하면 props 정의의 근본에 해당하는 세 가지 주요 prop을 정의할 수 있다.
- type: 데이터 타입
- required: 필수여부(중요값)
- default: 기본값
<script>
export default {
props: {
length: {
type: Number,
required: true,
default: 90
}
}
}
</script>
정의 가능한 기본 자바스크립트 데이터 타입들은 다음과 같다.
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbols
<script>
export default {
props: {
length: {
type: [Number, String],
required: true,
default: 90
}
}
}
</script>
<script>
export default {
props: {
length: {
type: Number,
default: 90
}
}
}
</script>
prop은 컴포넌트를 사용하는 방법에 대한 자세한 사양을 제공하는 데 유용하지만,
개발자 측에 유연성을 허용하지 않는 단점도 가지고 있다.
Props: Custom Validation
기존 props 정의 방법의 단점
props의 요구사항은 생각보다 더 복잡할 수 있다.
예를 들어 img src 의 포맷이 다르다던가 하는 일이 발생할 수 있다.
📄 BaseBanner.vue
export default {
props: {
image: {
type; String,
default: '/images/placeholder.png'
}
}
}
image 프롭에 다음과 같은 값이 들어오면...?
- /images/movie-poster.pn
- /imagesmovie-poster.png
- images/movie-poster.png
- 이미지는 반드시 루트의 image 디렉터리에 있어야 한다.
- 이미지는 반드시 PNG 혹은 JPEG 포맷이어야 한다.
- prop을 이용해 계산된 속성을 만든다.
- 해당 속성을 검증하고 오류 시 로그를 출력한다.
Custom Validation for Props
📄 MoviePoster.vue
export default {
props: {
image: {
type: String,
default: '/images/placeholder.png',
// Validator takes an anonymous function
// that receives the passed-down value
// as its first argument
validator: propValue => {
// Return a Boolean that will serve as your validation
const propExists = propValue.length > 0
return propExists
}
}
}
}
요구사항 1 : 이미지는 반드시 루트의 image 디렉터리에 있어야 한다.
export default {
props: {
image: {
type; String,
default: '/images/placeholder.png'
validator: propValue => {
const hasImagesDirectory = propValue.indexOf('/images/') > -1
return hasImagesDirectory
}
}
}
}
요구사항 2 : 이미지는 반드시 PNG 혹은 JPEG 포맷이어야 한다.
export default {
props: {
image: {
type; String,
default: '/images/placeholder.png'
validator: propValue => {
const hasImagesDirectory = propValue.indexOf('/images/') > -1
const isPNG = propValue.endsWith('.png')
const isJPEG = propValue.endsWith('.jpeg') || propValue.endsWith('.jpg')
const hasValidExtension = isPNG || isJPEG
return hasImagesDirectory && hasValidExtension
}
}
}
}
타입스크립트에서 사용하는 법 : Vue Playground
Prop의 한계
당신이 만드는 컴포넌트
해당 컴포넌트가 어떤 방식으로 탄생하는지 설명하기 전에, 컴포넌트를 아래처럼 개발하고 있지 않은가?
그렇다면 반드시 해당 문단을 읽어야 한다.
<template>
<h1>My App</h1>
<BaseButton
text="Submit"
:isLoading="loading"
iconLeftName="left-arrow"
iconRightName="right-arrow"
:isLoadingLeft="loadingLeft"
:isLoadingRight="loadingRight"
/>
</template>
버튼 컴포넌트 확장 예제
심플한 버튼이 있다고 가정해보자.
해당 버튼의 기능을 확장해보자
- 지난 라운드를 기반으로 다음 라운드마다 새로운 요구 사항이 주어진다.
- 템플릿 블록이 BaseButton.vue 및 App.vue에서 어떻게 보일지 상상해 보자
- 모든 스타일은 별도로 정의된 CSS 클래스(예: .button, .icon 등)에서 제공된다
요구사항 #1 : 사용자 정의 텍스트
요구사항 #2: 버튼 텍스트 오른쪽에 아이콘을 추가한다.
아래와 같은 컴포넌트를 버튼 텍스트 오른쪽에 추가하려면?
<app-icon :icon="iconName" />
요구사항 #3: 양 쪽 모두에 아이콘을 추가할 수 있어야 한다.
요구사항 #4: 컨텐츠를 로딩 스피너로 변경
아래와 같은 스피너 컴포넌트가 주어진다.
<LoadingSpinner />
해당 스피너를 이용해 로딩 상태를 표시해보자.
요구사항 #5: 아이콘 하나만 로딩스피너로 변경
무지성 Props 도배의 함정(A Common Descent into Props Madness)
📄App.vue
<template>
<h1>My App</h1>
<BaseButton text="Submit" />
</template>
📄BaseButton.vue
<template>
<button class="button">
{{ text }}
</button>
</template>
<script>
export default {
props: {
text: {
type: String,
required: true
}
}
}
</script>
그러나 각 요구 사항이 계속 서로를 기반으로 구축됨에 따라
오래지 않아 다음과 같은 모습이 된다.
📄App.vue
<template>
<h1>My App</h1>
<BaseButton
text="Submit"
:isLoading="loading"
iconLeftName="left-arrow"
iconRightName="right-arrow"
:isLoadingLeft="loadingLeft"
:isLoadingRight="loadingRight"
/>
</template>
📄BaseButton.vue
<template>
<button type="button" class=“nice-button“>
<LoadingSpinner v-if="isLoading" />
<template v-else>
<template v-if="iconLeftName">
<LoadingSpinner v-if="isLoadingLeft" />
<AppIcon v-else :icon=“iconLeftName” />
</template>
{{ text }}
<template v-if="iconRightName">
<LoadingSpinner v-if="isLoadingRight" />
<AppIcon v-else :icon=“iconRightName” />
</template>
</template>
</button>
</template>
<script>
export default {
// Props shortened to Array syntax for brevity
props: [
'text',
'iconLeftName',
'iconRightName',
'isLoading',
'isLoadingLeft',
'isLoadingRight'
]
}
</script>
무지성 Props 도배의 문제점
- 원하는 동작을 제어하는 새로운 prop을 정의하고 (control props)
- 기존 템플릿 및 데이터 모델에 쌓는 것이다.
- 컴포넌트에 대한 사전 경험이 없는 신규 개발자는 새로운 기능을 추가하거나 코드를 디버그하기 위해 복잡한 조건의 미로를 탐색해야 한다
- 많은 prop은 동작을 이해하기 위해 파악해야 하는 거대한 설정 덩어리다.
- 컴포넌트는 직관성을 상실한다.
- prop이 제대로 문서화되지 않았다면 재앙이다
- 복잡한 컴포넌트는 유사한 대체 컴포넌트를 낳는다.
- 데드라인이 고품질 코드보다 우선하기 때문
- 재사용을 포기하고 대체 컴포넌트를 개발하기 시작하면 관리가 어려워짐
간단하고 깔끔한 해결 방법
prop에만 의존하는 대신 다음 컴포넌트 설계 기술인 slot을 사용할 때가 되었다.
📄App.vue
<template>
<h1>My App</h1>
<BaseButton>Submit <AppIcon name="arrow-right" /></BaseButton>
</template>
📄BaseButton.vue
<template>
<button class="button">
<slot />
</button>
</template>
단일 슬롯 요소를 사용하면 컴포넌트의 코드 한 줄을 변경하지 않고도 모든 요구 사항을 충족하였다.
수정이 필요없으니 버그의 가능성이 줄어든다! (기존 코드 깨짐 x)
정리
- props는 컴포넌트의 일관성 규칙을 정의하는 매우 강력한 기술이다.
- 하지만 컴포넌트 기능 추가 시 prop에 너무 많이 의존하면 복잡성이 증가하고 유연하지 않은 컴포넌트가 생성될 수 있다.
참조
https://catchts.com/validators
https://www.uplift.ltd/posts/validating-routes-with-typescript/
'FrontEnd' 카테고리의 다른 글
Vue3 컴포넌트 디자인 패턴 : Scoped Slots (0) | 2022.12.22 |
---|---|
Vue3 컴포넌트 디자인 패턴 : Slots (0) | 2022.12.22 |
Vue3 컴포넌트 디자인 패턴 소개 (0) | 2022.12.21 |
[Vue3] Vue3은 리렌더링을 어떻게 트리거할까? (0) | 2022.12.21 |
[Storybook/ Vue3] Play 함수를 이용하여 컴포넌트 상호작용 자동화 (0) | 2022.12.19 |