v-model은 Vue의 가장 강력한 기능 중 하나로, 양방향 데이터 바인딩을 간단하게 구현할 수 있습니다. Vue 3.4에서는 defineModel 매크로를 도입하며 기존 방식보다 훨씬 간결하고 직관적으로 v-model을 사용할 수 있게 되었습니다.
1. 기존 방식: props와 emit을 사용한 v-model 구현
Vue 3.4 이전에는 v-model을 컴포넌트에서 구현하기 위해 props와 emit을 직접 사용해야 했습니다. 다음은 부모 컴포넌트에서 값을 전달받아 자식 컴포넌트에서 값을 업데이트하는 방법의 예제입니다.
기존 방식 예제
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<div>Child Input</div>
<input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>
부모 컴포넌트에서는 다음과 같이 사용할 수 있습니다.
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
</script>
<template>
<h1>Parent: {{ count }}</h1>
<Child v-model="count" />
</template>
위 방식은 동작은 잘하지만, 코드가 다소 장황합니다. 특히, 다수의 v-model 바인딩을 사용하거나, 값을 동적으로 처리해야 할 경우 코드가 복잡해질 수 있습니다.
결과
2. 새로운 방식: defineModel을 활용한 간결한 v-model 구현
Vue 3.4부터 도입된 defineModel은 v-model의 구현을 훨씬 간단하게 만듭니다. defineModel은 내부적으로 modelValue와 update:modelValue를 처리해주므로, 복잡한 설정 없이 바로 사용할 수 있습니다.
defineModel 기본 사용법
<!-- Child.vue -->
<script setup>
const model = defineModel()
</script>
<template>
<div>Child - defineModel</div>
<input v-model="model" />
</template>
부모 컴포넌트에서의 사용법은 기존과 동일합니다.
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
</script>
<template>
<h1>Parent: {{ count }}</h1>
<Child v-model="count" />
</template>
이 방식은 코드가 더 간결할 뿐 아니라, 유지보수성이 크게 향상됩니다.
결과
3. 부모-자식-손자 관계에서 v-model 활용하기
기본적으로 v-model은 부모와 자식 간 데이터 동기화를 지원합니다. 하지만, 더 깊은 계층 구조(예: 자식의 자식 컴포넌트)에서도 v-model을 활용하여 데이터를 동기화할 수 있습니다.
3단계 계층 구조 예제
손자 컴포넌트 (GrandChild.vue)
<!-- GrandChild.vue -->
<script setup>
const model = defineModel()
</script>
<template>
<div>GrandChild</div>
<input v-model="model" />
</template>
자식 컴포넌트 (Child.vue)
<!-- Child.vue -->
<script setup>
const model = defineModel()
</script>
<template>
<div>
<h2>Child: {{ model }}</h2>
<GrandChild v-model="model" />
</div>
</template>
<script>
import GrandChild from './GrandChild.vue'
export default {
components: { GrandChild },
}
</script>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
</script>
<template>
<h1>Parent: {{ count }}</h1>
<Child v-model="count" />
</template>
동작 원리
- 부모는 count를 Child 컴포넌트로 전달하고, Child는 이를 다시 GrandChild에 전달합니다.
- GrandChild에서 입력값을 변경하면, 이 값이 즉시 Child와 부모의 count로 동기화됩니다.
- 이러한 방식으로 계층 간 데이터를 효율적으로 동기화할 수 있습니다.
결과
4. v-model과 수정자 활용하기
v-model은 .trim, .number, .lazy와 같은 수정자를 기본적으로 지원합니다. 사용자 정의 수정자도 defineModel을 통해 구현할 수 있습니다.
사용자 정의 수정자 예제
다음은 capitalize 수정자를 활용하여 입력값의 첫 글자를 대문자로 변환하는 예제입니다.
<script setup>
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
return value;
},
});
</script>
<template>
<input v-model.capitalize="model" />
</template>
결론
Vue 3.4의 defineModel은 v-model 구현을 간소화하고 코드 가독성을 크게 향상시킵니다. 또한, 깊은 계층 구조에서도 간단하게 데이터를 동기화할 수 있어 실무에서도 유용하게 활용될 수 있습니다. 앞으로 Vue 프로젝트에서 v-model을 사용할 때, 자식에서 defineModel을 통해 model을 받아 사용하면 될거 같습니다.