v-bind와 v-on을 조합하면 사용자 입력에 따라 JavaScript의 값이 실시간으로 업데이트되도록 만들 수 있습니다. 아래 코드는 사용자가 input에 입력한 값을 inputText1 변수에 반영합니다.
<template>
<div>
<p>{{ inputText1 }}</p>
<input :value="inputText1" @input="onInput" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const inputText1 = ref('')
const onInput = function (event) {
inputText1.value = event.target.value
}
</script>
이처럼 v-bind는 input 요소의 값을 화면에 반영하고, v-on은 사용자의 입력 이벤트를 감지해 데이터에 반영합니다.
즉, 양방향 바인딩을 수동으로 구현하고 있는 셈입니다.
Vue의 v-model은 위 코드에서 사용한 v-bind와 v-on의 조합을 한 줄로 축약한 디렉티브입니다.
같은 기능을 아래처럼 훨씬 간단하게 작성할 수 있습니다.
<template>
<div>
<p>{{ inputText2 }}</p>
<input v-model="inputText2" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const inputText2 = ref('')
</script>
주의할 점: 모든 태그나 이벤트에 v-model을 쓸 수 있는 건 아닙니다.
v-model은 기본적으로 <input>, <textarea>, <select> 같은 폼 요소에 최적화되어 있으며, 내부적으로 value 속성과 input 이벤트를 통해 동작합니다.
따라서, <button>처럼 값 입력이 없는 요소에는 v-model이 의미 없습니다.
제가 공부하며 가장 놀란 점은, v-model이 단순 input 뿐만 아니라 checkbox, radio, select 등의 요소까지도 자동으로 적절하게 처리해준다는 것입니다.
예를 들어, 다음과 같은 코드를 봅시다.
<template>
<div>
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
</div>
</template>
<script setup>
import { ref } from 'vue'
const checked = ref(false)
</script>
v-model은 checkbox 타입의 <input> 요소에 대해 자동으로 true/false 값을 바인딩해줍니다.
사용자가 체크박스를 클릭하면 checked 변수는 true 또는 false로 즉시 반응하며 변경됩니다.
별도의 이벤트 핸들러나 event.target.checked 같은 코드를 작성할 필요가 없습니다.

다중 checkbox도 당연히 적절하게 처리해줍니다.
<template>
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="alice" value="Alice" v-model="checkedNames" />
<label for="alice">Alice</label>
<input type="checkbox" id="bella" value="Bella" v-model="checkedNames" />
<label for="bella">Bella</label>
</template>
<script setup>
import { ref } from 'vue'
const checkedNames = ref([])
</script>

또, v-model은 select 태그도 적절하게 잘 처리해줍니다.
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>Alice</option>
<option>Bella</option>
<option>Cathy</option>
</select>
<script setup>
import { ref } from 'vue'
const selected = ref('')
</script>

v-model은 기본적으로 input 이벤트와 value를 자동으로 연결해주지만,
사용자 입력을 다룰 때 조금 더 정교한 동작이 필요할 때는 modifier를 활용할 수 있습니다.
Vue에서 제공하는 주요 v-model modifier는 다음과 같습니다.
| Modifier | 설명 |
|---|---|
.lazy | input 이벤트가 아니라 change 이벤트에서 동기화 |
.number | 입력 값을 자동으로 숫자로 변환 |
.trim | 앞뒤 공백 제거 후 값 반영 |
.lazy - 사용자가 입력을 마치고 나서 반영<template>
<input type="text" v-model="inputText1" />
<div>
{{ inputText1 }}
</div>
<input type="text" v-model.lazy="inputText2">
<div>
{{ inputText2 }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const inputText1 = ref('');
const inputText2 = ref('');
</script>
기본적으로는 입력할 때마다 v-model이 반응하지만,
.lazy를 사용하면 포커스를 잃거나 Enter 등의 키를 눌러야 값이 반영됩니다.

.number - 자동 숫자 변환사용자 입력이 자동으로 숫자로 유형 변환되도록 하려면, v-model 수식어로 .number를 추가하면 됩니다:
<template>
<input type="text" v-model="inputText1" />
<div>
{{ inputText1 + 1}}
</div>
<input type="text" v-model.number="inputText2">
<div>
{{ inputText2 + 1}}
</div>
<input type="number" v-model="inputText3">
<div>
{{ inputText3 + 1}}
</div>
</template>
<script setup>
import { ref } from 'vue'
const inputText1 = ref('');
const inputText2 = ref('');
const inputText3 = ref('');
</script>

주의
값을 parseFloat()로 파싱할 수 없으면 원래 값이 대신 사용됩니다.
인풋에 type="number"가 있으면 .number 수식어가 자동으로 적용됩니다.
.trim - 입력값 양끝 공백 제거사용자 입력의 공백이 자동으로 트리밍되도록 하려면 v-model 수식어로 .trim을 추가하면 됩니다:
<template>
<input v-model.trim="username" />
[{{ username }}]
</template>
<script setup>
import { ref } from 'vue'
const username = ref("");
</script>

참고자료 Vue.js 공식문서