Vue3 에서 달라진 점
https://v3.ko.vuejs.org/guide/migration/introduction.html#주목할-만한-새로운-기능들
https://composition-api.nuxtjs.org/
Composition API 탄생 배경
1. Options API
data
,methods
,computed
에 각각 로직이 분산되어 추적이 어렵다.- 하나의 데이터가 어떻게 변화하고 사용되는지 그룹핑하기 어렵다.
mixin
을 사용하면 되지만 다중mixin
을 상속하게 되면 오버라이딩 문제나 component 관리가 어려워진다.- 즉, 유지보수가 어렵다 어려워 :(
2. Composition API 의 setup
함수에 데이터가 그룹핑 되어 보다 용이하게 데이터의 흐름을 파악하고 로직의 재사용성과 가독성을 높여준다.
Composition API 예시
- 기존 방식
<template>
<div>
<h1>Count: {{ count }}</h1>
<h1>Double: {{ double }}</h1>
<button @click="increase">increase</button>
<button @click="decrease">decrease</button>
</div>
</template>
<script>
export default {
data () {
return {
count: 0,
}
},
computed: {
double () {
return this.count * 2;
}
},
methods: {
increase () {
++this.count;
},
decrease () {
--this.count;
}
}
}
</script>
딱히 별 기능 없이 간단한 카운트 기능을 하는 Component 에 비슷한 기능을 가진 로직들이 data
, methods
, computed
로 각각 분리되어 있다.
- Component API 방식
<template>
<div>
<h1>Count: {{ count }}</h1>
<h1>Double: {{ double }}</h1>
<button @click="increase">increase</button>
<button @click="decrease">decrease</button>
</div>
</template>
<script>
import { reactive, computed } from '@vue/composition-api';
const useCount = () => {
const count = ref(0);
const double = computed(() => count.value * 2);
const increase = () => ++count.value;
const decrease = () => --count.value;
return { count, double, increase, decrease }
}
export default {
setup () {
const { count, double, increase, decrease } = useCount();
return {
count,
double,
increase,
decrease
}
}
}
</script>
관련된 것들을 한 부분에서 해결하고 있기 때문에 추후 Component 를 나눌 때도 편해지고 가독성도 좋아진다.
Setup
- 새로 추가된 라이프 사이클 훅으로써 Composition API 를 사용하기 위한 초기화 지점이다.
- 기존의
beforCreate
훅 이전에setup
이 호출되며, Composition API 를 사용하는 경우beaforCreate
,created
hook을setup
훅으로 대체하여 사용된다. - 따라서 다음과 같이 기존 라이프 사이클에도 변화가 생긴다. (기존의 라이프 사이클을 사용할 땐 앞에
on
을 붙여서 사용해야 된다.)
import { onMounted, onUpdated, onUnmounted } from '@vue/composition-api'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
}
Data 선언
ref
: 원시값(ex. 1, 'abc', false), 객체 선언 시 사용하며 모두 반응형을 가진다.reactive
: 원시값에 대해서는 반응형을 가지지 않고 객체나 배열에서만 깊은 감지를 수행한다.
<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const count = ref(0);
const object = reactive({ foo: 'bar' });
console.log(count.value); // 0
return {
count,
object
}
}
}
</script>
예제에서 보다 시피 ref
로 선언한 값은 반드시 .value
를 통해 접근해야 실제 값을 받아올 수 있다. 그러나 template 에서는 접근하려 할 경우는 생략 가능하다.
ref
와reactive
둘다 객체 선언이 가능하면ref
만 사용하면 되는게 아닐까?ref
는 원본 값을value
라는 속성에 담아두고 변경을 감시하는 객체이고,reactive
는 원본 객체 자체에 변경을 감지하는 옵저버를 추가하여 그대로 반환하기 때문에 각 특성에 따라 사용하면 된다.
ref
로만 구성했을 때 예시
setup(props, context) {
const inputText = ref('');
const isFocused = ref(false);
const onChange = (): void => {
context.emit('on-change', inputText.value);
}
const onFocus = (): void => {
isFocused.value = true; // value에 할당
}
const onBlur = (): void => {
isFocused.value = false; // value에 할당
}
onBeforeMount(() => {
inputText.value = props.value; // value에 할당
})
return {
inputText,
isFocused,
onChange,
onFocus,
onBlur
}
}
reactive
로 구성했을 때 예시 ( +toRefs
)
setup(props, context) {
const state = reactive({
inputText: '',
isFocused: false
});
const onChange = (): void => {
context.emit('on-change', state.inputText);
}
const onFocus = (): void => {
state.isFocused = true; // state의 property를 통해 값을 할당한다.
}
const onBlur = (): void => {
state.isFocused = false; // state의 property에 값을 할당한다.
}
onBeforeMount(() => {
state.inputText = props.value; // state의 property에 값을 할당한다.
})
return {
...toRefs(state), // toRefs를 사용하여 ref로 변환한다.
onChange,
onFocus,
onBlur
}
}
이처럼 위와 같이 reactive
로 객체 값을 생성한 뒤 각 속성을 분해해서 재할당 할 경우 반응형으로 동작하지 않기 때문에 반환 시 toRefs
를 사용해 문제를 해결할 수 있다.
ifRef
...
const printValue = obj => {
console.log(isRef(obj) ? obj.vlaue : obj); //ref 이면 .value 붙여서 출력, 아닐 경우 obj만으로 출력
}
...
Component 선언
computed(fn)
처럼 감싸서 값 선언을 해준다.
<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const count = ref(0);
const plusOne = computed(() => count.value + 1);
console.log(plusOne.value); // 1
return {
count,
object
}
}
}
</script>
get
,set
또는 추가 설정 가능하다.
<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const count = ref(0);
const plusOne = computed({
get: () => count.value + 1,
set: val => { count.value = val - 1 }
});
console.log(plusOne.value); // 1
return {
count,
object
}
}
}
</script>
Props
- 상위에서 어떤
props
를 받을 것인지 알려준 후,setup
에서props.xxx
로 접근한다.
// Parent Component
<template>
<dlv class="home">
<PostList :posts="posts" />
</div>
</template>
<script>
import PostList from '../components/PostList.vue'
import { ref } from 'vue';
export default {
name: 'Home',
components: { PostList },
setup() {
const posts = ref([
{ title: '1번 타이틀', body: '1번 제목', id: 1 },
{ title: '2번 타이틀', body: '2번 제목', id: 2 },
]);
return { posts }
}
}
</script>
// Child Component
<template>
<div>
{{ post.title }}
{{ post.body }}
</div>
</template>
<script>
export default {
props: ["posts"], // 사용할 props를 배열내에 정의합니다.
setup(props) {
console.log(props.posts); // 받은 prop 사용가능
}
};
</script>
- 이로써 죄다
this
에 때려박는 바인딩에서 탈피할 수 있다 (props
는 props!this
는 this!)
그 외
- TemplateRef
- 내에서
와 같이 TemplateRef를 사용할 때는 항상 ref로 정의한 state를 사용해야 한다. reactive로 정의한 state는 정상적으로 참조되지 않는다.
- 내에서
- Composition API 에서 fetch 관련 라이프 사이클이 없다.
- 따라서 middleware 로 사용할 것을 권장한다.
- …etc