TypeScript와 Vue
Vue 3는 TypeScript로 작성되었기 때문에, TypeScript와의 통합이 매우 자연스럽습니다. 타입 추론이 잘 되고, 컴파일 시점에 많은 에러를 잡을 수 있습니다.
면접에서 "Vue에서 TypeScript를 어떻게 활용하나요?"라고 물으면, defineProps/defineEmits의 타입 기반 선언과 제네릭 컴포넌트까지 설명할 수 있으면 좋습니다.
Props 타입 선언
<script setup lang="ts">
// 타입 기반 선언 (권장)
interface Props {
title: string
count: number
items: string[]
user?: { name: string; age: number }
status: 'active' | 'inactive' | 'pending'
callback?: (id: number) => void
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [], // 배열/객체 기본값은 팩토리 함수
status: 'active',
user: () => ({ name: '', age: 0 })
})
// props.title → string
// props.user → { name: string; age: number }
</script>
Emits 타입 선언
<script setup lang="ts">
// 타입 기반 선언
const emit = defineEmits<{
'update:modelValue': [value: string]
submit: [data: { name: string; email: string }]
close: []
change: [id: number, value: string]
}>()
// 타입 체크가 동작함
emit('submit', { name: '심정훈', email: 'test@test.com' })
// emit('submit', { name: '심정훈' }) // 에러! email 누락
</script>
ref와 reactive의 타입
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
// ref — 타입 추론 또는 명시
const count = ref(0) // Ref<number>
const message = ref<string | null>(null) // Ref<string | null>
// reactive — 인터페이스 사용
interface State {
users: Array<{ id: number; name: string }>
loading: boolean
error: string | null
}
const state = reactive<State>({
users: [],
loading: false,
error: null
})
// computed — 반환 타입 자동 추론
const userCount = computed(() => state.users.length) // ComputedRef<number>
// 명시적 타입 지정도 가능
const activeUsers = computed<Array<{ id: number; name: string }>>(() =>
state.users.filter(u => u.id > 0)
)
</script>
Template Ref 타입
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM 엘리먼트 ref
const inputRef = ref<HTMLInputElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
// 컴포넌트 ref
import ChildComponent from './ChildComponent.vue'
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)
onMounted(() => {
inputRef.value?.focus()
childRef.value?.someExposedMethod()
})
</script>
<template>
<input ref="inputRef" />
<ChildComponent ref="childRef" />
</template>
Composable 타입
// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref<number>
doubleCount: ComputedRef<number>
increment: () => void
decrement: () => void
reset: () => void
}
export function useCounter(initialValue = 0): UseCounterReturn {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => { count.value++ }
const decrement = () => { count.value-- }
const reset = () => { count.value = initialValue }
return { count, doubleCount, increment, decrement, reset }
}
제네릭 컴포넌트
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: number }">
// generic 속성으로 타입 파라미터 선언 (Vue 3.3+)
defineProps<{
items: T[]
selected?: T
}>()
const emit = defineEmits<{
select: [item: T]
}>()
</script>
<template>
<div v-for="item in items" :key="item.id" @click="emit('select', item)">
<slot :item="item" />
</div>
</template>
<!-- 사용 — T가 User로 추론됨 -->
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const users: User[] = [
{ id: 1, name: '김개발', email: 'kim@test.com' }
]
const handleSelect = (user: User) => {
console.log(user.name)
}
</script>
<template>
<GenericList :items="users" @select="handleSelect">
<template #default="{ item }">
<!-- item은 User 타입으로 추론됨 -->
<span>{{ item.name }} ({{ item.email }})</span>
</template>
</GenericList>
</template>
Provide/Inject 타입
// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
export interface AuthContext {
user: Ref<{ name: string } | null>
login: (token: string) => void
logout: () => void
}
export const AuthKey: InjectionKey<AuthContext> = Symbol('auth')
<script setup lang="ts">
import { provide, ref } from 'vue'
import { AuthKey, type AuthContext } from '@/types/injection-keys'
const user = ref<{ name: string } | null>(null)
const authContext: AuthContext = {
user,
login: (token: string) => { /* ... */ },
logout: () => { user.value = null }
}
provide(AuthKey, authContext) // 타입 체크됨
</script>
면접 팁
- Vue 3에서 TypeScript를 쓸 때
script setup+ 타입 기반 선언 이 가장 간결합니다 - 제네릭 컴포넌트(
generic속성)는 Vue 3.3부터 지원되며, 재사용성 높은 컴포넌트 설계의 핵심입니다 InjectionKey를 사용한 타입 안전한 Provide/Inject는 대규모 앱에서 필수적인 패턴입니다
요약
Vue 3와 TypeScript의 통합은 defineProps/defineEmits의 타입 기반 선언, ref/reactive의 타입 추론, 제네릭 컴포넌트, InjectionKey를 통한 타입 안전한 Provide/Inject로 구성됩니다. script setup과 함께 사용하면 최소한의 보일러플레이트로 완전한 타입 안전성을 확보할 수 있습니다.
댓글 로딩 중...