Skip to content

컴포넌트 v-model

기본 사용법

v-model은 컴포넌트에서 양방향 바인딩을 구현하는 데 사용할 수 있습니다.

Vue 3.4부터, 이를 달성하는 권장 방법은 defineModel() 매크로를 사용하는 것입니다:

vue
<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>부모에 바인딩된 v-model 값: {{ model }}</div>
  <button @click="update">증가</button>
</template>

부모는 v-model로 값을 바인딩할 수 있습니다:

template
<!-- Parent.vue -->
<Child v-model="countModel" />

defineModel()이 반환하는 값은 ref입니다. 이 ref는 다른 ref와 마찬가지로 접근하고 변경할 수 있지만, 부모 값과 로컬 값 사이의 양방향 바인딩 역할을 합니다:

  • .value는 부모 v-model에 바인딩된 값과 동기화됩니다;
  • 자식에서 변경되면, 부모에 바인딩된 값도 업데이트됩니다.

즉, 이 ref를 네이티브 input 요소에 v-model로 바인딩할 수도 있으므로, 네이티브 input 요소를 감싸면서 동일한 v-model 사용법을 제공하는 것이 간단해집니다:

vue
<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>

플레이그라운드에서 직접 해보기

내부 동작 방식

defineModel은 편의 매크로입니다. 컴파일러는 이를 다음과 같이 확장합니다:

  • 로컬 ref의 값과 동기화되는 modelValue라는 prop;
  • 로컬 ref의 값이 변경될 때 발생하는 update:modelValue라는 이벤트.

아래는 3.4 이전에 동일한 자식 컴포넌트를 구현하는 방법입니다:

vue
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

그런 다음, 부모 컴포넌트에서 v-model="foo"는 다음과 같이 컴파일됩니다:

template
<!-- Parent.vue -->
<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

보다시피, 훨씬 더 장황합니다. 하지만 내부에서 무슨 일이 일어나는지 이해하는 데 도움이 됩니다.

defineModel이 prop을 선언하기 때문에, prop 옵션을 defineModel에 전달하여 선언할 수 있습니다:

js
// v-model을 필수로 만들기
const model = defineModel({ required: true })

// 기본값 제공
const model = defineModel({ default: 0 })

WARNING

defineModel prop에 default 값을 지정하고 부모 컴포넌트에서 이 prop에 값을 제공하지 않으면, 부모와 자식 컴포넌트 간에 동기화가 깨질 수 있습니다. 아래 예시에서, 부모의 myRef는 undefined이지만, 자식의 model은 1입니다:

자식 컴포넌트:

js
const model = defineModel({ default: 1 })

부모 컴포넌트:

js
const myRef = ref()
html
<Child v-model="myRef"></Child>

먼저, 네이티브 요소에서 v-model이 어떻게 사용되는지 다시 살펴보겠습니다:

template
<input v-model="searchText" />

내부적으로, 템플릿 컴파일러는 v-model을 더 장황한 동등 코드로 확장합니다. 따라서 위 코드는 다음과 동일합니다:

template
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

컴포넌트에서 사용될 때, v-model은 대신 다음과 같이 확장됩니다:

template
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

이것이 실제로 동작하려면, <CustomInput> 컴포넌트는 두 가지를 해야 합니다:

  1. 네이티브 <input> 요소의 value 속성을 modelValue prop에 바인딩
  2. 네이티브 input 이벤트가 발생하면, 새로운 값으로 update:modelValue 커스텀 이벤트를 emit

아래는 그 예시입니다:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

이제 v-model이 이 컴포넌트에서 완벽하게 동작합니다:

template
<CustomInput v-model="searchText" />

플레이그라운드에서 직접 해보기

이 컴포넌트 내에서 v-model을 구현하는 또 다른 방법은 getter와 setter가 모두 있는 쓰기 가능한 computed 속성을 사용하는 것입니다. get 메서드는 modelValue 속성을 반환하고, set 메서드는 해당 이벤트를 emit해야 합니다:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

v-model 인자

컴포넌트의 v-model은 인자도 받을 수 있습니다:

template
<MyComponent v-model:title="bookTitle" />

자식 컴포넌트에서는, defineModel()의 첫 번째 인자로 문자열을 전달하여 해당 인자를 지원할 수 있습니다:

vue
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>

<template>
  <input type="text" v-model="title" />
</template>

플레이그라운드에서 직접 해보기

prop 옵션도 필요하다면, 모델 이름 뒤에 전달해야 합니다:

js
const title = defineModel('title', { required: true })
3.4 이전 사용법
vue
<!-- MyComponent.vue -->
<script setup>
defineProps({
  title: {
    required: true
  }
})
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

플레이그라운드에서 직접 해보기

이 경우, 기본 modelValue prop과 update:modelValue 이벤트 대신, 자식 컴포넌트는 title prop을 기대하고, 부모 값을 업데이트하기 위해 update:title 이벤트를 emit해야 합니다:

vue
<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

플레이그라운드에서 직접 해보기

다중 v-model 바인딩

v-model 인자에서 배운 대로, 특정 prop과 이벤트를 지정하는 기능을 활용하여, 이제 하나의 컴포넌트 인스턴스에 여러 개의 v-model 바인딩을 만들 수 있습니다.

v-model은 별도의 prop과 동기화되며, 컴포넌트에 추가 옵션이 필요하지 않습니다:

template
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
vue
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

플레이그라운드에서 직접 해보기

3.4 이전 사용법
vue
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

플레이그라운드에서 직접 해보기

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

플레이그라운드에서 직접 해보기

v-model 수식어 처리

폼 입력 바인딩에 대해 배울 때, v-model에는 내장 수식어 - .trim, .number, .lazy가 있다는 것을 보았습니다. 경우에 따라, 커스텀 입력 컴포넌트의 v-model도 커스텀 수식어를 지원하길 원할 수 있습니다.

예시로, v-model 바인딩으로 전달된 문자열의 첫 글자를 대문자로 만드는 커스텀 수식어 capitalize를 만들어봅시다:

template
<MyComponent v-model.capitalize="myText" />

컴포넌트 v-model에 추가된 수식어는 자식 컴포넌트에서 defineModel() 반환값을 구조 분해 할당하여 접근할 수 있습니다:

vue
<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

수식어에 따라 값을 읽거나 쓸 때 조건부로 조정하려면, defineModel()getset 옵션을 전달할 수 있습니다. 이 두 옵션은 모델 ref의 get/set 시 값을 받아 변환된 값을 반환해야 합니다. 아래는 set 옵션을 사용해 capitalize 수식어를 구현하는 방법입니다:

vue
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

플레이그라운드에서 직접 해보기

3.4 이전 사용법
vue
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="props.modelValue" @input="emitValue" />
</template>

플레이그라운드에서 직접 해보기

컴포넌트 v-model에 추가된 수식어는 modelModifiers prop을 통해 컴포넌트에 전달됩니다. 아래 예시에서는, 기본값이 빈 객체인 modelModifiers prop을 가진 컴포넌트를 만들었습니다:

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
}
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

컴포넌트의 modelModifiers prop에는 capitalize가 포함되어 있고, 값은 true입니다. 이는 v-model.capitalize="myText" 바인딩에 의해 설정된 것입니다.

prop이 준비되었으니, 이제 modelModifiers 객체의 키를 확인하고, emit되는 값을 변경하는 핸들러를 작성할 수 있습니다. 아래 코드에서는 <input /> 요소가 input 이벤트를 발생시킬 때마다 문자열을 대문자로 만듭니다.

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

플레이그라운드에서 직접 해보기

인자가 있는 v-model의 수식어

인자와 수식어가 모두 있는 v-model 바인딩의 경우, 생성되는 prop 이름은 arg + "Modifiers"가 됩니다. 예를 들어:

template
<MyComponent v-model:title.capitalize="myText">

해당 선언은 다음과 같아야 합니다:

js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}

다음은 여러 인자와 각각 다른 수식어를 가진 다중 v-model에서 수식어를 사용하는 또 다른 예시입니다:

template
<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
3.4 이전 사용법
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])

console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>
vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>
컴포넌트 v-model has loaded