13. Nested Component - Props πŸ‘©β€πŸ’»

1. Parent/Child Component and Props

  • Parent Component

1 ) Component Tags λŠ” PascalCaseλ₯Ό μ‚¬μš©ν•œλ‹€.

<template>
  <MyComponent />
</template>

<script>
import MyComponent from "@/components/MyComponent.vue";

export default {
  components: {
    MyComponent
  }
}
</script>

2 ) μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— 전달할 PropsλŠ” HTML attributes 와 convention 을 λ§žμΆ”κΈ° μœ„ν•΄ kebab-caseλ₯Ό μ‚¬μš©ν•˜λ©°, ν‚€μ˜ 쀑볡을 막기 μœ„ν•΄ 이름을 μΆ•μ•½ 없이 길게 μ„ μ–Έν•œλ‹€.

<template>
  <MyComponent greeting-message="hello" />
</template>

3 ) Dynamic Propsλ₯Ό μ „λ‹¬ν•˜κΈ° μœ„ν•΄ v-bind λ˜λŠ” :λ₯Ό μ‚¬μš©ν•œλ‹€.

<template>
  <MyComponent :greeting-message="post.title" />
</template>
  • Child Component

1 ) λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈκ°€ μ „λ‹¬ν•œ λ°μ΄ν„°λŠ” props property 에 Objectλ₯Ό λ§€ν•‘ν•΄ λ°›λŠ”λ‹€. μ΄λ•Œ 이름은 JavaScript 의 μ‹λ³„μž κ·œμΉ™μ— 맞게 camelCaseλ₯Ό μ‚¬μš©ν•œλ‹€.

<script>
export default {
  props: {
    greetingMessage: String
  }
}
</script>

2 ) propsλŠ” TypeScriptλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³ λ„ λΆ€λͺ¨μ—κ²Œ 전달 받은 Data Type을 μ§€μ •ν•  수 있게 도와쀀닀. μžμ„Έν•œ Type 체크 및 Validation 은 5. Prop Validation λ₯Ό μ°Έκ³ ν•œλ‹€.

2. Static Props

Child Component PageTitleλ₯Ό λ§Œλ“ λ‹€.

  • /src/components/PageTitle.vue
<template>
  <h2>{{ myTitle }}</h2>
</template>

<script>
export default {
  name: "PageTitle",
  props: {
    myTitle: { type: String, default: "νŽ˜μ΄μ§€ 제λͺ©μž…λ‹ˆλ‹€." },
  },
};
</script>


PageTitle μ»΄ν¬λ„ŒνŠΈλ₯Ό λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ— μ£Όμž…μ‹œν‚€κ³ , Static Props둜 데이터λ₯Ό μ „λ‹¬ν•΄λ³΄μž.

  • /src/views/AboutView.vue
<template>
  <PageTitle my-title="About νŽ˜μ΄μ§€μž…λ‹ˆλ‹€." />
</template>

<script>
import PageTitle from "@/components/PageTitle.vue";

export default {
  name: "AboutView",
  components: {
    PageTitle,
  },
};
</script>

Nested Component Props

3. Dynamic Props

Value 자체λ₯Ό μ „λ‹¬ν•˜λŠ” 것이 μ•„λ‹Œ λ³€μˆ˜/μƒμˆ˜λ₯Ό μ „λ‹¬ν•˜λ €λ©΄ v-bind λ˜λŠ” μΆ•μ•½ν˜•μΈ :λ₯Ό μ‚¬μš©ν•œλ‹€.

  • /src/views/AboutView.vue
<template>
  <PageTitle :my-title="someTitle" />
</template>

<script>
import PageTitle from "@/components/PageTitle.vue";

export default {
  name: "AboutView",
  components: {
    PageTitle,
  },
  data() {
    return {
      someTitle: "About νŽ˜μ΄μ§€μž…λ‹ˆλ‹€.",
    };
  },
};
</script>

μžμ‹ μ»΄ν¬λ„ŒνŠΈλŠ” λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ „λ‹¬ν•˜λŠ” 데이터가 Static Props인지, Dynamic Props인지 μ•Œ 수 μ—†λ‹€. 단지 전달 받을 νƒ€μž…λ§Œ μ •μ˜ν•˜κ³  κ·Έ κ°’(Value Types or Reference Types)을 받을 뿐이닀.

λ³„λ„μ˜ λ³€μˆ˜ 없이 μ „λ‹¬ν•˜λ €λ©΄ λ‹€μŒκ³Ό 같이 IIFEλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.

<template>
  <PageTitle :my-title="(() => 'About νŽ˜μ΄μ§€μž…λ‹ˆλ‹€.')()" />
</template>

Static PropsλŠ” String만 전달할 수 μžˆλ‹€. λ”°λΌμ„œ Number, Boolean, Array, Object, Property of Object와 같은 λ°μ΄ν„°λŠ” λͺ¨λ‘ Dynamic Propsλ₯Ό μ‚¬μš©ν•΄ μ „λ‹¬ν•΄μ•Όν•œλ‹€.

4. One-way Data Flow

Vue μžμ²΄λŠ” Two-way data bindingλ₯Ό μ§€μ›ν•˜μ§€λ§Œ, λΆ€λͺ¨ μžμ‹ μ»΄ν¬λ„ŒνŠΈ μ‚¬μ΄λŠ” One-way down binding으둜 μž‘λ™ν•œλ‹€. μ΄λŠ” μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— μ˜ν•΄ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ stateκ°€ λ³€κ²½λ˜λŠ” 것을 ν—ˆμš©ν•˜κ²Œ 되면 μ•±μ˜ 데이터 흐름을 μ΄ν•΄ν•˜κΈ° μ–΄λ ΅κ²Œ λ§Œλ“€κΈ° λ•Œλ¬Έμ΄λ‹€.

Vue에 μ˜ν•΄ Two-way data binding이 μ•ˆ λœλ‹€λŠ” 것 뿐이지 μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈλ‘œ 데이터 전달이 λΆˆκ°€λŠ₯ν•œ 것은 μ•„λ‹ˆλ‹€. Vue에 μ˜ν•΄ binding이 λ˜μ§€ μ•Šκ³ , μžμ‹ μ»΄ν¬λ„ŒνŠΈκ°€ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈλ₯Ό λ³€κ²½ν•  수 μ—†κΈ° λ•Œλ¬Έμ— λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈ μͺ½μ—μ„œ λŠ₯λ™μ μœΌλ‘œ 데이터λ₯Ό λ°˜μ˜ν•˜λŠ” 방법을 μ‚¬μš©ν•  수 μžˆλ‹€.

  1. μžμ‹ μ»΄ν¬λ„ŒνŠΈκ°€ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ ν•¨μˆ˜μ— 데이터λ₯Ό arguments 둜 던져 올리고, λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ ν•¨μˆ˜κ°€ 이 데이터λ₯Ό μžμ‹ μ˜ λ³€μˆ˜μ— μ €μž₯ν•œλ‹€. See: Emitting and Listening to Events
  2. λ‹€λ₯Έ λ°©λ²•μœΌλ‘œλŠ” λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈκ°€ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터λ₯Ό Template Refsλ₯Ό μ΄μš©ν•΄ λ³€ν™”λ₯Ό κ°μ‹œν•˜λ„λ‘ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈ 내에 Computed Propertiesλ₯Ό μ„ μ–Έν•˜κ³ , λ³€ν™”λ₯Ό κ°μ‹œν•  값을 Template Refsλ₯Ό μ΄μš©ν•΄ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ λ³€μˆ˜λ₯Ό μ§€μ •ν•œλ‹€. See:


1 ) Make Props like Local Variable

즉, λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ „λ‹¬λœ μžμ‹ μ»΄ν¬λ„ŒνŠΈλŠ” down binding μƒνƒœμ΄λ―€λ‘œ λΆ€λͺ¨μ˜ μƒνƒœκ°€ λ³€ν•˜λ©΄ 값이 λ³€ν•˜κ²Œ λœλ‹€. λ”°λΌμ„œ, μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ 이λ₯Ό local variable처럼 μ‚¬μš©ν•˜κΈ°λ₯Ό μ›ν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 λ³„λ„μ˜ λ³€μˆ˜μ— 값을 μ €μž₯ν•΄ μ‚¬μš©ν•œλ‹€.

<script>
export default {
  props: ['initialCounter'],
  data() {
    return {
      // counter only uses this.initialCounter as the initial value;
      // it is disconnected from future prop updates.
      counter: this.initialCounter
    }
  }
}
</script>


2 ) Props with Computed Properties

λ³€μˆ˜μ— 값을 μ €μž₯ν•  λ•Œ Observerλ₯Ό μ΄μš©ν•΄ κ°μ‹œν•˜κ³ , μ‚¬μš©μž μ •μ˜ λ‘œμ§μ„ μΆ”κ°€ν•΄ μ €μž₯ν•  수 μžˆλ‹€. ν•˜μ§€λ§Œ, Vue의 Watch의 경우 λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λ„˜μ–΄μ˜€λŠ” 데이터λ₯Ό μžμ‹ μ»΄ν¬λ„ŒνŠΈ λ³€μˆ˜μ— willSet이 μ—†κ³  didSet만 μžˆλ‹€λ³΄λ‹ˆ(Vue Watch Property) Vue 곡식 λ¬Έμ„œλ₯Ό 보면 이런 경우 Compouted Propertiesλ₯Ό μ‚¬μš©ν•˜λ„λ‘ ν•˜κ³  μžˆλ‹€.

<script>
export default {
  props: ['size'],
  computed: {
    // computed property that auto-updates when the prop changes
    normalizedSize() {
      return this.size.trim().toLowerCase()
    }
  }
}
</script>

5. Prop Validation

λ‹¨μˆœνžˆ 값을 μ „λ‹¬ν•˜λŠ” 것 외에 λ‹€μŒκ³Ό 같이 μœ νš¨μ„± 검사λ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€.

export default {
  props: {
    // Basic type check
    //  (`null` and `undefined` values will allow any type)
    propA: Number,
    // Multiple possible types
    propB: [String, Number],
    // Required string
    propC: {
      type: String,
      required: true
    },
    // Number with a default value
    propD: {
      type: Number,
      default: 100
    },
    // Object with a default value
    propE: {
      type: Object,
      // Object or array defaults must be returned from
      // a factory function. The function receives the raw
      // props received by the component as the argument.
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // Custom validator function
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // Function with a default value
    propG: {
      type: Function,
      // Unlike object or array default, this is not a factory function - this is a function to serve as a default value
      default() {
        return 'Default function'
      }
    }
  }
}

14. Nested Component - Template Refs and Event Call πŸ‘©β€πŸ’»

1. Template Refs ($ref) (Parent to Child)

HTMLμ—μ„œ id attribute κ°€ uniqueν•œ 속성을 κ°€μ§„ κ²ƒμ²˜λŸΌ, Vueμ—μ„œλŠ” ref attribute κ°€ 이 역할을 ν•œλ‹€. λ”°λΌμ„œ, Vueμ—μ„œ μ–΄λ–€ μ»΄ν¬λ„ŒνŠΈ λ˜λŠ” Real DOM에 μ ‘κ·Όν•˜κ³  μ‹Άλ‹€λ©΄ refλ₯Ό μ‚¬μš©ν•΄ μ ‘κ·Όν•  수 μžˆλ‹€.

Vueμ—μ„œ Real DOM에 μ ‘κ·Όν•˜κ³  이λ₯Ό λ‹€λ£¨λŠ” 것이 쒋은 방법은 μ•„λ‹ˆμ§€λ§Œ, refλ₯Ό μ΄μš©ν•˜λ©΄ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈκ°€ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ DOM 에 μ ‘κ·Όν•΄ click 이벀트λ₯Ό ν˜ΈμΆœν•˜κ±°λ‚˜, μžμ‹ μ»΄ν¬λ„ŒνŠΈ 내에 μ •μ˜λœ ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜κ±°λ‚˜, μžμ‹ μ»΄ν¬λ„ŒνŠΈ 내에 μ •μ˜λœ λ³€μˆ˜μ— μ ‘κ·Όν•˜λŠ” 것과 같은 λͺ¨λ“  ν–‰μœ„κ°€ κ°€λŠ₯ν•˜λ‹€(window.opener둜 μ ‘κ·Όν•  λ•Œμ˜ λΆ€λͺ¨ μžμ‹ μ°½ 관계와 μœ μ‚¬ν•˜λ‹€).

2. Emitting and Listening to Events ($emit) (Child to Parent)

반면, μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ— 무언가 μ§μ ‘μ μœΌλ‘œ 데이터λ₯Ό 전달할 수 μžˆλŠ” 방법은 λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ— μ •μ˜λ˜μ–΄ 있으며, μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— binding 된 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜κΈ° μœ„ν•΄ emittingν•˜λŠ” κ²ƒλ§Œ ν—ˆμš©λœλ‹€. ν•˜μžλ―Ό μ •ν™•νžˆ μ–˜κΈ°ν•˜λ©΄, λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈκ°€ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— μ œκ³΅ν•œ ν•¨μˆ˜κ°€ 호좜 μš”μ²­μ΄ μžˆλŠ”μ§€ listeningν•˜κ³  있고, μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ 제곡된 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λŠ” μš”μ²­κ³Ό 함깨 argumentsλ₯Ό 전달해 μ˜¬λ¦¬λŠ” emitting이 λ°œμƒλ¨μœΌλ‘œ 인해 μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λŠ” κ²ƒμ²˜λŸΌ λ³΄μ΄λŠ” 것일 뿐이닀.

Props와 λ§ˆμ°¬κ°€μ§€λ‘œ, Parent Componentμ—μ„œ Component Tags에 attribute ν˜•νƒœλ‘œ λ“±λ‘λ˜λŠ” v-bindλŠ” λͺ¨λ‘ kebab-caseλ₯Ό μ‚¬μš©ν•˜κ³ , Child Componenetμ—μ„œ 이것을 binding ν•  λ³€μˆ˜λŠ” λͺ¨λ‘ camelCaseλ₯Ό μ‚¬μš©ν•œλ‹€.

3. Computed Properties (Child to Parent)

Computed Properties의 νƒ€κ²Ÿμ΄ 자기 μžμ‹ μ˜ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터일 경우 Two-way binding도 되고 μ•„λ¬΄λŸ° μ œμ•½μ΄ μ—†λ˜ 것과 달리 νƒ€κ²Ÿμ„ Template Refsλ₯Ό μ΄μš©ν•΄ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ 값을 κ°μ‹œν•  경우, 이 λ³€μˆ˜λŠ” λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ script λ‚΄μ—μ„œλ§Œ μ‚¬μš©μ΄ κ°€λŠ₯ν•˜λ‹€.
(template에 μ‚¬μš©λœ λ³€μˆ˜λŠ” Two-way data binding으둜 μž‘λ™ν•˜λŠ”λ°, μ΄λ•Œ template의 λ³€κ²½ 내역이 script에 μ •μ˜λœ Computed Property둜 전달 되고, μ΄λŠ” λ‹€μ‹œ νƒ€κ²ŸμΈ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터λ₯Ό ν–₯ν•˜κ²Œ λœλ‹€. λ¬Έμ œλŠ” μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ λ³€κ²½ 사항은 λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λŠ₯λ™μ μœΌλ‘œ μ œμ–΄λ˜λŠ” 것일 뿐, μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈλ‘œ 전달을 ν•˜μ§€ λͺ» ν•˜λ―€λ‘œ Two-way data binding의 흐름이 λŠμ–΄μ§„λ‹€.)

4. Template Refs and Event Call Examples

  • /src/views/EventCallView.vue
<template>
  <h1>Parent Component</h1>
  <button type="button" @click="parentFunc">
    λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈ 이벀트 호좜
  </button>
  <button type="button" @click="callChildEvent">
    λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈ 이벀트 호좜(Real DOM 에 μ ‘κ·Ό)
  </button>
  <button type="button" @click="callChildFunc">
    λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈ 이벀트 호좜(ν•¨μˆ˜μ— 직접 μ ‘κ·Ό)
  </button>
  <button type="button" @click="setChildVariable">
    λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈ 데이터 'alpha' λ³€κ²½
  </button>
  <!--  <p>-->
  <!--    μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터λ₯Ό 'beta' λ₯Ό computed ν•˜λŠ” λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터:-->
  <!--    {{ syncedWithChildComponentVariable }}-->
  <!--  </p>-->
  <button type="button" @click="popSyncedVariable">
    μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터 'beta' λ₯Ό computed ν•˜λŠ” λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ 데이터 νŒμ—…
  </button>
  <hr />
  <ChildComponent @parent-Func="parentFunc" ref="childComponent" />
</template>

<script>
import ChildComponent from "@/components/ChildComponent.vue";

export default {
  name: "EventCallView",
  components: {
    ChildComponent,
  },
  methods: {
    parentFunc() {
      alert("λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈ μ΄λ²€νŠΈκ°€ ν˜ΈμΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
    },
    callChildEvent() {
      this.$refs.childComponent.$refs.myChildButton.click();
    },
    callChildFunc() {
      this.$refs.childComponent.childFunc();
    },
    setChildVariable() {
      this.$refs.childComponent.alpha = Math.trunc(Math.random() * 10);
    },
    popSyncedVariable() {
      alert(this.syncedWithChildComponentVariable);
    },
  },
  computed: {
    syncedWithChildComponentVariable() {
      return this.$refs.childComponent.beta;
    },
  },
};
</script>
  • /src/components/ChildComponent.vue
<template>
  <h1>Child Component</h1>
  <button type="button" @click="childFunc" ref="myChildButton">
    μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈ 이벀트 호좜
  </button>
  <button type="button" @click="callParentFunc">
    μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈ 이벀트 호좜
  </button>
  <button type="button" @click="setSelfVariable">
    μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈ 데이터 'beta' λ³€κ²½
  </button>
  <p>alpha: {{ alpha }}</p>
  <p>beta: {{ beta }}</p>
</template>

<script>
export default {
  name: "ChildComponent",
  data() {
    return {
      alpha: 0,
      beta: 0,
      gamma: "abc",
    };
  },
  methods: {
    childFunc() {
      alert("μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ μ΄λ²€νŠΈκ°€ ν˜ΈμΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
    },
    callParentFunc() {
      this.$emit("parentFunc");
    },
    setSelfVariable() {
      this.beta = Math.trunc(Math.random() * 10);
    },
  },
};
</script>

Nested Component Refs and Emit


15. Nested Component - Slots πŸ‘©β€πŸ’»

1. Slot Content and Outlet

μΌκ΄€λœ λ””μžμΈμ€ UI/UX에 맀우 μ€‘μš”ν•˜λ‹€. νŒμ—…μ°½μ„ 예둜 λ“€λ©΄, λ™μΌν•œ νŒμ—…μ΄λΌλ„ κ°œλ°œμžκ°€ 맀번 직접 κ΅¬ν˜„ν•  경우 μ‹€μˆ˜λ“  μ„œλ‘œ λ‹€λ₯Έ κ°œλ°œμžμ— μ˜ν•΄ 개발자의 주관이 μ„žμ΄κ²Œ λ˜λ“  λ‹€λ₯Έ 뢀뢄이 λ‚˜νƒ€λ‚˜κ²Œ λœλ‹€.

곡톡화 및 μž¬μ‚¬μš©μ„ μœ„ν•΄ Vue λŠ” Componenetsλ₯Ό μ΄μš©ν•œλ‹€. ν•˜μ§€λ§Œ λ‹¨μˆœν•œ λͺ¨λ‹¬μ°½, 타이틀과 같은 μ»΄ν¬λ„ŒνŠΈλŠ” λΆ€λͺ¨ μžμ‹κ°„ propsλ₯Ό μ΄μš©ν•΄ 데이터λ₯Ό μ „λ‹¬ν•˜κ³  μ „λΆ€ κ΅¬ν˜„ν•΄μ•Όν•˜λŠ” λΆˆνŽΈν•¨μ΄ μžˆλ‹€. 이런 곡톡 μ»΄ν¬λ„ŒνŠΈ 내에 Slots을 μ΄μš©ν•˜λ©΄ HTML을 μž‘μ„±ν•΄ κ·ΈλŒ€λ‘œ μ£Όμž…ν•˜λŠ” 것이 κ°€λŠ₯ν•΄ κ°€λ²Όμš΄ λ ˆμ΄μ•„μ›ƒμ„ μ‰½κ²Œ μž¬μ‚¬μš© ν•  수 μžˆλ‹€.

  • /src/components/PageTitle.vue

2. Static Props μ—μ„œ /src/components/PageTitle.vue λ₯Ό μ΄μš©ν•΄ νŽ˜μ΄μ§€μ— 타이틀을 κ³΅ν†΅ν™”ν–ˆλ‹€.

<template>
  <h2>{{ myTitle }}</h2>
</template>

<script>
export default {
  name: "PageTitle",
  props: {
    myTitle: { type: String, default: "νŽ˜μ΄μ§€ 제λͺ©μž…λ‹ˆλ‹€." },
  },
};
</script>
  • /src/components/common/SlotPageTitle.vue

이것을 Slot으둜 λ°”κΎΈλ©΄ λ‹€μŒκ³Ό κ°™λ‹€.

<template>
  <h2><slot /></h2>
</template>

<script>
export default {
  name: "SlotPageTitle",
};
</script>


λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈ μ—­μ‹œ λ‹¨μˆœ μ»΄ν¬λ„ŒνŠΈλ₯Ό μž¬μ‚¬μš© ν•  λ•Œμ™€ Slot을 μ‚¬μš©ν•œ μ»΄ν¬λ„ŒνŠΈλ₯Ό μž¬μ‚¬μš© ν•  λ•Œ μ½”λ“œλŠ” λ‹€μŒκ³Ό 같이 λ³€κ²½λœλ‹€.

  • /src/views/AboutView.vue (with Ordinary Component)
<template>
  <PageTitle my-title="About νŽ˜μ΄μ§€μž…λ‹ˆλ‹€." />
</template>

<script>
import PageTitle from "@/components/PageTitle.vue";

export default {
  name: "AboutView",
  components: {
    PageTitle,
  },
};
</script>
  • /src/views/AboutView.vue (with Slot Component)
<template>
  <SlotPageTitle> About νŽ˜μ΄μ§€μž…λ‹ˆλ‹€. </SlotPageTitle>
</template>

<script>
import SlotPageTitle from "@/components/common/SlotPageTitle.vue";

export default {
  name: "AboutView",
  components: {
    SlotPageTitle,
  },
};
</script>

Nested Component Slots

2. Named Slots

단일 Slot 이 μ•„λ‹Œ 경우 κ΅¬λΆ„ν•˜κΈ° μœ„ν•΄ 'name' 이 ν•„μš”ν•˜λ‹€. 각 name 은 element λ₯Ό μ΄μš©ν•΄ μ‚½μž…ν•˜κ³ , v-slot attribute λ₯Ό μ΄μš©ν•΄ μ—°κ²°ν•  수 μžˆλ‹€.
μ—¬λŸ¬ Slots 쀑 ν•˜λ‚˜μ˜ Slot 에 ν•œν•΄ name 을 μƒλž΅ν•  수 μžˆλŠ”λ°, 이 경우 v-slot:default을 μ΄μš©ν•΄ μ—°κ²°ν•œλ‹€.

header, main, footer 둜 κ΅¬μ„±λœ νŒμ—… λͺ¨λ‹¬ λ ˆμ΄μ•„μ›ƒμ„ Slot으둜 λ§Œλ“€μ–΄ μ μš©ν•΄λ³΄μž.

  • /src/components/common/SlotModalLayout.vue
<template>
  <div class="modal-container">
    <header>
      <h1>
        <slot name="header"></slot>
      </h1>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<script>
export default {
  name: "SlotModalLayout",
};
</script>

<style scoped>
.modal-container {
  width: 500px;
  --modal-border: 30px;
}

.modal-container > header {
  height: 50px;
  background: aquamarine;
  border-top-left-radius: var(--modal-border);
  border-top-right-radius: var(--modal-border);
}

.modal-container > main {
}

.modal-container > footer {
  height: 40px;
  background: aquamarine;
  border-bottom-left-radius: var(--modal-border);
  border-bottom-right-radius: var(--modal-border);
}
</style>
  • /src/views/SlotModalLayoutView.vue
<template>
  <button type="button" @click="openPopup">
    {{ popupState ? "Close Popup" : "Open Popup!!" }}
  </button>

  <hr />

  <SlotModalLayout v-show="popupState === true">
    <template v-slot:header> νŒμ—… 타이틀 </template>
    <template v-slot:default>
      <p>μ•Œλ¦Ό 1 : μ•ˆλ…•ν•˜μ„Έμš”</p>
      <p>μ•Œλ¦Ό 2 : λ°˜κ°‘μŠ΅λ‹ˆλ‹€</p>
    </template>
    <template v-slot:footer>
      <button type="button" @click="openPopup">λ‹«κΈ°</button>
    </template>
  </SlotModalLayout>
</template>

<script>
import SlotModalLayout from "@/components/common/SlotModalLayout.vue";

export default {
  name: "SlotModalLayoutView",
  data() {
    return {
      popupState: false,
    };
  },
  components: {
    SlotModalLayout,
  },
  methods: {
    openPopup() {
      this.popupState = !this.popupState;
    },
  },
};
</script>

그리고 v-slot:은 #을 μ΄μš©ν•΄ λ‹¨μΆ•ν˜•μœΌλ‘œ μž‘μ„±ν•  수 μžˆλ‹€.

<template>
  <button type="button" @click="openPopup">
    {{ popupState ? "Close Popup" : "Open Popup!!" }}
  </button>

  <hr />

  <SlotModalLayout v-show="popupState === true">
    <template #header> νŒμ—… 타이틀 </template>
    <template #default>
      <p>μ•Œλ¦Ό 1 : μ•ˆλ…•ν•˜μ„Έμš”</p>
      <p>μ•Œλ¦Ό 2 : λ°˜κ°‘μŠ΅λ‹ˆλ‹€</p>
    </template>
    <template #footer>
      <button type="button" @click="openPopup">λ‹«κΈ°</button>
    </template>
  </SlotModalLayout>
</template>

Nested Component Slots

3. Slot Examples

1. Slot Content and Outlet, 2. Named Slots 에 μΆ”κ°€λ‘œ λ²„νŠΌ μŠ€νƒ€μΌμ„ 곡톡화 ν•˜λŠ” Vue.js documents 예제λ₯Ό ν•˜λ‚˜ 더 μ†Œκ°œν•œλ‹€.

  • /src/components/common/FancyButton.vue
<template>
  <button class="fancy-btn">
    <slot />
  </button>
</template>

<style scoped>
.fancy-btn {
  color: #fff;
  background: linear-gradient(315deg, #42d392 25%, #647eff);
  border: none;
  padding: 5px 10px;
  margin: 5px;
  border-radius: 8px;
  cursor: pointer;
}
</style>
  • /src/components/common/AwesomeIcon.vue
<!-- using an emoji just for demo purposes -->
<template>❀️</template>
  • /src/views/FancyButtonView.vue
<template>
  <FancyButton> Click me </FancyButton>

  <FancyButton>
    <span style="color: cyan">Click me! </span>
    <AwesomeIcon />
  </FancyButton>
</template>

<script>
import FancyButton from "@/components/common/FancyButton.vue";
import AwesomeIcon from "@/components/common/AwesomeIcon.vue";

export default {
  name: "FancyButtonView",
  components: {
    FancyButton,
    AwesomeIcon,
  },
};
</script>

Nested Component Slots 2


16. Nested Component - Provide/Inject πŸ‘©β€πŸ’»

Propsλ₯Ό μ‚¬μš©ν•˜λ©΄ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈλ‘œ 데이터λ₯Ό 전달할 수 μžˆλ‹€. λ¬Έμ œλŠ” 계측이 1 계측이 μ•„λ‹Œ 경우 λΆ€λͺ¨μ—μ„œ μžμ‹μ—κ²Œ, 또 λ‹€μ‹œ λΆ€λͺ¨μ—μ„œ μžμ‹μ—κ²Œβ€¦ μ΄λŸ°μ‹μœΌλ‘œ μ—¬λŸ¬ μ°¨λ‘€ λ°˜λ³΅ν•΄ λ‚΄λ €κ°€μ•Ό ν•œλ‹€λŠ” λ¬Έμ œκ°€ μžˆλ‹€.

Nested Component Project/Inject Tree

λ”°λΌμ„œ 이런 경우 Provide둜 μ œκ³΅ν•˜κ³ , Inject둜 μ£Όμž…ν•˜λ©΄ μ—¬λŸ¬ κ³„μΈ΅μœΌλ‘œ κ΅¬μ„±λœ large tree μ»΄ν¬λ„ŒνŠΈμ—μ„œλ„ DeepChild κΉŒμ§€ ν•œ λ²ˆμ— μ£Όμž…μ΄ κ°€λŠ₯μΌ€ ν•œλ‹€Prop Drilling.

단, Provideλ₯Ό 톡해 제곡된 λ°μ΄ν„°λŠ” μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ£Όμž…ν•  λ•Œ μ–΄λ–€ μƒμœ„ μ»΄ν¬λ„ŒνŠΈμ—μ„œ 온 데이터인지 μ•Œ 수 μ—†λ‹€λŠ” 단점이 μ‘΄μž¬ν•œλ‹€.

1. Provide

  • /src/views/RootView.vue
<template>
  <FirstChild />
</template>

<script>
import FirstChild from "@/components/FirstChild.vue";

export default {
  name: "RootView",
  components: {
    FirstChild,
  },
  provide() {
    return {
      rootValue: "Hello~ I'm root.",
    };
  },
};
</script>
  • /src/components/FirstChild.vue
<template>
  <SecondChild />
</template>

<script>
import SecondChild from "@/components/SecondChild.vue";

export default {
  name: "FirstChild",
  components: {
    SecondChild,
  },
};
</script>
  • /src/components/SecondChild.vue
<template>
  <ThirdChild />
</template>

<script>
import ThirdChild from "@/components/ThirdChild.vue";

export default {
  name: "SecondChild",
  components: {
    ThirdChild,
  },
};
</script>

2. App-level Provide

Provideλ₯Ό App-levelμ—μ„œ μ œκ³΅ν•˜λ©΄ λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ‚¬μš©ν•  수 μžˆλ‹€. 일반적으둜 ν”ŒλŸ¬κ·ΈμΈμ€ μ»΄ν¬λ„ŒνŠΈλ₯Ό μ΄μš©ν•΄ 값을 μ œκ³΅ν•  수 μ—†κΈ° λ•Œλ¬Έμ— ν”ŒλŸ¬κ·ΈμΈμ„ μ•± 전역에 μž‘μ„±ν•  λ•Œ μœ μš©ν•˜λ‹€.

  • /src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import mixins from "@/mixins";

createApp(App)
  .use(store)
  .use(router)
  .mixin(mixins)
  .provide("appLevelValue", "Hello~ This is App")
  .mount("#app");

3. Inject

  • /src/components/ThirdChild.vue

Provide 된 데이터λ₯Ό μ»΄ν¬λ„ŒνŠΈμ— μ£Όμž…ν•˜λŠ” 방법은 Arrayλ₯Ό μ΄μš©ν•˜λŠ” 것과 Objectλ₯Ό μ΄μš©ν•˜λŠ” 것 두 κ°€μ§€κ°€ μžˆλ‹€.

  • Arrayλ₯Ό μ΄μš©ν•΄ λ‹¨μˆœ μ£Όμž…ν•˜κΈ°
<template>
  <p>This message is come from root : {{ rootValue }}</p>
  <p>This message is come from app : {{ appLevelValue }}</p>
</template>

<script>
export default {
  name: "ThirdChild",
  inject: ["rootValue", "appLevelValue"],
};
</script>

Nested Component Project/Inject

  • Objectλ₯Ό μ΄μš©ν•΄ key-value νƒ€μž…μœΌλ‘œ μ£Όμž…ν•˜κΈ°
<template>
  <p>This message is come from root : {{ rootMessage }}</p>
  <p>This message is come from app : {{ appMessage }}</p>
</template>

<script>
export default {
  name: "ThirdChild",
  inject: {
    rootMessage: "rootValue",
    appMessage: "appLevelValue",
  },
};
</script>

Objectλ₯Ό μ΄μš©ν•΄ key-value νƒ€μž…μœΌλ‘œ μ£Όμž…ν•˜λ©΄, μƒμœ„ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ œκ³΅ν•œ Provide의 λ³€μˆ˜λͺ… λŒ€μ‹  μžμ‹ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μƒμ„±ν•œ λ³€μˆ˜λͺ…을 μ‚¬μš©ν•˜λŠ” 것이 κ°€λŠ₯ν•˜λ‹€.




Reference

  1. κ³ μŠΉμ›. Vue.js ν”„λ‘œμ νŠΈ νˆ¬μž… 일주일 μ „. λΉ„μ œμ΄νΌλΈ”λ¦­ Chapter 8, 2021.