v-model은 폼 입력과 컴포넌트에서 양방향 데이터 바인딩을 구현하는 Vue의 핵심 디렉티브입니다.

면접에서 "v-model이 내부적으로 어떻게 동작하나요?"라고 물으면, :value + @input의 문법 설탕(syntactic sugar) 이라고 답할 수 있어야 합니다.


기본 폼 바인딩

VUE
<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">valueinput
<textarea>valueinput
<input type="checkbox">checkedchange
<input type="radio">checkedchange
<select>valuechange
VUE
<template>
  <!-- 이 두 코드는 동일하게 동작합니다 -->
  <input v-model="text" />
  <input :value="text" @input="text = ($event.target as HTMLInputElement).value" />
</template>

v-model 수식어

VUE
<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

VUE
<!-- 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>
VUE
<!-- 부모 컴포넌트 -->
<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 이전 방식 (참고용)

VUE
<!-- 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을 바인딩할 수 있습니다.

VUE
<!-- 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>
VUE
<!-- 부모 컴포넌트 -->
<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 수식어

VUE
<!-- 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>
VUE
<!-- 사용 -->
<template>
  <CustomInput v-model.capitalize="text" />
</template>

실전 패턴 — 체크박스 커스텀 값

VUE
<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 입력 처리 (한국어/중국어/일본어)

VUE
<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과 커스텀 수식어까지 활용하면 재사용성 높은 폼 컴포넌트를 만들 수 있습니다.

댓글 로딩 중...