Dewdew logo-mobile
Uses
Tech
방명록
포트폴리오

Vue3.3에서 타입 안전한 컴포넌트 만들기 (일명 Generic Components)

Vue3.3에서 타입 안전한 컴포넌트를 만드는 방법을 배워봅시다.

vue3 vue type-safe components vue generic component generic typescript nuxt ui
dewdew

Dewdew

Jan 28, 2024

7 min read

cover

Vue Nation 2024…

Day 2에는 여러 훌륭한 세션이 있었습니다.

약간의 실망은 UI 라이브러리에 대한 소개가 기술적인 솔루션보다 많았기 때문입니다. 하지만, 저는 사용하지 않는 라이브러리들이 2024년에 많은 개발 계획을 가지고 있다는 것을 보았고, 나중에 사용하고 소개할 기회를 만들어야 한다고 생각했습니다. (Prime VueVuetify가 저의 관심을 끌었습니다!)

Day 2 이후, 저는 다음과 같은 인사이트를 얻었습니다. 이는 이 포스트 이후에 순차적으로 소개할 예정입니다!

  • 타입 안전한 컴포넌트 만들기 (Generic 타입 사용)
  • Playwright & Vitest를 사용하여 Vue3 애플리케이션 테스트
  • Vue.js 사용 시 발생하는 일반적인 오류와 방지 방법

오늘의 주제는 타입 안전한 컴포넌트 만들기 (Generic 타입 사용)입니다.


Vue 3.3에서 Generically Typed Components 지원

먼저, 이 포스트를 작성할 수 있게 해준 Abdelrahman Awad에게 감사의 말씀을 전합니다. 그는 정말 훌륭한 세션이었습니다!

Vue 3.3는 2023년 3월에 공식적으로 출시되었습니다. 그 중 하나는 Generically Typed Vue Components를 지원하는 것이었습니다.

위의 블로그에서 설명한 것처럼, <script setup> 태그에서 generic 속성을 통해 타입 매개변수를 설정할 수 있습니다. 아래와 같이 할 수 있습니다.

<script setup lang='ts' generic='T'>
defineProps<{
  items: T[].
  selectedItem: T
}>()
</script>

또한, 아래와 같이 extends를 사용하여 여러 매개변수, 제약 조건, 기본 타입, 가져온 타입을 확장할 수 있습니다.

<script setup lang='ts' generic='T extends string | number, U extends Item'>
import type { Item } from './types'

defineProps<{
  label: T,
  itemList: U[]
}>()
</script>

이를 통해, 두 개 이상의 속성이 컴포넌트에 전달되었는지 여부를 명확하게 타입 검사할 수 있습니다. (타입이 다르면, Volar plugin은 빨간색 선으로 표시합니다!)

이제 프로젝트를 만들어봅시다!

아래는 저가 직접 작성한 간단한 예제입니다. (예제에서 Nuxt UI를 사용했습니다. 나중에 이에 대한 포스트를 작성할 예정입니다!)

케이스 1: Input

// /components/A/Card.vue

<script setup lang="ts" generic="T extends string | number">

defineProps<{
  label: T
  cardType: T
}>()

const model = defineModel<T extends 'number' ? number : string>()

</script>

<template>
  <p>
    {{ label }}:
  </p>
  <div>
    <DDInput
      v-model="model"
      :type="(cardType as string)"
    />
  </div>
</template>
///page/index.vue

<script setup lang="ts">

const text = ref('')
const count = ref(0)

</script>

<template>
  <div class="w-full min-h-screen flex justify-center items-center gap-8">
    <div class="flex flex-col gap-2">
      <ACard
        v-model="text"
        label="string"
        :card-type="'string'"
      />
      <pre>
        {{ { value: text, type: typeof text } }}
      </pre>
    </div>
    <div class="flex flex-col gap-2">
      <ACard
        v-model="count"
        label="number"
        :card-type="'number'"
      />
      <pre>
        {{ { value: count, type: typeof count } }}
      </pre>
    </div>
  </div>
</template>

위의 예제는 다음과 같이 구성되어 있습니다.

  • ACard.vue 컴포넌트는 labelcardType을 속성으로 받아 defineModel을 통해 반응형 값을 허용합니다.
  • index.vue에서, 특정 타입의 값이 전달되지 않으면, 빨간색 선으로 표시되며, 타입 추론이 가능합니다. 또한, ACard 컴포넌트의 modelValueindex.vue에서 제공된 타입에 따라 필요한 타입을 추론합니다. (저는 검색했지만, Volar VSCode 플러그인이 자동으로 누락된 매개변수를 표시하는 것 같습니다. 하지만, 저는 그렇게 할 수 없고, 더 자세히 알아보아야 합니다.)

type safe! (내 VSCode에 무슨 일이 있었나…)

type safe!

type safe!

케이스 2: SelectMenu

아래 예제는 SelectMenu를 사용한 간단한 예제입니다. 이는 Generic 컴포넌트를 사용하여 컴포넌트의 타입 추론을 허용합니다.

// /components/A/List.vue

<script setup lang="ts" generic="T extends Option">
import type { Option } from '~/types/index'

const props = defineProps<{
  modelValue: T
  options: T[]
}>()

const emit = defineEmits<{
  'update:model': [value: T]
}>()

const computedModel = computed({
  get () {
    return props.modelValue
  },
  set (value: T) {
    emit('update:model', value)
  }
})

</script>

<template>
  <div>
    <DDSelectMenu
      v-model="computedModel"
      :options="options"
    >
      <template #leading>
        <span>
          {{ computedModel.id }}
        </span>
      </template>
    </DDSelectMenu>
  </div>
</template>
// /pages/select.vue

<script setup lang="ts">
import type { Option } from '~/types/index'

const options: Option[] = [
  { id: 1, label: 'banana' },
  { id: 2, label: 'orange' },
  { id: 3, label: 'apple' },
  { id: 10, label: 'strowberry' }
]

const selectedOption = ref<Option>(options[0])

</script>

<template>
  <div class="h-screen w-full flex flex-col justify-center items-center gap-8">
    <AList
      v-model="selectedOption"
      :options="options"
      @update:model="(value) => selectedOption = value"
    />
    <pre>
      {{ { value: selectedOption, type: typeof selectedOption } }}
    </pre>
  </div>
</template>
// /types/index.ts

export interface Option {
  id: string | number
  label: string
}

type safe!

modelValue에 다른 타입의 값을 전달하면, 빨간색 선으로 표시되며, 타입 추론이 가능합니다.

// /pages/select.vue

<script setup lang="ts">
import type { Option } from '~/types/index'

const options: Option[] = [
  { id: 1, label: 'banana' },
  { id: 2, label: 'orange' },
  { id: 3, label: 'apple' },
  { id: 10, label: 'strowberry' }
]

const selectedOption = ref<Option>(options[0])
const selectedSometing = ref<{id: number, value: string}>({ id: 1, value: 'banana' })

</script>

<template>
  <div class="h-screen w-full flex flex-col justify-center items-center gap-8">
    <AList
      v-model="selectedSometing"
      :options="options"
      @update:model="(value) => selectedOption = value"
    />
    <pre>
      {{ { value: selectedOption, type: typeof selectedOption } }}
    </pre>
  </div>
</template>

type safe!


이제 프로젝트를 만들어봅시다!

저는 아직 완전히 사용하지 않았지만, Generic ComponentDX 경험을 크게 향상시킬 것이라고 생각합니다. Volar plugin에서 누락된 매개변수를 추론하는 기능이 제대로 작동하지 않는 이유를 확인하는 것이 가치가 있는지 확인해야 합니다!!

이 세션에서 얻은 인사이트입니다. 문제가 있거나 개선할 부분이 있으면, 댓글을 남겨주세요! 피드백은 언제나 환영합니다!

다음에 봐요!


참고 문서

Dewdew of the Internet © 2024