폼 패턴
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가지로 구분하여 관리합니다.
| 상태 | 타입 | 용도 |
|---|---|---|
isLoading | boolean | 초기 데이터 로딩 (수정 폼 등) |
isSubmitting | boolean | 제출 진행 중 여부 |
isSuccess | boolean | 제출 성공 완료 여부 |
errors | Record<string, string> | 필드별 에러 메시지 |
typescript
const isLoading = ref(false)
const isSubmitting = ref(false)
const isSuccess = ref(false)
const errors = ref<Record<string, string>>({})isLoading과isSubmitting은 동시에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>defineModel은modelValueProps와update:modelValueEmit을 자동 생성합니다.- 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속성을 병행합니다. - 제출 성공 후 폼 상태를 초기값으로 리셋합니다.