본문으로 건너뛰기
Advertisement

12.1 <script setup lang="ts"> — defineProps와 defineEmits 타입

Vue 3 + TypeScript 개요

Vue 3의 Composition API와 <script setup> 문법은 TypeScript와 매우 잘 통합됩니다. lang="ts"를 지정하면 완전한 타입 안전성을 얻을 수 있습니다.

<script setup lang="ts">
import { ref, computed } from 'vue'

// 타입이 자동 추론됨
const count = ref(0) // Ref<number>
const doubled = computed(() => count.value * 2) // ComputedRef<number>
</script>

<template>
<button @click="count++">{{ count }}</button>
<p>{{ doubled }}</p>
</template>

defineProps 타입 선언

런타임 선언 방식

<script setup lang="ts">
// 런타임 props 선언 (Vue가 실제로 검사)
const props = defineProps({
name: {
type: String,
required: true,
},
age: {
type: Number,
default: 0,
},
active: Boolean,
})
</script>

타입 기반 선언 방식 (권장)

<script setup lang="ts">
// 타입만으로 Props 정의 — 더 간결하고 TypeScript 친화적
interface Props {
name: string
age?: number
active?: boolean
tags?: string[]
}

const props = defineProps<Props>()

// props.name: string
// props.age: number | undefined
</script>

withDefaults로 기본값 설정

<script setup lang="ts">
interface ButtonProps {
label: string
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
}

const props = withDefaults(defineProps<ButtonProps>(), {
variant: 'primary',
size: 'md',
disabled: false,
loading: false,
})

// props.variant: 'primary' | 'secondary' | 'danger' (기본값 'primary')
</script>

<template>
<button
:class="['btn', `btn-${props.variant}`, `btn-${props.size}`]"
:disabled="props.disabled || props.loading"
>
<span v-if="props.loading">로딩 중...</span>
<span v-else>{{ props.label }}</span>
</button>
</template>

defineEmits 타입 선언

타입 기반 emit 선언

<script setup lang="ts">
// emit 이벤트 타입 정의
const emit = defineEmits<{
click: [event: MouseEvent] // 이벤트 인수 타입
change: [value: string]
update: [id: number, data: User] // 여러 인수
'update:modelValue': [value: string] // v-model 이벤트
close: [] // 인수 없는 이벤트
}>()

function handleClick(e: MouseEvent) {
emit('click', e)
}

function handleInput(value: string) {
emit('change', value)
emit('update:modelValue', value) // v-model 지원
}
</script>

v-model 지원

<!-- 커스텀 v-model 컴포넌트 -->
<script setup lang="ts">
interface Props {
modelValue: string
}

const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()

function onInput(e: Event) {
const target = e.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>

<template>
<input :value="props.modelValue" @input="onInput" />
</template>
<!-- 부모에서 사용 -->
<template>
<CustomInput v-model="username" />
<!-- 동일: <CustomInput :modelValue="username" @update:modelValue="username = $event" /> -->
</template>

다중 v-model

<script setup lang="ts">
interface Props {
firstName: string
lastName: string
}

const props = defineProps<Props>()
const emit = defineEmits<{
'update:firstName': [value: string]
'update:lastName': [value: string]
}>()
</script>

<!-- 부모에서 사용 -->
<!-- <FullName v-model:firstName="first" v-model:lastName="last" /> -->

defineModel (Vue 3.4+)

더 간결한 v-model 지원입니다.

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

// v-model을 위한 단순화된 선언
const modelValue = defineModel<string>({ required: true })

// 명명된 v-model
const title = defineModel<string>('title')
const count = defineModel<number>('count', { default: 0 })
</script>

<template>
<input v-model="modelValue" />
<input v-model="title" />
<input type="number" v-model="count" />
</template>

defineExpose — 컴포넌트 외부 API

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

const count = ref(0)

function increment() {
count.value++
}

function reset() {
count.value = 0
}

// 외부에 노출할 API만 명시적으로 선택
defineExpose({
count,
increment,
reset,
})
</script>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 컴포넌트 인스턴스 타입 추출
type ChildInstance = InstanceType<typeof ChildComponent>

const childRef = ref<ChildInstance | null>(null)

function handleReset() {
childRef.value?.reset()
}
</script>

<template>
<ChildComponent ref="childRef" />
<button @click="handleReset">리셋</button>
</template>

고수 팁

1. Props 타입 재사용

// types/components.ts
export interface TableColumn<T> {
key: keyof T
label: string
sortable?: boolean
width?: number
}

export interface PaginationProps {
page: number
pageSize: number
total: number
}
<script setup lang="ts">
import type { TableColumn, PaginationProps } from '@/types/components'

interface Props extends PaginationProps {
columns: TableColumn<User>[]
data: User[]
}

defineProps<Props>()
</script>

2. 외부 타입 import와 defineProps

Vue 3.3+ 이전에는 외부 파일에서 import한 타입을 defineProps에서 직접 사용할 수 없었습니다. 3.3+에서는 가능합니다.

<script setup lang="ts">
// Vue 3.3+: 외부 타입 직접 사용 가능
import type { User } from '@/types/user'

defineProps<{
user: User // ✅ 외부 타입 사용 가능
}>()
</script>
Advertisement