Vue 3는 TypeScript로 작성되었기 때문에, TypeScript와의 통합이 매우 자연스럽습니다. 타입 추론이 잘 되고, 컴파일 시점에 많은 에러를 잡을 수 있습니다.

면접에서 "Vue에서 TypeScript를 어떻게 활용하나요?"라고 물으면, defineProps/defineEmits의 타입 기반 선언과 제네릭 컴포넌트까지 설명할 수 있으면 좋습니다.


Props 타입 선언

VUE
<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 타입 선언

VUE
<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의 타입

VUE
<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 타입

VUE
<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 타입

TYPESCRIPT
// 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 }
}

제네릭 컴포넌트

VUE
<!-- 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>
VUE
<!-- 사용 — 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 타입

TYPESCRIPT
// 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')
VUE
<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과 함께 사용하면 최소한의 보일러플레이트로 완전한 타입 안전성을 확보할 수 있습니다.

댓글 로딩 중...