Vue 3에서는 혼동을 줄이고 개발자가
v-model
디렉티브를 보다 유연하게 사용할 수 있도록,양방향 데이터 바인딩을 위한 API가 표준화되었다.
2.x | 3.x |
---|---|
value | modelValue |
input | update:modelValue |
model 옵션 | 전달인자 사용 |
<!-- 부모 컴포넌트 -->
<template>
<div>
<!-- 변수 inputData를 속성 value에 바인딩하고 input이벤트를 수신하여 변수 inputData를 업데이트 한다. -->
<my-input-component v-model="inputData"></my-input-component>
</div>
</template>
<script>
import MyInputComponent from "./MyInputComponent.vue";
export default {
name: "MyComponent",
components: {
MyInputComponent,
},
data() {
return {
inputData: "",
};
},
};
</script>
- 2.x에서 컴포넌트에 v-model을 사용하는 것은 value를 prop으로 전달하고 input 이벤트를 emit 하는 것과 같다.
- 즉, input 이벤트가 발생했을 때, value값이 변경되는 것과 같다.
<!-- 자식 컴포넌트 -->
<template>
<div>
<input type="text" :value="value" @input="$emit('input', $event.target.value)">
</div>
</template>
<script>
export default {
name: "MyInputComponent",
props: [
"value"
],
}
</script>
prop이나 이벤트 명을 변경하려면?
<!-- 자식 컴포넌트 -->
<template>
<div>
<input type="text" :textData="textData" @input="$emit('custom-change', $event.target.value)">
</div>
</template>
<script>
export default {
name: "MyInputComponent",
model: {
props: "textData"
event: "custom-change"
}
props: {
textData
}
}
</script>
- 만약 prop 또는 이벤트 명을 다른 이름으로 변경하려면 model 옵션의 추가가 필요하다.
props에 양방향 바인딩이 필요한 경우 (v-model외에 추가적으로 다른 props에 양방향 바인딩을 사용해야 하는 경우), 다음과 같이 코드를 작성한다. 자식 컴포넌트에서 부모 컴포넌트의 값을 수정하기에, 부모 컴포넌트에서는 어떠한 자식 컴포넌트가 값을 수정한 것인지 파악하기 어렵다. 그래서 자식 컴포넌트에서 update:propName
을 이용하여 이벤트를 부모 컴포넌트에 전달한다.
자식 컴포넌트에서 this.$emit('update:propName', newValue)
를 통해 새로운 값이 할당되었음을 부모에게 알린다.
그때 부모는 아래와 같은 코드로 해당 이벤트를 듣는다.
<!-- 변수 inputData를 속성 propName에 바인딩하고 update:PropName 이벤트를 수신하여 변수 inputData를 업데이트한다. -->
<my-input-component :propName="inputData" @update:propName="inputData = $event"/>
<!-- 축약된 방식은 아래와 같다 -->
<my-input-component :propName.sync="inputData"/>
- v-model과의 차이점
- sync를 사용할 때는 자식 컴포넌트에는 valueProp이 필요하지 않다. 대신 부모와 동일한 prop 이름을 사용한다.
- 또한, prop을 업데이트 하기위해 input 이벤트를 emit하는 것이 아니라
update:propName
을 emit한다. - 하나의 컴포넌트에 다중으로 사용이 가능하다.
<!-- 부모 컴포넌트 -->
<template>
<div>
<h1>3.x</h1>
<my-input-component v-model="inputData3"></my-input-component>
</div>
</template>
<script>
import MyInputComponent from "./MyInputComponent.vue";
export default {
name: "MyComponent",
components: {
MyInputComponent,
},
data() {
return {
inputData: "",
};
},
};
</script>
- 컴포넌트에 v-model을 사용하는 것은 modelValue prop를 전달하고 update:modelValue 이벤트를 emit 하는 것과 같다.
<!-- 자식 컴포넌트 -->
<template>
<input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script>
export default {
name: "MyInputComponent",
props: {
modelValue: {
type: String
}
},
}
</script>
prop이나 이벤트 명을 변경하려면?
<my-input-component v-model:content="inputData3" />
<!-- 축약된 방식은 아래와 같다 -->
<my-input-component :content="inputData3" @update:content="inputData3 = $event" />
- 3.x에서 모델명을 변경하려면 model 옵션 대신에 전달인자를 v-model에 전달할 수 있다. (props를 통한 부모와 자식의 양방향 통신)
- 이는
.sync
수식어를 대체하는 역할을 한다.
다중으로 v-model을 바인딩 할 수 있다.
한 컴포넌트에 다중으로 사용할 수 있는.sync
수식어를 완벽하게 대체하기 위해 v-model에도 적용되게 되었다.
이제는 단일 컴포넌트 인스턴스에 대해 다중 v-model 바인딩이 가능해졌다.
<!-- 부모 컴포넌트 -->
<template>
<div>
<h1>3.x</h1>
<my-input-component v-model="firstName" v-model="lastName" ></my-input-component>
</div>
</template>
<script>
import MyInputComponent from "./MyInputComponent.vue";
export default {
name: "MyComponent",
components: {
MyInputComponent,
},
data() {
return {
firstName: "",
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>
<script>
export default {
name: "MyInputComponent",
props: {
firstName: {
type: String
},
lastName: {
type: String
}
},
}
</script>
커스텀 수식어를 추가할 수 있다.
빌트인 수식어 | 설명 | 예시 |
---|---|---|
.lazy |
input대신 change 이벤트 이후에 동기화 할 수 있다. | <input v-model.lazy="msg"> |
.number |
사용자 입력이 자동으로 숫자로 형변환 된다. | <input v-model.number="age" type="number" /> |
.trim |
사용자 입력 중 양쪽 공백을 제거한다. | <input v-model.trim="msg" /> |
v-model은 위의 표에 정리된 것처럼 빌트인 수식어를 가지고 있다. 이제는 상황에 따라서 커스텀 수식어를 만들 수 있다.
공식문서에 예제가 있으니 참고하면 된다.
이전의 vue는 optionAPI기반으로 컴포넌트를 구성했다. optionAPI는 컴포넌트의 옵션들 (data, computed, methods, watch)로 논리에 따라 구분하여 컴포넌트를 구성하게 된다. 하지만, 컴포넌트가 커지면 커질 수록 해당 옵션들 또한 커지게된다. 기능별로 묶여있는 것이 아닌 옵션 별(논리적 구분)로 묶여있기 때문에 코드가 길어질 수록 코드 가독성이 떨어질 수 밖에 없다. 즉, 하나의 기능을 논리적인 분류에 따라 분산하여 작성하기 때문에 코드가 분산되어 작성될 수 밖에 없다.
그래서 이를 보완하기 위해 compositionAPI가 등장하게 되었다. 기능별 + 논리별로 코드를 배치하게 한다. 흩어져있는 데이터를 한 곳에 모아서 관리를 함으로 재사용성을 높일 수 있다.
새로운 setup
컴포넌트 옵션은 컴포넌트가 생성되기 전에, props
가 한번 resolved될 때 실행되며 composition API의 진입점 역할을 한다.
이곳에 논리적인 흐름에 따라서 구현하고 싶은 기능을 넣어준다.
- setup은 컴포넌트 인스턴스가 생성되지 않았기 때문에 props, context를 제외하고 컴포넌트 내부 데이터에 접근할 수 없다.
- 즉, this 접근이 불가능하다.
- setup 함수의 첫번째 전달인자는 props이고 두번째 전달 인자는 context이다.
- props는 반응성이 있다. (ES6 구조 분해 할당을 하면 반응성을 잃는다. )
- context는 3가지 컴포넌트 프로퍼티를 가지는 일반 JS 객체로 반응성이 없다.
- context.attrs
- context.slots
- context.emit
attrs
와 slots
은 컴포넌트 자체가 업데이트될 때, 항상 업데이트되는 상태 저장 객체이다. 즉, attrs와 slots에 구조분해할당을 피하고, 항상 속성을 attrs.x
와 slots.x
의 형태로 참조해야한다. 그래서 attrs
나 slots
의 변경으로 인한 사이드이펙트를 의도하려면, onUpdated
라이프사이클 훅 안에서 수행해야한다.
<script>
export default {
setup(props, {attrs, slots, emit}){
...
}
}
</script>
원시자료형 데이터를 반응형으로 만드는 함수
<template>
<div> {{ count }}</div>
<button @click="countUp"> 카운트 업!</button>
</template>
<script>
import { ref } from "vue"
export default {
setup(){
const count = ref(0);
const countUp = ()=> {
counter.value ++
}
return {
count,
countUp
};
};
};
</script>
<!--======= optionAPI =======-->
<script>
export default {
data(){
return {
count: 0
};
},
methods: {
count(){
this.count++
}
};
};
</script>
ref
: 전달인자를 받고 반응형 변수의 값에 접근하거나 변경할 수 있는value
속성을 가진 객체를 반환한다. (값에 반응형 참조를 만든다 )ref
객체는 단 하나의 속성을 가지는데, 내부 값을 가리키는.value
이다.ref
가 retrun을 통해 반환되고 템플릿에서 접근되면, 자동적으로 내부 값을 풀어냅니다. 즉, 템플릿에서.value
를 추가할 필요가 없다.
관련 함수 | 설명 |
---|---|
unref |
주어진 인자가 ref라면 내부 값을 반환하고, 아니라면 주어진 인자를 반환한다. |
toRef |
소스가 되는 reacitve 객체의 속성을 가져와 ref 를 만들 수 있다. 이 ref는 여기저기 인자로 전달할수 있으면서, 소스 객체에 대해 리액티브 연결을 유지할수 있습니다. |
toRefs |
reactive 객체를 일반 객체로 변환하여 반환하지만, 반환되는 객체의 각 속성들이 ref 로 원래의 reactive 객체 속성으로 연결된다.반응성을 잃지 않고 반환된 값을 구조 분해 할당 할 수 있다. |
isRef |
주어진 값이 ref 객체인지 확인한다. |
<!-- 2.toRef -->
<script>
import {toRef, reactive} from "vue";
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
// 연결이 유지되어있다.
state.foo++
console.log(fooRef.value) // 3
</script>
<!-- 3.toRefs -->
<script>
import {toRefs, reactive} from "vue";
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
return toRefs(state)
};
export default {
setup(props) {
const { baz } = toRefs(props);
const { foo, bar } = useFeatureX();
return {
foo,
bar
};
};
};
<script>
- props의 구조분해할당이 필요한 경우,
setup
펑션의toRefs
를 사용하여 반응성을 유지할 수 있습니다.props
는 반응성이 있다. 그러다 단순하게, ES6의 구조분해할당을 사용한다면 props의 반응성이 제거된다.
toRefs
는 소스 객체에 포함 된 속성에 대한 ref만 생성합니다. 특정 속성에 대한 참조를 만들려면 대신toRef
를 사용하면 된다.
객체 형태의 데이터를 반응형 상태로 생성하기 위해 사용하는 메서드
<template>
<div> {{ count }}</div>
</template>
<script>
import { ref, reactive } from "vue"
export default {
setup(){
const age = ref(20);
const state = {
name: "박싸피",
age
}
return {
state
};
};
};
</script>
reactive
: 객체의 반응형 복사본을 반환한다.- 반응형이 깊게 적용 된다. 모든 중첩된 속성의 변화를 감지한다.
reactive
객체의 속성에ref
를 할당하면, 자동적으로 내부 값으로 벗겨내서 사용된다.
관련 함수 | 설명 |
---|---|
readonly |
객체(반응형 또는 일반 객체) 또는 ref를 가져와서 원본에 대한 읽기전용으로 변경한다. (수정을 가하지 못하는 복사본) |
isReactive |
객체가 반응형(reactive)로 생성된 것인지 아닌지 확인한다. |
shallowReadonly |
고유한 속성을 읽기 전용으로 만들지만 중첩된 객체의 경우 수정이 가능하다. |
<!-- 1. readonly -->
<script>
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// 원본이 변이되면 복사본인 copy의 값도 변한다.
original.count++
// 복사본을 변이하려고 하면 경고와 함께 실패한다.
copy.count++ // warning: "Set operation on key 'count' failed: target is readonly."
</script>
<!-- 3. shallowReadonly -->
<script>
import { shallowReadonly } from 'vue'
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 객체의 자체적인 속성 변경이 안된다.
state.foo++
// 하지만 중첩된 객체에서는 수정이 가능하다.
isReadonly(state.nested) // false
state.nested.bar++ // 반응
</script>
Vue에서 import한
watch
펑션을 사용하여 동일한 작업을 수행할 수 있습니다.
3가지의 전달인자를 허용한다.
- 감시를 원하는 반응성 참조나 getter function
- 콜백 (
(value, oldValue, onInvalidate) => void
형태의 콜백) - 선택적인 구성 옵션 (immediate나 deep과 같은 wathchOptions)
<template>
<div> {{ count }}</div>
<button @click="countUp"> 카운트 업!</button>
</template>
<script>
import { ref, watch } from "vue"
export default {
setup(){
const count = ref(0);
const countUp = ()=> {
counter.value ++
}
watch(count, (newValue, oldValue) => {
console.log(`${oldValue}=> ${newValue}`)
})
return {
count,
countUp
};
};
};
</script>
<!--======= optionAPI =======-->
<script>
export default {
data(){
return {
count: 0
};
},
methods: {
count(){
this.count++
};
},
watch: {
count(newValue, oldValue){
console.log(`${oldValue}=> ${newValue}`);
};
};
};
</script>
<template>
<div> {{ twiceTheCount }}</div>
</template>
<script>
import { ref, computed } from "vue"
export default {
setup(){
const count = ref(0);
const twiceTheCount =
computed(()=> counter.value ++);
// 값에 접근하기 위해서는 value속성에 접근해야 한다.
console.log(twiceTheCount.value)
return {
count,
twiceTheCount
};
};
};
</script>
-
computed
함수는computed
의 첫번째 인자를 전달된 게터와 같은 콜백의 결과에 대한 읽기 전용 반응성 참조를 리턴한다. -
새로 생성된 computed 변수의 value에 접근하려면,
ref
와 마찬가지로.value
속성을 사용해야 한다.
Options API와 비교하여 Composition API 형태를 완벽하게 만들기 위해서,
setup
안에 라이프사이클 훅을 등록하는 방법도 필요하다.
<template>
<div> {{ twiceTheCount }}</div>
</template>
<script>
import { ref, onMounted } from "vue"
export default {
setup(){
const count = ref(0);
const countUp = ()=> {
counter.value ++
}
onMounted(countUp) // mounted에서 countUp함수 호출
return {
count,
countUp
};
};
};
</script>
- Composition API의 라이프사이클 훅은 Options API의 라이프사이클 훅의 이름과 동일하지만, 접두사
on
이 붙는다. 예)mounted
->onMounted
- 컴포넌트에 의해 훅이 호출될 때 실행될 콜백함수를 인자로 받는다.
📌 Composition API의 라이프 사이클 훅
Options API | setup 내부의 훅 |
---|---|
beforeCreate |
필요하지 않음* |
created |
필요하지 않음* |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
setup
은 beforeCreate
, created
라이프사이클 훅 사이에 실행되는 시점이므로, 명시적으로 정의할 필요가 없다.
Vue는 UI 및 관련 동작을 컴포넌트로 캡슐화하여 UI를 구축하도록 권장한다. 그러나, 논리적으로 컴포넌트에 속하는 템플릿의 일부라도 기술적인 관점에서 보면 Vue 앱 범위를 벗어나 DOM의 다른 곳으로 템플릿의 일부를 옮기는 게 더 바람직할 때도 있다. ex) 모달 창
처음 vue로 프로젝트를 시작하면 index.html의 <div id="app"></div>
태그에 모든 화면을 넣는다.
<html>
<body>
<div id="app">
<!-- 이곳에 vue로 만드는 모든 화면이 들어간다. -->
</div>
</body>
</html>
만약 모달창을 구현하면 현재 보이는 화면을 뚫고 최상위에 올라와야 하는데, 이는 프로젝트가 커질 수록 번거롭다. 그러나 teleport를 활용하면, 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링 할 수 있게 된다.
<template>
<button @click="toggleModal"> 모달 열기! </button>
<!-- 이 HTML을 "body" 태그로 teleport 해라!! -->
<teleport to="body">
<div v-if="modal" class="modal">
<button @click="toggleModal"> 모달 닫기! </button>
</div>
</teleport>
</template>
<script>
import {ref} from "vue";
export default {
setup(){
const modal = ref(false);
const toggleModal = ()=> {
modal.value = !modal.value;
};
return {
modal,
toggleModal
}
}
}
</script>
[모달 off] |
[모달 on] |
---|