vue

[vue3] defineModel 알아보기

weaklion 2025. 5. 28. 18:01

vue의 가장 큰 특징 중 하나인 양방향 데이터 바인딩에서 주로 사용되는 기능 중 하나가 v-model 입니다. 

<script setup>
const model = ref('');
</script>

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

 

(아주)지난 포스팅에서 v-model의 대략적인 사용법에 대해 글을 적은 적이 있습니다. 개인적으로 vue의 가장 강력한 장점 중 하나라 생각하는데 최근에 나온 vue 3.4에서 이걸 더욱 편리하게 해주는 defineModel이 등장했습니다.

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

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

 

뭔 차이야...? 생각 할 수 있는데 좀 더 봅시다. 

InputField.vue

<script setup lang="ts">
const modelValue = defineModel<string>()
</script>

<template>
  <input
    type="text"
    v-model="modelValue"
    class="border px-2 py-1 rounded"
    placeholder="입력하세요"
  />
</template>

 

App.vue

<script setup lang="ts">
import { ref } from 'vue'
import InputField from './components/InputField.vue'

const text = ref('초기값')
</script>

<template>
  <div class="p-4">
    <InputField v-model="text" />
    <p class="mt-2 text-gray-600">입력된 값: {{ text }}</p>
  </div>
</template>

 

코드를 작성하다 보면 공통 컴포넌트를 만들고 부모가 상태값을 제어하는 경우가 많습니다. vue에선 v-model을 통해 양방향 데이터 바인딩으로 값을 구현하죠.

 

부모에서 컴포넌트에 v-model에 제어할 상태값을 넣은 후 자식에서 그 값을 defineModel로 가져와 다시 v-model로 제어합니다. 이걸 defineModel 없이 써 봅시다.

 

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: string): void
}>()
</script>

<template>
  <input
    type="text"
    :value="props.modelValue"
    @input="event => emit('update:modelValue', event.target.value)"
    class="border px-2 py-1 rounded"
    placeholder="입력하세요"
  />
</template>

 

defineModel이 없을땐 수동으로 modelValue를 명시하고 emit으로 처리해야 합니다. 타입도 직접 명시해야 하며 코드량도 당연히 많아지겠죠. 

input 하나만 해도 이정돈데, 더 많은 상태값을 다뤄야 한다면 그만큼 코드의 리팩토링이 힘들어질 수 있습니다.

 

defineModel을 썼을 때 

UserForm.vue

<script setup lang="ts">
type UserInfo = {
  name: string
  email: string
}

const userInfo = defineModel<UserInfo>('userInfo')
const agreed = defineModel<boolean>('agreed', { required : true })
</script>

<template>
  <div class="space-y-3">
    <input
      v-model="userInfo.name"
      placeholder="이름"
      class="border px-2 py-1 rounded w-full"
    />
    <input
      v-model="userInfo.email"
      placeholder="이메일"
      class="border px-2 py-1 rounded w-full"
    />
    <label class="flex items-center gap-2">
      <input type="checkbox" v-model="agreed" />
      약관에 동의합니다
    </label>
  </div>
</template>
App.vue

<script setup lang="ts">
import { ref } from 'vue'
import UserForm from './components/UserForm.vue'

const userInfo = ref({ name: '홍길동', email: 'hong@example.com' })
const agreed = ref(false)
</script>

<template>
  <UserForm v-model:userInfo="userInfo" v-model:agreed="agreed" />
  <p>입력 결과: {{ userInfo }} / 동의 여부: {{ agreed }}</p>
</template>

 

defineModel 이전

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

type UserInfo = {
  name: string
  email: string
}

const props = defineProps<{
  userInfo: UserInfo
  agreed: boolean
}>()

const emit = defineEmits<{
  (e: 'update:userInfo', value: UserInfo): void
  (e: 'update:agreed', value: boolean): void
}>()

// 반응형 객체로 만들기 위해 복사
const localUserInfo = { ...props.userInfo }

watchEffect(() => {
  emit('update:userInfo', localUserInfo)
})
</script>

<template>
  <div class="space-y-3">
    <input
      v-model="localUserInfo.name"
      placeholder="이름"
      class="border px-2 py-1 rounded w-full"
    />
    <input
      v-model="localUserInfo.email"
      placeholder="이메일"
      class="border px-2 py-1 rounded w-full"
    />
    <label class="flex items-center gap-2">
      <input
        type="checkbox"
        :checked="props.agreed"
        @change="e => emit('update:agreed', e.target.checked)"
      />
      약관에 동의합니다
    </label>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import UserForm from './components/UserForm.vue'

const userInfo = ref({ name: '홍길동', email: 'hong@example.com' })
const agreed = ref(false)
</script>

<template>
  <UserForm v-model:userInfo="userInfo" v-model:agreed="agreed" />
</template>

 

안 써도 되긴 하는데... 안 쓸 이유가 없겠죠?