Skip to content

Slots in Vue.js

Vue 컴포넌트는 자바스크립트 값과는 달리 템플릿 콘텐츠를 자식 컴포넌트로 전달하고, 그 컴포넌트 내에서 이를 렌더링하게 할 수 있는 기능, 즉 슬롯을 제공합니다.

슬롯의 기본 사용

예를 들어, <FancyButton> 컴포넌트가 다음과 같이 사용될 수 있습니다:

template
<FancyButton>
  Click me! <!-- 슬롯 콘텐츠 -->
</FancyButton>

<FancyButton>의 템플릿은 다음과 같습니다:

template
<button class="fancy-btn">
  <slot></slot> <!-- 슬롯 아웃렛 -->
</button>

렌더링된 최종 DOM은 다음과 같습니다:

html
<button class="fancy-btn">Click me!</button>

이 예에서 <FancyButton>은 외부 <button>을 렌더링하고 (그리고 그 멋진 스타일링을 제공하면서), 내부 콘텐츠는 부모 컴포넌트에서 제공됩니다.

렌더 스코프

슬롯 콘텐츠는 부모 컴포넌트의 데이터 스코프에 접근할 수 있습니다. 예를 들어:

template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

여기서 두 인터폴레이션은 같은 내용을 렌더링합니다.

슬롯 콘텐츠는 자식 컴포넌트의 데이터에 접근할 수 없습니다. Vue 템플릿의 표현식은 정의된 스코프에서만 접근할 수 있으며, 이는 자바스크립트의 렉시컬 스코핑과 일관됩니다.

폴백 콘텐츠

슬롯에 대해 폴백(기본) 콘텐츠를 지정하는 것이 유용할 때가 있습니다. 예를 들어, <SubmitButton> 컴포넌트에서:

template
<button type="submit">
  <slot></slot>
</button>

부모가 슬롯 콘텐츠를 제공하지 않았다면 "Submit" 텍스트를 렌더링하고 싶을 수 있습니다. "Submit"을 폴백 콘텐츠로 만들기 위해 <slot> 태그 사이에 배치할 수 있습니다:

template
<button type="submit">
  <slot>
    Submit <!-- 폴백 콘텐츠 -->
  </slot>
</button>

이제 <SubmitButton>을 부모 컴포넌트에서 사용하고 슬롯 콘텐츠를 제공하지 않는 경우:

template
<SubmitButton />

폴백 콘텐츠 "Submit"이 렌더링됩니다:

html
<button type="submit">Submit</button>

콘텐츠를 제공하는 경우:

template
<SubmitButton>Save</SubmitButton>

제공된 콘텐츠가 대신 렌더링 됩니다:

html
<button type="submit">Save</button>

명명된 슬롯(Named Slots)

단일 컴포넌트에서 여러 슬롯 아웃렛을 가질 필요가 있을 때가 있습니다. 예를 들어, 다음 템플릿을 가진 <BaseLayout> 컴포넌트를 고려해 보세요:

template
<div class="container">
  <header>
    <!-- 헤더 콘텐츠가 필요합니다 -->
  </header>
  <main>
    <!-- 메인 콘텐츠가 필요합니다 -->
  </main>
  <footer>
    <!-- 푸터 콘텐츠가 필요합니다 -->
  </footer>
</div>

이 경우, <slot> 요소는 고유 ID를 할당하기 위한 name 특수 속성을 사용할 수 있습니다:

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<slot> 요소에 이름이 없으면 자동으로 "default" 이름이 됩니다.

<BaseLayout>을 사용하는 부모 컴포넌트에서는 다른 슬롯 아웃렛을 대상으로 여러 슬롯 콘텐츠 조각을 전달할 수 있는 방법이 필요합니다. 이것이 명명된 슬롯의 사용처입니다.

명명된 슬롯을 전달하려면, v-slot 디렉티브를 사용하는 <template> 요소를 사용해야 하며, v-slot에 슬롯의 이름을 인수로 전달합니다:

template
<BaseLayout>
  <template v-slot:header>
    <!-- 헤더 슬롯 콘텐츠 -->
  </template>
</BaseLayout>

v-slot은 #로 축약될 수 있으므로, <template v-slot:header><template #header>로 간단히 줄일 수 있습니다. 이는 "자식 컴포넌트의 'header' 슬롯에서 이 템플릿 조각을 렌더링하라"라고 생각할 수 있습니다.

모든 세 슬롯을 위해 콘텐츠를 전달하는 코드는 축약 구문을 사용하여 다음과 같이 됩니다:

template
<BaseLayout>
  <template #header>
    <h1>여기에 페이지 타이틀이 있을 수 있습니다</h1>
  </template>

  <template #default>
    <p>메인 콘텐츠를 위한 단락입니다.</p>
    <p>그리고 또 하나입니다.</p>
  </template>

  <template #footer>
    <p>여기에 연락처 정보가 있습니다</p>
  </template>
</BaseLayout>

이제 <template> 요소 내의 모든 것은 해당 슬롯에 전달됩니다. 최종 렌더링된 HTML은 다음과 같을 것입니다:

html
<div class="container">
  <header>
    <h1>여기에 페이지 타이틀이 있을 수 있습니다</h1>
  </header>
  <main>
    <p>메인 콘텐츠를 위한 단락입니다.</p>
    <p>그리고 또 하나입니다.</p>
  </main>
  <footer>
    <p>여기에 연락처 정보가 있습니다</p>
  </footer>
</div>

자바스크립트 함수로 비유하면, 여러 슬롯 프래그먼트를 다른 장소에서 렌더링할 수 있도록 패스하는 것을 다루고 있습니다:

js
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`,
});

// <BaseLayout>는 서로 다른 장소에서 이들을 렌더링합니다
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`;
}

조건부 슬롯

슬롯이 존재하는지 여부에 따라 무언가를 렌더링하고 싶을 때가 있습니다.

$slots 속성과 v-if를 조합하여 이를 실현할 수 있습니다.

아래 예시에서는 헤더/푸터가 존재할 때 추가 스타일링을 제공하고자 할 때 래핑하는 Card 컴포넌트를 정의합니다:

vue
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>

    <div class="card-content">
      <slot />
    </div>

    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

동적 슬롯 이름

v-slot에도 동적 지시문 인수가 작동하여 동적 슬롯 이름을 정의할 수 있습니다:

vue
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- shorthand -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

동적 지시문 인수의 표현식은 동적 지시문 인수의 구문 제약을 따릅니다.

범위가 지정된 슬롯(Scoped Slots)

슬롯 콘텐츠는 부모 스코프의 데이터에 접근할 수 있지만, 슬롯의 콘텐츠가 자식 컴포넌트의 데이터를 사용할 수 있게 하려면 자식이 렌더링할 때 데이터를 슬롯에 전달하는 방법이 필요합니다.

실제로 자식 컴포넌트는 슬롯에 속성을 전달할 수 있습니다:

vue
<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

단일 기본 슬롯과 명명된 슬롯을 사용할 때 슬롯 속성을 받는 방법을 보여줍니다. 먼저 단일 기본 슬롯을 사용할 때 v-slot을 직접 자식 컴포넌트 태그에 사용하여 슬롯 속성을 받습니다:

vue
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

슬롯 속성은 자식이 슬롯을 렌더링할 때 전달한 속성이며, 슬롯 내의 표현식에서 접근할 수 있습니다.

명명된 슬롯을 사용할 경우, 슬롯 속성은 v-slot 디렉티브의 값으로 접근 가능하며, 이는 v-slot:name="slotProps"와 같은 형식으로 사용됩니다. 축약 형식을 사용하면 다음과 같이 됩니다:

vue
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

슬롯을 통해 전달된 속성에 대한 자세한 설명은 슬롯의 용도와 기능성을 확장하여 범위가 지정된 슬롯의 사용 예를 포함하여 설명합니다.

명명된 범위가 지정된 슬롯(Named Scoped Slots)

명명된 범위가 지정된 슬롯은 기본적으로 같은 방식으로 작동합니다. 예를 들어, 다음과 같이 v-slot 디렉티브에 슬롯의 이름과 slotProps를 전달할 수 있습니다:

template
<MyComponent>
  <template #header="headerProps">
    <h1>{{ headerProps.title }}</h1>
  </template>

  <template #default="defaultProps">
    <div>{{ defaultProps.description }}</div>
  </template>

  <template #footer="footerProps">
    <footer>{{ footerProps.copyright }}</footer>
  </template>
</MyComponent>

슬롯을 통해 전달되는 속성은 자식 컴포넌트에서 슬롯 아웃렛에 지정된 속성을 통해 제공됩니다. 이는 함수를 통해 슬롯 내용을 조정하는 방식과 유사합니다.

동적 슬롯

동적 슬롯 이름을 사용하는 예시는 다음과 같습니다. 이는 템플릿 내에서 계산된 속성 이름이나 변수를 기반으로 슬롯 이름을 동적으로 결정할 때 유용합니다:

vue
<DynamicComponent>
  <template v-slot:[dynamicName]>
    <p>This is a dynamic slot content.</p>
  </template>
</DynamicComponent>

여기서 dynamicName은 컴포넌트 내에서 계산되거나 props를 통해 전달된 값일 수 있습니다.

슬롯과 컴포넌트의 상호작용

Vue.js에서 슬롯을 사용하면 자식 컴포넌트의 내부 템플릿에 부모 컴포넌트에서 전달된 콘텐츠를 주입할 수 있습니다. 이는 코드의 재사용성과 유지보수성을 향상시키는데 도움을 줍니다. 슬롯은 Vue 컴포넌트 디자인의 핵심 부분이며, 효과적인 아키텍처 설계에 기여합니다.