Slot 컨텐츠와 아웃렛
모든 타입의 자바스크립트 값이 될 수 있는 props를 수용할 수 있는 컴포넌트를 배웠다. 템플릿 컨텐츠는 어떻게 할까? 필요에따라, 템플릿 조각을 자식컴포넌트로 보내기 위해, 그리고 자식컴포넌트가 자신의 템플릿 안에 조각을 렌더링하도록 하는 것을 원한다.
예를들면, 다음과 같은 사용법을 지원하는 <FancyButton> 컴포넌트가 있다:
<FancyButton>
Click me! <!-- slot 내용 -->
</FancyButton>
<FancyButton>의 템플릿은 다음과 같다:
<button class="fancy-btn">
<slot></slot> <!-- slot outlet -->
</button>
<slot> 엘리먼트는 렌더링 될 수 있는 부모가 제공하는 slot 컨텐츠를 가리키는 slot outlet이다.
최종적으로 렌더링된 DOM은 다음과같다:
<button class="fancy-btn">
Click me!
</button>
slot들과 함께, <FancyButton>은 안쪽 컨텐츠가 부모 컴포넌트로부터 제공되는 동안 바깥쪽<button>을 렌더링할 책임이 있다.( 그리고 <button>을 스타일링하는 책임)
slots을 이해하는 다른 방법은 자바스크립트 함수와 slot을 비교하는 것이다:
// 부모 컴포넌트가 넘긴 slot 컨텐츠
FancyButton('Click me!')
// FancyButton은 스스로의 템플릿에 있는 slot content를 렌더링함
function FancyButton(slotContext) {
return (
`<button class="fancy-btn">
${slotContent}
</button`
)
}
Slot content는 text로 제간되지 않는다. slot content는 모든 유효한 템플릿 컨텐츠가 될 수 있다. 예를 들면, 여러개의 엘리먼트나 심지어 다른 컴포넌트도 넘길 수 있다:
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name=:"plus" />
</FancyButton>
slot을 사용해서, <FancyButton>은 더 유연하고 재사용가능하다. 같은 고급스러운 스타일링을 적용하면서 다른 내용을 다른 장소에서 사용할 수 있다.
뷰 컴포넌트의 slot 메카니즘은 native웹 컴포넌트 <slot> 엘리먼트에서 영감을 받았지만, 나중에 살펴볼 부가적인 능력을 지녔다.
렌더링 스코프
Slot content는 부모 컴포너트의 데이터 스코프에 접근하는데, 왜냐면 부모에 정의되었기 때문이다. 에를들어:
<span>{{ message }}</span>
<FanyButton>{{ message }}</FancyButton>
{{ message }} 보간법 둘 다는 같은 내용을 렌더링 할 것이다.
Slot content는 자식 컴포넌트의 데이터에는 접근할 수 없다. 이 룰로 인해, 다음을 기억하라:
부모 템플릿에 있는 모든 것은 부모 범위에서 컴파일 되고,
자식 템플릿에 있는 모든 것은 자식 범위에서 컴파일 된다.
Fallback Content
아무 컨텐츠도 제공되지 않았을 때 렌더링 하기 위해, slot에 대한 특정한 fallback(즉, default)> 컨텐츠가 유용한 경우가 있다. 예를 들어, <SubmitButton> 컴포넌트가 있다:
<button type="submit">
<slot></slot>
</button>
만약 부모가 어떤 slot 컨텐츠도 제공하지 않았다면, <button>안에 렌더링 될 Submit이라는 텍스트를 원한다. fallback content로 "Submit"을 만들기 위해, <slot> 태그 사이에 놓을 수 있다:
<button type="submit">
<slot>
Submit <!-- fallback content -->
</slot>
</button>
이제 slot을 위한 컨텐츠를 제공하지 않는 부모 컴포넌트를 <SubmitButton>에 사용할 때는:
<SubmitButton />
"Submit" fallback content가 렌더링 될 것이다:
<button type="submit">Submit</button>
하지만 content를 제공하면:
<SubmitButton>Save</SubmitButton>
제공된 content가 대신 렌더링 될 것이다:
<button type="submit">Save</button>
Named Slots
싱글 컴포넌트에서 여러개의 slot outlest을 갖는게 유용한 경우가 있다. 예를들면, <BaseLayout> 컴포넌트는 다음과 같은 템플릿을 가진다:
<div class="containter">
<header>
<!-- We want header content here -->
</header>
<main>
<!-- We want main content here -->
</main>
<footer>
<!-- We want footer content here -->
</footer>
</div>
이 경우에, <slot> 엘리먼트는 트별한 어트리뷰트인 name 을 가진다. name 어트리뷰트는 다른 slots들 사이에 유니크한 ID값을 배정하는데 사용되서, 어떤 컨텐츠가 렌더링될지 결정할 수 있다:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
명시적인. name을 가지지 않은 <slot>의 outlet은 "default" name을 가진다.
부모 컴포넌트에서 <BaseLayout>을 사용할 때, multiple slot content 조각을 넘기는 방법이 필요한데, 다른 slot outlet을 각각 타겟으로 해야 한다. named slots이 필요한 때 이다.
named slot을 넘기기 위해, v-slot 지시어와 <template> 엘리먼트가 필요하다. 그리고 v-slot의 아규먼트로서 slot의 이름을 넘겨야 한다:
<BaseLayout>
<template v-slot:header>
<!-- content for the header slot -->
</template>
</BaseLayout>
v-slot은 전용 약어인 # 이 있다. 그래서 <template v-slot:header>는 <template #header>로 줄여 쓸 수 있다. "자식 컴포넌트의 'header' slot에 템플릿 조각을 렌더링한다"고 생각하라.
여기 약어문법을 사용한 <BaseLayout>의 모든 세개의 slots를 위해 컨텐츠를 넘긴 코드가 있다:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here`s some contact info</p>
</template>
</BaseLayout>
컴포넌트가 default slot과 named slots 둘 다 받을 때, 모든 top-level의 <template>이 아닌 노드들은 암묵적으로 default slot의 컨텐츠로 다뤄진다. 그래서 위 예제는 이렇게 쓰일 수도 있다:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- 암묵적으로 default slot -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here`s some contatct info</p>
</template>
</BaseLayout>
이제 <template> 엘리먼트 안에있는 모든 것들은 대응하는 slots에 넘겨질 것이다. 마지막으로 렌더링된 HTML은 다음과 같다:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here`s some contact info</p>
</footer>
</div>
아래 예제는, 자바스크립트 함수 비유를 사용하면 namted slots을 더 잘 이해하게 도와줄 것이다:
// 다른 이름의 여러개의 슬롯 조각을 넘김
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout>은 다른 장소에 렌더링 한다.
function BaseLayout(slots) {
return (
`<div vlass="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
)
}
다이나믹 Slot Names
Dynmaic directive arguments 는 다이나믹 슬롯 네임의 정의 또한 허락하는 v-slot과도 동작한다:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 약어를 사용하여 -->
<template #{dynamicSlotName]>
...
</template>
</base-layout>
표현식이 다이나믹 지시어 아규먼트의 문법 제약사항을 적용받는 다는 것을 기억하라.
범위가 지정된 Slots (Scoped Slots)
Render Scope에서 다루엇듯이, slot content는 자식 컴포넌트 상태에 접근할 수 없다.
하지만, 슬롯의 컨텐츠가 부모 스코프와 자식 스코프 둘 다의 데이터를 사용하도록 만드는 것이 유용한 경우가 있다. 이를위해, 렌더링 될 때, 자식이 슬롯에게 데이터를 넘기기위한 방법이 필요하다.
실제로, 정확히 할 수 있다. 컴포넌트로 props를 보내듯이 slot outlet으로 어트리뷰트를 보낼 수 있다:
<!-- <MyComponent> template -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
slot props를 받는 것은 싱글 default slot을 사용할 때 vs named slots를 사용할 때 각각 조금 다르다. single default slot을 사용할 때 자식 컴포넌트 태그에서 v-slot 명령어를 사용해서 props를 어떻게 받는지 먼저 보여주겠다:
// 부모 컴포넌트
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotPorps.count }}
</MyComponent>
자식에 의해 slot으로 전달된 props는 (slot안에 표현식으로 접근될 수 있는 )대응하는 v-slot 지시어의 값으로서 사용이 가능하다.
자식 컴포넌트에 전달되는 함수로서 scoped slot을 생각할 수 있다. 그럼 자식 컴포넌트는 이것을 호출하고, 아규먼트로서 props를 패싱한다:
MyComponent({
// default slot을 넘김, 함수로서 넘김
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return (
`<div>${
// props와 함꼐 slot 함수를 호출!
slots.default({ text: greetingMessage, count: 1 })
}</div>`
)
}
실제로, scoped slots이 컴파일 되는 방식과 매우 유사하고, and how you would use scoped slots in manual render functions.
v-slot="slotProps"가 slot 함수 시그니쳐를 어떻게 매칭하는지 기억하라. 함수 아규먼트와 같이 우리는 v-slot 구조분할도 사용할수 있다:
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
Named Scoped Slots
Named scoped slots는 비슷하게 동작한다 - slot props는 v-slot 지시어의 값으로서 접근가능하다: v-slot:name="slotProps". 약어를 쓸때, 이렇게 보인다:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
named slot으로 값 넘기기:
<slot name="header" message="hello"></slot>
slot의 이름은 props안에 포함되지 않는다 왜냐하면 이건 예약되어 있기 때문이다. - 그래서 headerProps의 결과는 {meesage: 'hello' } 이다.
Fancy List 예제
scoped slots의 좋은 유즈 케이스에대해 궁금할 수도 있겠다. 여기 예제가 있다: 아이템의 리스트를 렌더링하는 컴포넌트인 <FancyList>를 상상해라 - 원격 데이터를 싣고, 리스트를 보여주기 위해 데이터를 사용하고, 페이지네이션이나 무한 스크롤링 같은 부가적인 기능등의 로직을 캡슐화했다.하지만, 부모 컴포넌트에서 생각한대로 각각의 아이템이 따로 스타일링 되고, 유연하게 보여지기를 원한다. 그래서 그런 열망하는 사용법은 다음과 같다:
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
<FancyList> 안에서, 다른 아이템 데이터로 여러번 같은<slot>을 렌더링 할 수 있다.(slot props로서 객체를 넘기기위해 v-bind를 사용하는 것을 기억해라):
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
RederlessComponents
위에서 논의한 <FancyList> 유즈 케이스는 재사용가능한 로직(data fetching, pagination etc.)과 비쥬얼 아웃풋이 캡슐화하는 동시에, scoped slots를 통해서 consumer component로 비쥬얼 아웃풋의 일부분을 위임하고 있다.
만약 이 개념을 좀 더 확장하면, 스스로 아무것도 렌더링하지 않으면서 로직을 캡슐화하는 컴포넌트를 생각할 수 있다 - 비쥬얼 아웃풋은 scoped slots로 consumer coponent에 전부 위임한다. 이런 타입의 컴포넌트를 우리는 Renderless Component라고 한다.
Renderless component의 예제는 현재 마우스 위치를 추적하는 로직을 캡슐화하는 것이 있다:
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
흥미로운 패턴인 동시에, Renderless Components로 이룰수있는 대부분은 , 추가적인 컴포넌트의 중첩의 오버헤드를 초래하는일 없이, Composition API로 좀 더 효율적인 패션으로 이룰 수 있다. 추후에, Composable로 같은 마우스 트래킹 기능을 어떻게 구현하는지 살펴볼 것이다.
이건 scoped slots는 <FacyList> 예제와 같이 비쥬얼 아웃풋 구성과 캡슐화된 로직 둘다 필요로 하는 상황에 여전히 유용하다는 점을 시사한다.
'front > vue' 카테고리의 다른 글
[vue3 공식문서 번역]Components.7.Async Components (0) | 2022.05.04 |
---|---|
[vue3 공식문서 번역]Components.6.Provide/inject (0) | 2022.05.04 |
[vue3 공식문서 번역]Components.4.Fallthrough Attributes (0) | 2022.05.03 |
[뷰3 공식문서 번역] 반응형 심화 Reactivity in Depth (0) | 2022.05.03 |
[vue3 공식문서 번역]Components.3.Events (0) | 2022.04.29 |
댓글