v-model
v-model은 폼 입력과 컴포넌트에서 양방향 데이터 바인딩을 구현하는 Vue의 핵심 디렉티브입니다.
면접에서 "v-model이 내부적으로 어떻게 동작하나요?"라고 물으면, :value + @input의 문법 설탕(syntactic sugar) 이라고 답할 수 있어야 합니다.
기본 폼 바인딩
<script setup lang="ts">
import { ref } from 'vue'
const text = ref('')
const message = ref('')
const selected = ref('')
const checked = ref(false)
const checkedNames = ref<string[]>([])
const picked = ref('')
</script>
<template>
<!-- 텍스트 input -->
<input v-model="text" placeholder="텍스트 입력" />
<p>입력값: {{ text }}</p>
<!-- textarea -->
<textarea v-model="message" placeholder="메시지"></textarea>
<!-- 체크박스 — 단일 (boolean) -->
<input type="checkbox" v-model="checked" id="agree" />
<label for="agree">동의: {{ checked }}</label>
<!-- 체크박스 — 다중 (배열) -->
<input type="checkbox" v-model="checkedNames" value="Vue" id="vue" />
<label for="vue">Vue</label>
<input type="checkbox" v-model="checkedNames" value="React" id="react" />
<label for="react">React</label>
<p>선택: {{ checkedNames }}</p>
<!-- 라디오 -->
<input type="radio" v-model="picked" value="A" id="a" />
<label for="a">A</label>
<input type="radio" v-model="picked" value="B" id="b" />
<label for="b">B</label>
<!-- select -->
<select v-model="selected">
<option disabled value="">선택하세요</option>
<option>Vue</option>
<option>React</option>
<option>Svelte</option>
</select>
</template>
v-model의 내부 동작
v-model은 엘리먼트 타입에 따라 다른 속성과 이벤트를 사용합니다.
| 엘리먼트 | 속성 | 이벤트 |
|---|---|---|
<input type="text"> | value | input |
<textarea> | value | input |
<input type="checkbox"> | checked | change |
<input type="radio"> | checked | change |
<select> | value | change |
<template>
<!-- 이 두 코드는 동일하게 동작합니다 -->
<input v-model="text" />
<input :value="text" @input="text = ($event.target as HTMLInputElement).value" />
</template>
v-model 수식어
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('')
const age = ref(0)
const search = ref('')
</script>
<template>
<!-- .lazy — input 대신 change 이벤트에서 동기화 -->
<!-- 입력 중에는 업데이트하지 않고, 포커스를 잃을 때 반영 -->
<input v-model.lazy="message" />
<!-- .number — 입력값을 숫자로 자동 변환 -->
<input v-model.number="age" type="number" />
<!-- .trim — 앞뒤 공백 자동 제거 -->
<input v-model.trim="search" />
<!-- 수식어 조합 -->
<input v-model.lazy.trim="message" />
</template>
컴포넌트 v-model
컴포넌트에서 v-model을 사용하면 prop과 emit의 조합으로 동작합니다.
Vue 3 기본 v-model
<!-- CustomInput.vue -->
<script setup lang="ts">
// Vue 3.4+ — defineModel 매크로 (가장 간결한 방법)
const model = defineModel<string>()
</script>
<template>
<input :value="model" @input="model = ($event.target as HTMLInputElement).value" />
</template>
<!-- 부모 컴포넌트 -->
<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const username = ref('')
</script>
<template>
<CustomInput v-model="username" />
<p>입력값: {{ username }}</p>
</template>
defineModel 이전 방식 (참고용)
<!-- CustomInput.vue — defineModel 없이 -->
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
v-model은 내부적으로 :modelValue + @update:modelValue로 변환됩니다.
다중 v-model
하나의 컴포넌트에 여러 v-model을 바인딩할 수 있습니다.
<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
const email = defineModel<string>('email')
</script>
<template>
<div class="form">
<input :value="firstName" @input="firstName = ($event.target as HTMLInputElement).value" placeholder="이름" />
<input :value="lastName" @input="lastName = ($event.target as HTMLInputElement).value" placeholder="성" />
<input :value="email" @input="email = ($event.target as HTMLInputElement).value" placeholder="이메일" />
</div>
</template>
<!-- 부모 컴포넌트 -->
<script setup lang="ts">
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const first = ref('')
const last = ref('')
const email = ref('')
</script>
<template>
<UserForm
v-model:firstName="first"
v-model:lastName="last"
v-model:email="email"
/>
</template>
커스텀 v-model 수식어
<!-- CustomInput.vue -->
<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
// 수식어가 적용될 때의 변환 로직
set(value) {
// .capitalize 수식어가 있으면 첫 글자를 대문자로
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
<template>
<input :value="model" @input="model = ($event.target as HTMLInputElement).value" />
</template>
<!-- 사용 -->
<template>
<CustomInput v-model.capitalize="text" />
</template>
실전 패턴 — 체크박스 커스텀 값
<script setup lang="ts">
import { ref } from 'vue'
const status = ref('inactive')
</script>
<template>
<!-- true-value / false-value로 체크 시 값 지정 -->
<input
type="checkbox"
v-model="status"
true-value="active"
false-value="inactive"
/>
<p>상태: {{ status }}</p>
</template>
IME 입력 처리 (한국어/중국어/일본어)
<script setup lang="ts">
import { ref } from 'vue'
const koreanText = ref('')
</script>
<template>
<!-- v-model은 IME 조합 중에는 업데이트되지 않음 -->
<!-- 조합 완료 후 업데이트됨 -->
<input v-model="koreanText" placeholder="한국어 입력" />
<!-- 조합 중에도 실시간 업데이트가 필요하면 -->
<input
:value="koreanText"
@input="koreanText = ($event.target as HTMLInputElement).value"
placeholder="실시간 한국어 입력"
/>
</template>
이 차이는 한국어 검색 자동완성 같은 기능을 구현할 때 중요합니다.
면접 팁
- v-model이 문법 설탕 이라는 것을 알고 있으면, 컴포넌트에서 커스텀 v-model을 구현할 수 있습니다
- Vue 3.4의
defineModel은 코드를 크게 줄여주는 매크로입니다. 이전 방식(modelValue+update:modelValue)도 알고 있어야 합니다 - 다중 v-model은 복잡한 폼 컴포넌트에서 매우 유용한 패턴입니다
요약
v-model은 :value + @input (또는 :modelValue + @update:modelValue)의 문법 설탕입니다. .lazy, .number, .trim 수식어로 동작을 조절할 수 있고, defineModel로 컴포넌트 v-model을 간결하게 구현할 수 있습니다. 다중 v-model과 커스텀 수식어까지 활용하면 재사용성 높은 폼 컴포넌트를 만들 수 있습니다.
댓글 로딩 중...