Skip to content

폼 패턴

15.1.1. v-model 바인딩

폼 입력값은 ref를 이용한 v-model 양방향 바인딩으로 관리합니다. 폼 필드가 2개 이상인 경우 객체 ref로 상태를 묶어 관리합니다.

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

interface SignUpForm {
  name: string
  email: string
  password: string
  agreeTerms: boolean
}

const form = ref<SignUpForm>({
  name: '',
  email: '',
  password: '',
  agreeTerms: false,
})
</script>

<template>
  <form>
    <input v-model="form.name" type="text" placeholder="이름" />
    <input v-model="form.email" type="email" placeholder="이메일" />
    <input v-model="form.password" type="password" placeholder="비밀번호" />
    <label>
      <input v-model="form.agreeTerms" type="checkbox" />
      이용약관 동의
    </label>
  </form>
</template>
  • 폼 상태 타입은 interface로 명시합니다.
  • 초기값은 빈 문자열(''), false, null 등 타입에 맞는 기본값을 할당합니다.

15.1.2. 폼 상태 관리

폼의 비동기 처리 상태는 다음 4가지로 구분하여 관리합니다.

상태타입용도
isLoadingboolean초기 데이터 로딩 (수정 폼 등)
isSubmittingboolean제출 진행 중 여부
isSuccessboolean제출 성공 완료 여부
errorsRecord<string, string>필드별 에러 메시지
typescript
const isLoading = ref(false)
const isSubmitting = ref(false)
const isSuccess = ref(false)
const errors = ref<Record<string, string>>({})
  • isLoadingisSubmitting은 동시에 true가 되지 않도록 관리합니다.
  • errors는 필드명을 키로 사용하여 에러 메시지를 매핑합니다.

15.1.3. 제어 컴포넌트 패턴

Vue 3.4 이상에서는 defineModel을 사용하여 커스텀 Input 컴포넌트를 작성합니다.

vue
<script setup lang="ts">
// src/components/BaseInput.vue
const model = defineModel<string>({ required: true })

defineProps<{
  label: string
  error?: string
}>()
</script>

<template>
  <div class="base-input">
    <label>{{ label }}</label>
    <input v-model="model" />
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>

부모 컴포넌트에서는 v-model로 직접 바인딩합니다.

vue
<template>
  <BaseInput v-model="form.name" label="이름" :error="errors.name" />
  <BaseInput v-model="form.email" label="이메일" :error="errors.email" />
</template>
  • defineModelmodelValue Props와 update:modelValue Emit을 자동 생성합니다.
  • Vue 3.4 미만 환경에서는 defineProps + defineEmits 조합을 사용합니다.

15.1.4. 폼 제출 처리

폼 제출은 @submit.prevent로 기본 동작을 차단하고 비동기 함수로 처리합니다. 중복 제출을 반드시 방지해야 합니다.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { createUser } from '@/api/user'

const form = ref({ name: '', email: '' })
const isSubmitting = ref(false)
const errors = ref<Record<string, string>>({})

async function handleSubmit() {
  if (isSubmitting.value) return

  isSubmitting.value = true
  errors.value = {}

  try {
    await createUser(form.value)
    form.value = { name: '', email: '' }
  } catch (e) {
    // 에러 처리는 15.3절 참조
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.name" type="text" />
    <input v-model="form.email" type="email" />
    <button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? '처리 중...' : '등록' }}
    </button>
  </form>
</template>
  • @submit.prevent는 모든 폼에 필수로 적용합니다.
  • 중복 제출 방지는 isSubmitting 가드와 버튼 disabled 속성을 병행합니다.
  • 제출 성공 후 폼 상태를 초기값으로 리셋합니다.

TIENIPIA QUALIFIED STANDARD