1. 컴포넌트 정의 및 재사용성 (전역과 지역 등록)
Vue에서 컴포넌트는 UI를 재사용 가능한 단위로 분리하는 핵심 메커니즘입니다. 컴포넌트는 사용 범위에 따라 지역(Local) 또는 전역(Global)으로 등록할 수 있습니다. 지역 컴포넌트는 일반적으로 파스칼 케이스(PascalCase)로 명명하며, 전역 컴포넌트는 케밥 케이스(kebab-case)로 명명하는 것이 관례입니다.
// 지역 컴포넌트 정의: 등록한 앱 인스턴스 내에서만 사용 가능
const ScoreTracker = {
data() {
return {
score: 10
}
},
template: `<button @click="score += 5">Current Score: {{ score }}</button>`
}
const app = Vue.createApp({
components: { ScoreTracker },
template: `
<div>
<ScoreTracker />
</div>
`
})
// 전역 컴포넌트 정의: 앱 내 모든 곳에서 사용 가능
app.component('click-counter', {
data() {
return {
clicks: 0
}
},
template: `<button @click="clicks++">Total Clicks: {{ clicks }}</button>`
})
app.mount('#app')
2. Props 기반 데이터 전달 및 유효성 검증
2.1 데이터 전달 방식
부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때는 props를 사용합니다. 정적 값을 전달할 수도 있고, v-bind(또는 :)를 사용하여 동적으로 반응형 데이터를 바인딩할 수도 있습니다.
const app = Vue.createApp({
data() {
return { userAge: 28 }
},
template: `<UserProfile :age="userAge" />`
})
app.component('UserProfile', {
props: ['age'],
template: `<p>User Age: {{ age }}</p>`
})
app.mount('#app')
2.2 Props 유효성 검증
컴포넌트의 안정성을 높이기 위해 props의 타입, 기본값, 필수 여부 및 커스텀 검증 로직을 명시할 수 있습니다.
- 지원 타입:
String,Number,Boolean,Array,Object,Function,Symbol
app.component('UserProfile', {
props: {
age: {
type: Number,
default: 18,
required: true,
validator: (value) => {
return value >= 0 && value <= 120
}
}
},
template: `<p>User Age: {{ age }}</p>`
})
3. 단방향 데이터 흐름(One-Way Data Flow)의 원칙
3.1 객체 속성 일괄 바인딩
여러 개의 속성을 한 번에 전달해야 할 경우, v-bind에 객체를 바인딩하면 객체의 모든 키가 개별 prop으로 매핑됩니다.
const app = Vue.createApp({
data() {
return {
themeConfig: {
primaryColor: '#3498db',
fontSize: 16,
isDarkMode: false
}
}
},
template: `<ThemeApplier v-bind="themeConfig" />`
})
app.component('ThemeApplier', {
props: ['primaryColor', 'fontSize', 'isDarkMode'],
template: `<div>Color: {{ primaryColor }}, Size: {{ fontSize }}</div>`
})
app.mount('#app')
3.2 단방향 데이터 흐름과 대소문자 규칙
HTML 템플릿에서는 kebab-case로 속성을 전달하고, 자식 컴포넌트의 props에서는 camelCase로 수신합니다. 단방향 데이터 흐름의 핵심은 자식 컴포넌트가 부모로부터 전달받은 prop을 직접 수정해서는 안 된다는 것입니다. 이를 통해 컴포넌트 간의 데이터 결합도를 낮추고 상태 관리를 예측 가능하게 만듭니다. 수정이 필요하다면 data나 computed를 사용하여 지역 복사본을 만들어야 합니다.
app.component('ScoreTracker', {
props: ['initialScore'],
data() {
return {
localScore: this.initialScore
}
},
template: `<button @click="localScore += 1">Score: {{ localScore }}</button>`
})
4. Non-Props 속성과 $attrs 활용
부모 컴포넌트에서 전달한 속성 중 자식 컴포넌트의 props로 선언되지 않은 속성(Non-Props Attributes)은 기본적으로 자식 컴포넌트의 최상위 루트 DOM 요소에 자동으로 상속됩니다. 이는 클래스나 data-* 속성을 적용할 때 유용합니다.
4.1 속성 상속 차단 및 재배치
inheritAttrs: false를 사용하여 자동 상속을 막고, $attrs를 통해 특정 내부 요소에 수동으로 적용할 수 있습니다.
app.component('BaseInput', {
inheritAttrs: false,
template: `
<div class="input-wrapper">
<label>Enter Value:</label>
<input v-bind="$attrs" />
</div>
`
})
// 사용 예시: <BaseInput type="email" placeholder="Email" aria-label="Email Input" />
자식 컴포넌트가 여러 개의 루트 노드를 가질 경우, v-bind="$attrs"를 사용하여 속성을 적용할 타겟 요소를 명시적으로 지정해야 합니다. 스크립트 내에서는 this.$attrs 객체를 통해 Non-Props 속성에 접근할 수 있습니다.
5. 사용자 정의 이벤트를 통한 컴포넌트 간 통신
자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달하거나 상태를 변경해야 할 때는 this.$emit()을 사용하여 사용자 정의 이벤트를 발생시킵니다. 부모는 v-on(또는 @)을 통해 해당 이벤트를 수신합니다.
const app = Vue.createApp({
data() {
return { totalScore: 0 }
},
methods: {
handleScoreUpdate(points) {
this.totalScore += points
}
},
template: `<ScoreTracker :score="totalScore" @add-points="handleScoreUpdate" />`
})
app.component('ScoreTracker', {
props: ['score'],
methods: {
addPoints() {
this.$emit('add-points', 5)
}
},
template: `
<div>
<p>Total: {{ score }}</p>
<button @click="addPoints">Add 5 Points</button>
</div>
`
})
app.mount('#app')
6. v-model을 활용한 고급 양방향 바인딩
6.1 기본 v-model과 커스텀 이벤트
Vue 3에서 v-model은 기본적으로 modelValue라는 prop과 update:modelValue 이벤트를 사용합니다.
app.component('CustomCounter', {
props: ['modelValue'],
methods: {
increment() {
this.$emit('update:modelValue', this.modelValue + 10)
}
},
template: `<button @click="increment">Value: {{ modelValue }}</button>`
})
6.2 다중 v-model 바인딩
v-model:argument 형식을 사용하면 하나의 컴포넌트에 여러 개의 양방향 바인딩을 적용할 수 있습니다.
app.component('PriceInput', {
props: ['price'],
methods: {
applyDiscount() {
this.$emit('update:price', this.price * 0.9)
}
},
template: `<button @click="applyDiscount">Discounted: {{ price }}</button>`
})
// 사용 예시: <PriceInput v-model:price="itemPrice" />
6.3 v-model 수정자(Modifiers) 처리
modelModifiers prop을 통해 부모가 전달한 v-model 수정자를 감지하고 커스텀 로직을 적용할 수 있습니다.
app.component('TextInput', {
props: {
modelValue: String,
modelModifiers: { default: () => ({}) }
},
methods: {
handleInput(e) {
let value = e.target.value
if (this.modelModifiers.uppercase) {
value = value.toUpperCase()
}
this.$emit('update:modelValue', value)
}
},
template: `<input :value="modelValue" @input="handleInput" />`
})
7. 슬롯(Slot)과 이름 있는 슬롯을 통한 콘텐츠 주입
7.1 슬롯의 데이터 작용 범위와 기본값
슬롯 내부에서 사용되는 변수는 해당 슬롯을 정의한 부모 템플릿의 데이터 스코프를 따릅니다. 자식 컴포넌트는 <slot> 태그 사이에 fallback 콘텐츠를 배치하여 기본값을 제공할 수 있습니다.
7.2 이름 있는 슬롯과 약어
복잡한 레이아웃을 구현할 때 name 속성을 사용하여 여러 개의 슬롯을 구분할 수 있습니다. 템플릿에서는 v-slot: 또는 # 약어를 사용합니다.
const app = Vue.createApp({
template: `
<DashboardLayout>
<template #header>
<h1>Dashboard Header</h1>
</template>
<template #sidebar>
<nav>Navigation Links</nav>
</template>
</DashboardLayout>
`
})
app.component('DashboardLayout', {
template: `
<div class="layout">
<header><slot name="header" /></header>
<aside><slot name="sidebar" /></aside>
<main><slot /></main>
</div>
`
})
app.mount('#app')
8. 범위가 지정된 슬롯(Scoped Slots)
자식 컴포넌트 내부의 데이터를 부모 컴포넌트의 슬롯 템플릿에서 사용해야 할 때, <slot>에 속성을 바인딩하여 데이터를 외부로 노출할 수 있습니다.
const app = Vue.createApp({
template: `
<DataGrid>
<template #default="{ rowData }">
<div class="custom-cell">{{ rowData.name }} - {{ rowData.status }}</div>
</template>
</DataGrid>
`
})
app.component('DataGrid', {
data() {
return {
items: [
{ id: 1, name: 'Server A', status: 'Online' },
{ id: 2, name: 'Server B', status: 'Offline' }
]
}
},
template: `
<ul>
<li v-for="item in items" :key="item.id">
<slot :rowData="item"></slot>
</li>
</ul>
`
})
app.mount('#app')
9. 동적 및 비동기 컴포넌트 패턴
9.1 동적 컴포넌트와 상태 캐싱
<component :is="...">를 사용하면 데이터에 따라 렌더링할 컴포넌트를 동적으로 전환할 수 있습니다. <KeepAlive>로 감싸면 전환 시 컴포넌트 인스턴스가 파괴되지 않고 메모리에 캐싱되어 상태를 유지합니다.
const app = Vue.createApp({
data() {
return { activeTab: 'TabA' }
},
template: `
<div>
<button @click="activeTab = activeTab === 'TabA' ? 'TabB' : 'TabA'">Toggle</button>
<KeepAlive>
<component :is="activeTab" />
</KeepAlive>
</div>
`
})
app.component('TabA', { template: `<div>Content of Tab A</div>` })
app.component('TabB', { template: `<div>Content of Tab B</div>` })
app.mount('#app')
9.2 비동기 컴포넌트 로딩
defineAsyncComponent를 사용하면 컴포넌트가 실제로 필요해질 때까지 로딩을 지연시켜 초기 번들 크기를 최적화할 수 있습니다.
const HeavyChart = Vue.defineAsyncComponent(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
template: `<div>Heavy Chart Rendered</div>`
})
}, 1500)
})
})
app.component('HeavyChart', HeavyChart)
10. 핵심 디렉티브 및 인스턴스 API 심화
10.1 v-once 디렉티브
v-once가 적용된 요소는 초기 렌더링 한 번만 수행되며, 이후 반응형 데이터가 변경되어도 다시 렌더링되지 않아 성능을 최적화합니다.
app.component('StaticHeader', {
data() { return { timestamp: Date.now() } },
template: `<h1 v-once>Generated at: {{ timestamp }}</h1>`
})
10.2 ref를 통한 DOM 및 컴포넌트 인스턴스 접근
템플릿의 요소나 자식 컴포넌트에 ref 속성을 할당하면, 마운트 후 this.$refs를 통해 해당 인스턴스나 DOM 노드에 직접 접근할 수 있습니다.
app.component('SearchBar', {
mounted() {
this.$refs.searchInput.focus()
},
template: `<input ref="searchInput" type="text" placeholder="Search..." />`
})
10.3 provide와 inject를 활용한 의존성 주입
깊이 중첩된 컴포넌트 트리에서 Props Drilling을 피하기 위해 provide로 데이터를 제공하고, 하위 컴포넌트에서 inject로 이를 주입받을 수 있습니다.
const app = Vue.createApp({
data() {
return { globalTheme: 'dark' }
},
provide() {
return {
theme: this.globalTheme
}
},
template: `<DeepChild />`
})
app.component('DeepChild', {
inject: ['theme'],
template: `<div class="panel" :class="theme">Current Theme: {{ theme }}</div>`
})
app.mount('#app')