컴포넌트 기초를 넘어서면 Provide/Inject, 동적 컴포넌트, 비동기 로딩 같은 고급 패턴이 필요합니다. 이 패턴들은 대규모 앱 설계에 필수적입니다.

면접에서 "Props Drilling 문제를 어떻게 해결하나요?"라는 질문에 Provide/Inject를 설명할 수 있으면, 컴포넌트 설계에 대한 이해도를 보여줄 수 있습니다.


Provide / Inject — Props Drilling 해결

깊이 중첩된 컴포넌트에 데이터를 전달할 때, 중간 컴포넌트들이 불필요하게 Props를 전달하는 문제(Props Drilling)를 해결합니다.

VUE
<!-- 조상 컴포넌트 -->
<script setup lang="ts">
import { provide, ref, readonly } from 'vue'

const theme = ref<'light' | 'dark'>('light')
const user = ref({ name: '심정훈', role: 'developer' })

// 반응형 값 provide
provide('theme', readonly(theme))    // readonly로 자식의 변경 방지
provide('user', readonly(user))

// 값 변경 함수도 함께 provide
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
</script>
VUE
<!-- 자손 컴포넌트 (깊이 상관없이 사용 가능) -->
<script setup lang="ts">
import { inject } from 'vue'
import type { Ref } from 'vue'

// 타입과 기본값 지정
const theme = inject<Ref<'light' | 'dark'>>('theme')
const toggleTheme = inject<() => void>('toggleTheme')

// 기본값 제공 — provide가 없을 때 사용
const user = inject('user', { name: '게스트', role: 'visitor' })
</script>

<template>
  <div :class="theme">
    <p>현재 테마: {{ theme }}</p>
    <button @click="toggleTheme?.()">테마 전환</button>
  </div>
</template>

Symbol을 key로 사용하기 (권장)

TYPESCRIPT
// injection-keys.ts
import type { InjectionKey, Ref } from 'vue'

export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')
export const ToggleThemeKey: InjectionKey<() => void> = Symbol('toggleTheme')
VUE
<script setup lang="ts">
import { provide, ref } from 'vue'
import { ThemeKey, ToggleThemeKey } from './injection-keys'

const theme = ref<'light' | 'dark'>('light')
provide(ThemeKey, theme)  // 타입 자동 추론
</script>
VUE
<script setup lang="ts">
import { inject } from 'vue'
import { ThemeKey } from './injection-keys'

const theme = inject(ThemeKey)  // Ref<'light' | 'dark'> | undefined 타입
</script>

동적 컴포넌트

<component :is> 를 사용하면 런타임에 컴포넌트를 전환할 수 있습니다.

VUE
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import TabHome from './TabHome.vue'
import TabProfile from './TabProfile.vue'
import TabSettings from './TabSettings.vue'

// shallowRef — 컴포넌트 객체는 깊은 반응형이 불필요
const currentTab = shallowRef(TabHome)

const tabs = [
  { label: '홈', component: TabHome },
  { label: '프로필', component: TabProfile },
  { label: '설정', component: TabSettings }
]
</script>

<template>
  <div class="tabs">
    <button
      v-for="tab in tabs"
      :key="tab.label"
      :class="{ active: currentTab === tab.component }"
      @click="currentTab = tab.component"
    >
      {{ tab.label }}
    </button>
  </div>

  <!-- 동적 컴포넌트 렌더링 -->
  <component :is="currentTab" />
</template>

비동기 컴포넌트 (Lazy Loading)

VUE
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// 기본 사용 — 필요할 때 로드
const AsyncModal = defineAsyncComponent(() =>
  import('./HeavyModal.vue')
)

// 고급 옵션 — 로딩/에러 상태 처리
const AsyncChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: () => import('./LoadingSpinner.vue'),
  errorComponent: () => import('./ErrorDisplay.vue'),
  delay: 200,        // 로딩 컴포넌트 표시 전 대기 (ms)
  timeout: 10000     // 타임아웃 (ms)
})
</script>

<template>
  <AsyncModal v-if="showModal" @close="showModal = false" />
  <AsyncChart :data="chartData" />
</template>

비동기 컴포넌트는 번들 분할(Code Splitting)을 자동으로 처리합니다. Vite/Webpack이 별도 청크로 분리하여 초기 로딩을 줄여줍니다.


Fallthrough Attributes

컴포넌트에 전달된 속성 중 props나 emits로 선언되지 않은 것들은 자동으로 루트 엘리먼트에 전달됩니다.

VUE
<!-- CustomInput.vue -->
<script setup lang="ts">
defineProps<{
  label: string
}>()

// inheritAttrs를 false로 설정하면 자동 전달 비활성화
defineOptions({
  inheritAttrs: false
})
</script>

<template>
  <div class="input-group">
    <label>{{ label }}</label>
    <!-- $attrs로 수동 전달 — 원하는 엘리먼트에 적용 -->
    <input v-bind="$attrs" />
  </div>
</template>
VUE
<!-- 사용 -->
<template>
  <!-- class, placeholder, @input 등이 $attrs를 통해 input에 전달됨 -->
  <CustomInput
    label="이름"
    class="custom-class"
    placeholder="이름 입력"
    @input="handleInput"
  />
</template>

useAttrs로 접근

VUE
<script setup lang="ts">
import { useAttrs } from 'vue'

const attrs = useAttrs()
console.log(attrs.class)    // 전달된 class
console.log(attrs.style)    // 전달된 style
</script>

재귀 컴포넌트

컴포넌트가 자기 자신을 참조하는 패턴입니다. 트리 구조를 렌더링할 때 유용합니다.

VUE
<!-- TreeItem.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface TreeNode {
  id: number
  label: string
  children?: TreeNode[]
}

const props = defineProps<{
  node: TreeNode
  depth?: number
}>()

const isOpen = ref(true)
const hasChildren = props.node.children && props.node.children.length > 0
</script>

<template>
  <div :style="{ paddingLeft: `${(depth ?? 0) * 20}px` }">
    <span @click="isOpen = !isOpen" style="cursor: pointer">
      {{ hasChildren ? (isOpen ? '📂' : '📁') : '📄' }}
      {{ node.label }}
    </span>

    <!-- 재귀 — 자기 자신을 참조 -->
    <template v-if="isOpen && hasChildren">
      <TreeItem
        v-for="child in node.children"
        :key="child.id"
        :node="child"
        :depth="(depth ?? 0) + 1"
      />
    </template>
  </div>
</template>

컴포넌트 expose

script setup에서는 기본적으로 내부 상태가 외부에 노출되지 않습니다. defineExpose로 명시적으로 공개할 수 있습니다.

VUE
<!-- ChildForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const formData = ref({ name: '', email: '' })

const validate = (): boolean => {
  return formData.value.name.length > 0
}

const reset = () => {
  formData.value = { name: '', email: '' }
}

// 외부에 공개할 속성/메서드 선택
defineExpose({ validate, reset })
</script>
VUE
<!-- 부모 컴포넌트 -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildForm from './ChildForm.vue'

const formRef = ref<InstanceType<typeof ChildForm>>()

const handleSubmit = () => {
  if (formRef.value?.validate()) {
    console.log('유효함')
  }
}
</script>

<template>
  <ChildForm ref="formRef" />
  <button @click="handleSubmit">제출</button>
  <button @click="formRef?.reset()">초기화</button>
</template>

면접 팁

  • Provide/Inject는 DI(의존성 주입) 패턴 의 프론트엔드 버전입니다. Spring의 DI와 비교해서 설명하면 백엔드 면접관에게 좋은 인상
  • defineAsyncComponent초기 번들 크기 최적화 의 핵심 도구입니다. 성능 최적화 질문에서 활용하세요
  • defineExpose캡슐화 원칙 — 필요한 것만 공개하는 것이 좋은 컴포넌트 설계입니다

요약

Provide/Inject는 Props Drilling 문제를 해결하고, 동적 컴포넌트는 <component :is>로 런타임 전환을 가능하게 합니다. 비동기 컴포넌트는 코드 분할로 초기 로딩을 최적화하며, Fallthrough Attributes와 defineExpose는 컴포넌트 인터페이스를 세밀하게 제어합니다.

댓글 로딩 중...