Vue Advanced Components
해결하는 문제
- Reusable component에 data, user interaction, content variation을 모두 Props로 표현하면 API가 비대해진다.
- Child가 Props를 직접 수정하면 state ownership과 변경 경로가 불명확해진다.
- Parent가 child의 고정 markup 때문에 같은 UI 구조를 여러 번 복제하게 된다.
적용 맥락
- Form field, card, modal, list item처럼 여러 화면에서 재사용하는 component를 설계할 때.
- Parent가 state를 소유하면서 child interaction을 받아야 할 때.
- Component skeleton은 유지하되 header, body, footer 또는 row content를 바꿔야 할 때.
Vue Component Architecture와의 관계
Vue Component Architecture의 data input, behavior output, content input contract를 실제 API로 설계하는 pattern이다.
Props·Emit·Slot으로 contract 설계하기
1. Props로 read-only data input을 정의한다
- Component가 rendering과 validation에 필요한 최소 data만 받는다.
- Child 내부에서 Props를 변경하지 않는다 — Props를 read-only data로 다루는 것은 source의 설명과 일치한다.[1] parent state와 child state가 암묵적으로 결합되는 Props mutation을 이 원칙을 어길 때 생기는 대표적 실패 모드로 보는 것은 이 page의 해석이다.
- 변경 가능한 local draft가 필요하면 Props에서 local state를 초기화하고 동기화 규칙을 별도로 정한다.
2. Emit으로 interaction과 payload를 알린다
- Event name은 DOM 구현보다 사용자 의도를 표현한다:
click보다save,select,remove가 contract를 읽기 쉽다.change,update처럼 generic한 이벤트 이름만 쓰면 무엇이 발생했는지 알 수 없다. - Parent가 state owner로서 실제 변경을 수행한다.
- 여러 값을 전달할 때는 positional argument보다 의미가 드러나는 payload object를 사용한다. source는 여러 값을 하나의 객체로 묶어 전달하고, complex event logic을 function으로 분리하며,
defineEmits()로 event를 선언하는 방식을 보여준다.[1:1] child 내부 DOM event나 구현 객체 전체를 그대로 parent에 넘기는 payload leakage를 피하라는 것은 이 page의 해석이다.
3. Slot으로 content와 layout variation을 연다
- Text나 markup variation을 boolean/string Props 조합으로 만들지 않는다 — markup variation을 많은 boolean 조합으로 표현하는 Boolean Props explosion은 피해야 할 패턴이다.
- Named slot은 header, actions, footer 같은 명시적 insertion point를 제공한다. source는 Props가 data 전달에, Slots가 content/layout composition에 적합하다고 구분한다.[1:2]
- Scoped slot은 child가 가진 data를 parent template이 rendering할 수 있게 한다. source는 slot props를 통해 child data를 parent의 slot template에 노출하는 예를 제공한다.[1:3] row state나 selection state를 이런 용도의 예로 드는 것과, 단순 data까지 slot으로 넘기면(slot overuse) component 사용법이 장황해진다는 경고는 이 page의 해석이다.
언제 Props/Emit/Slot을 쓸까
| 요구 | 사용할 contract | 이유 |
|---|---|---|
| Child가 읽어야 하는 data | Props | Data ownership과 방향이 명확하다. |
| Child에서 발생한 사용자 의도 | Emit | Parent가 state 변경을 승인한다. |
| Parent가 제공할 markup/content | Slot | Child skeleton과 parent content를 분리한다. |
| Child data를 parent markup에서 rendering | Scoped slot | Child가 slot props로 필요한 범위만 노출한다. |
Reusable API를 소유권 기준으로 평가하기
Reusable component API는 "몇 개의 option을 제공하는가"보다 "누가 data를 소유하고 누가 rendering을 결정하는가"로 평가한다고 볼 수 있다. Props/Emit/Slots를 한 component에서 함께 사용할 수 있지만, 각 contract의 방향을 섞지 않는다. Project-specific vue-board 구현은 보존 대상 지식이 아니라 이 contract가 form/list component에 적용되는 검증 사례로만 사용한다.
예시
<!-- DataTable.vue -->
<script setup>
defineProps({ rows: Array })
const emit = defineEmits(['select'])
</script>
<template>
<table>
<tbody>
<tr v-for="row in rows" :key="row.id" @click="emit('select', { id: row.id })">
<slot name="row" :row="row">
<td>{{ row.name }}</td>
</slot>
</tr>
</tbody>
</table>
</template>
관련
- Vue Component Architecture: 이 pattern의 contract와 ownership 모델을 설명한다.
- Vue Directives:
v-bind,v-on,v-for가 component contract를 template에 연결한다.
출처
테스트 질문
- Props와 Slot 중 무엇을 선택할지 결정하는 기준은 무엇인가?
- Child가 Props를 직접 바꾸지 않고 event를 emit해야 하는 이유는 무엇인가?
- Scoped slot이 필요한 상황은 무엇인가?