1. v-model과 필터가 마이그레이션 핵심인 이유
Vue 2에서 Vue 3로 마이그레이션 할 때 생각보다 많이 깨지는 부분은
양방향 바인딩 → v-model과 템플릿 필터 {{ value \| something }}입니다.
특히 커스텀 컴포넌트에서 v-model을 어떻게 구현했는지,
템플릿 곳곳에 필터를 얼마나 써놨는지에 따라 마이그레이션 난이도가 달라집니다.
오늘은 Vue 2에서 Vue 3로 갈 때 v-model이 어떻게 달라졌는지,
.sync / 커스텀 v-model을 Vue 3에서 어떻게 바꾸는지,
필터가 제거되면서 어떤 식으로 대체해야 하는지,
프로젝트에서 뭐부터 검색해야 하는지 정리해보겠습니다.
2. DOM 요소의 v-model은 거의 그대로
기본 form input에서 쓰는 v-model은 거의 그대로입니다.
<!-- Vue 2 / Vue 3 둘 다 동일 -->
<input v-model="text" />
<textarea v-model="content"></textarea>
<select v-model="selected">
<option value="A">A</option>
</select>
DOM에 직접 붙을 때는 내부적으로 value / checked 같은 DOM 프로퍼티와
input/change 같은 이벤트 쌍으로 자동 확장되는 구조는 같습니다.
문제는 커스텀 컴포넌트에서의 v-model입니다.
3. 컴포넌트 v-model: Vue 2, Vue 3 차이
[Vue 2]: value + input 이벤트
Vue 2에서는 커스텀 컴포넌트에 v-model을 쓰면 아래와 같습니다.
<!-- Parent.vue -->
<Child v-model="pageTitle" />
<!-- 내부적으로는 아래와 동일 -->
<Child :value="pageTitle" @input="pageTitle = $event" />
<!-- Child.vue -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props:{
value: String
}
}
</script>
- prop 이름: value
- emit 이벤트: input
추가로, prop/event 이름을 바꾸고 싶으면 model 옵션을 썼습니다.
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
}
}
[Vue 3]: modelValue + update: modelValue로 통일
Vue 3에서는 같은 v-model이 이렇게 해석됩니다.
<!-- Parent.vue -->
<Child v-model="pageTitle" />
<!-- 내부적으로는 아래와 동일 -->
<Child
:value="pageTitle"
@update:modelValue="pageTitle = $event"
/>
<!-- Child.vue -->
<template>
<input :value="modelValue" @input="onInput" />
</template>
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const onInput = (event) => {
emit('update:modelVaule', event.target.value)
}
</script>
[차이 요약]
| 구분 | Vue 2 | Vue 3 |
| prop 이름 | value (기본) | modelValue |
| 이벤트 이름 | input (기본) | update:modelValue |
| 커스텀 모델 설정 | model 옵션 | v-model:foo + update:foo 이벤트 |
4. 다중 v-model과 .sync 대체 패턴
Vue 3에서는 v-model에 argument(인자)를 줄 수 있습니다.
<!-- Parent.vue -->
<Child v-model:title="pageTitle" />
<Child
:title="pageTitle"
@update:title="pageTitle = $event"
/>
- prop 이름: title
- 이벤트 이름: update:title
이 패턴 덕분에 여러 개의 v-model (v-model:title, v-model:content)도 가능하고
Vue 2 시절의 .sync 패턴도 대체할 수 있습니다.
[마이그레이션 포인트]
sync 또는 커스텀 model 옵션을 쓰던 곳은
v-model:propName + update:propName 패턴으로 바꾸는 걸 고려하기
5. 기존 v-model 컴포넌트 마이그레이션 예시
실제 마이그레이션에서는 기존 컴포넌트를 크게 세 가지 케이스로 나눠볼 수 있습니다.
1️⃣ value + input 그대로 쓰던 컴포넌트
[Vue 2]
<!-- Child.vue -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)"
</template>
<script>
export default {
props: {
value: String
}
}
</script>
<!-- Parent.vue -->
<Child v-model="pageTitle"/>
<!-- 내부적으로 -->
<Child
:value="pageTitle"
@input="pageTitle = $event"
/>
[Vue 3]
<!-- Child.vue (<script setup>) -->
<template>
<input :value="modelValue" @input="onInput" />
</template>
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const onInput = (event) => {
emit('update:modelVaule', event.target.value)
}
</script>
<!-- Parent.vue -->
<Child v-model="pageTitle"/>
<!-- 내부적으로 :modelValue / @update:modelValue -->
[Vue 2, 3 둘 다 잠깐 지원하고 싶을 때 (점진 전환용)]
<!-- Child.vue (양쪽 지원 버전) -->
<template>
<input v-model="currentValue" />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
// Vue 2 스타일
value: String,
// Vue 3 스타일
modelValue: String
})
const emit = defineEmits(['input', 'update:modelValue'])
const currentValue = computed({
get() {
// modelValue가 우선, 없으면 value 사용
return props.modelValue ?? props.value
},
set(val) {
emit('input', val) // Vue 2
emit('update:modelValue', val) // Vue 3
}
})
</script>
2️⃣ model 옵션을 썼던 컴포넌트(prop/event 이름 커스텀)
[Vue 2]
<!-- Child.vue -->
<template>
<input :checked="checked" @change="onChange" type="checkbox" />
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
methods: {
onChange(event) {
this.$emit('change', event.target.checked)
}
}
}
</script>
<!-- Parent.vue -->
<Checkbox v-model="isActive" />
<!-- 내부적으로 :checked / @change -->
[Vue 3]
Vue 3에서는 model 옵션 대신 v-model:propName + update:propName 패턴을 씁니다.
<!-- Parent.vue -->
<Checkbox v-model:checked="isActive" />
<!-- 내부적으로 :checked / @update:checked -->
<!-- Child.vue (<script setup>) -->
<template>
<input :checked="checked" @change="onChange" type="checkbox" />
</template>
<script setup>
const props = defineProps({
checked: Boolean
})
const emit = defineEmits(['update:checked'])
const onChange = (evnet) => {
emit('update:checked', event.target.checked)
}
</script>
[정리]
- Vue 2 model: { prop: 'checked', event: 'change' }
- Vue 3 model: checked + emit('update:checked', 값)
3️⃣ 여러 개의 v-model / .sync를 사용하던 컴포넌트
Vue 2에서는 이런 식으로 .sync와 커스텀 prop을 섞어서 쓰는 경우가 많았습니다.
[Vue 2]
<!-- Parent.vue -->
<MyDialog
:visible.sync="visible"
:title.sync="title"
/>
// MyDialog.vue
export default {
props: {
visible: Boolean,
title: String
}
}
그리고 안에서는 이런 식으로
this.$emit('update:visible', false)
this.$emit('update:title', newTitle)
또는 v-model + 추가 prop 조합으로
<MyInput v-model="form.name" :error.sync="erros.name" />
[Vue 3]
Vue 3에서는 .sync가 deprecated 되었고, v-model:propName 여러 개를 쓰는 방식으로 정리됩니다.
<!-- Parent.vue -->
<MyDialog
v-model:visible="visible"
v-model:title="title"
/>
<!-- MyDialog.vue (<script setup>) -->
<script setup>
const props = defineProps({
visible: Boolean,
title: String
})
const emit = defineEmits(['update:visible', 'update:title'])
function close() {
emit('update:visible', false)
}
function changeTitle(nextTitle) {
emit('update:title', nextTitle)
}
</script>
.sync를 그대로 쓰고 있다면 가능하면 v-model:xxx로 모두 통일하는 것을 권장합니다.
6. Vue 3에서 제거된 템플릿 필터
이제 두 번째 큰 덩어리인 필터(filter) 이야기입니다.
[Vue 2]
<!-- Vue 2 -->
<p>{{ price | currency }}</p>
<p>{{ createdAt | date('YYYY-MM-DD') }}</p>
// 전역 필터 예시
Vue.filter('currency', (value) => {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(value)
})
[Vue 3]: 템플릿 문법 자체가 삭제
Vue 3에서는 템플릿 필터 문법이 아예 제거되었습니다.
공식 마이그레이션 문서에 아래와 같이 명시되어 있어요.
In 3.x, filters are removed and no longer supported.
Instead, we recommend replacing them with method calls or computed properties.
즉, {{ value \| something }} 구문을 그대로 쓰면 컴파일 에러가 납니다.
7. 필터 대체 전략
필터는 computed, methods, 공통유틸/composable 중 하나로 바꾸면 됩니다.
computed로 대체
필터를 값 하나를 꾸준히 가공해서 쓰는 용도로 썼다면, computed가 잘 맞습니다.
<!-- 기존 -->
<p>{{ price | currency }}</p>
<!-- 변경 후 -->
<p>{{ priceInKRW }}</p>
<script setup>
import { computed } from 'vue'
const props = defineProps({ price: Number })
const priceInKRW = computed(() => {
new Intl.NumberFormat('ko-KR', {
style: 'currency',
currench: 'KRW'
}).format(props.price)
})
</script>
methods로 대체
여러 곳에서 같은 포맷 함수를 호출하고 싶다면 methods 패턴도 괜찮습니다.
<!-- 템플릿 -->
<p>{{ formatCurrency(price) }}</p>
<p>{{ formatCurrency(discountedPrice) }}</p>
<script setup>
function formatCurrency(value) {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(value)
}
</script>
공통 유틸/composable로 빼기
금액/날짜 포맷같이 프로젝트 전반에서 쓰는 포맷터는
유틸 모듈이나 composable로 빼두는 게 유지보수에 좋습니다.
// src/utils/formatters.js
export function formatCurrencyKRW(value) {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(value)
}
<script setup>
import { formatCurrencyKRW } from '@/utils/formatters'
const priceLabel = computed(() => formatCurrencyKRW(price.value))
</script>
8. 점검 포인트
v-model 관련
- 템플릿
- v-model (특히 커스텀 컴포넌트에 붙은 것들)
- .sync 사용 (:value.sync 등)
- 컴포넌트 구현
- props: { value: ... } + $emit('input', ...) 패턴
- model: { prop, event } 옵션 사용 여부
- @input, @change로 부모-자식 간 값 전달하는 커스텀 인풋
→ 이런 컴포넌트들은 modelValue + update: modelValue 패턴으로 옮길 후보입니다.
filter 관련
- 템플릿에 있는 | 기호: {{ something | filterName }}
- filters: { ... } 옵션
- Vue.filter('name', fn) 전역 필터 등록 코드
→ 이런 부분은 computed / methos / 유틸 함수로 대체해야 합니다.
실제로 어떤 팀들은 Vue 3로 올리기 전에 필터 제거/대체,
이벤트 버스 제거부터 먼저 끝내놓고 시작했다는 사례도 있습니다.
정리
- 커스텀 컴포넌트 v-model 변경
- Vue 2: value + input (또는 model 옵션)
- Vue 3: modelValue + update:modelValue / v-model:foo + update:foo
- 여러 개의 v-model, .sync 대체
- v-model:title, v-model:content 같은 인자 문법으로 해결
- .sync는 deprecated → v-model:prop 패턴으로 통합
- 필터(filter) 완전 제거
- Vue 3에서는 {{ value | filter }} 문법 자체가 사라짐
- computed, methods, 유틸 함수, composable로 대체
- 실제 작업 팁
- 우선 어디에서 v-model을 커스텀하게 구현했는지, 필터를 얼마나 썼는지부터 검색해서 리스트업하기
- 작은 컴포넌트 하나를 골라 Vue 2 스타일 → Vue 3 스타일로 직접 손으로 바꿔보면서 감 잡기
출처
'Framework > Vue' 카테고리의 다른 글
| Vue 2에서 Vue 3 마이그레이션 시 참고해야 할 체크리스트 (0) | 2025.12.01 |
|---|---|
| Vue 3 라이프사이클과 인스턴스 API 변경 정리 (0) | 2025.11.28 |
| Vue 2의 Vue.use에서 Vue 3 Global API, createApp로 마이그레이션 (0) | 2025.11.27 |
| Vue 2에서 Vue 3로: 공식 마이그레이션 가이드 구조 한 눈에 정리 (0) | 2025.11.27 |
| Vue 3 생태계 한 눈에 정리 (0) | 2025.11.27 |