주니어 프론트엔드 개발자로 거듭나기 위한 노력 6개월 차..! 저의 고군분투를 조금씩 얘기해볼까합니다. 그 중 첫 번째로 Vue composition API를 소개하겠습니다. 사실 소개하기엔 좋은 글이 너무 많아서, 간단하게 어떻게 Composition API로 옮겨가는 지 예시로 설명하려고 합니다.
여러 Composition API를 활용한 코드 예시를 보고싶으신 분들은 제가 개발하는 프로젝트 SpaceONE(github.com/spaceone-dev/console)을 참고해주세요~
이 글은 Vue에 대한 기본 지식이 있는 분들을 대상으로 하는 튜토리얼 글입니다!
Vue의 반응형, 기존의 라이프사이클, 렌더링 방식 등에 대한 설명을 생략합니다!
Composition API란 컴포넌트 로직을 유연하게 구성할 수 있는 API 모음으로 로직의 재사용성과 가독성을 높여줍니다.
위의 그림과 같이 기존에는 data, methods, computed에 각각 로직이 흩어져 있었는데 Composition API를 활용하면 로직이 모아지는 것을 알 수 있습니다.
Composition API의 가장 큰 특징 중 하나는 Setup입니다. 기존 Vue2의 Life Cycle과 비교하자면,
beforeCreate -> setup()
created -> setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
activated -> onActivated
deactivated -> onDeactivated
errorCaptured -> onErrorCaptured
위와 같이 beforeCreate나 created는 모두 setup() 안으로 들어가고, 나머지 life cycle은 onXXX function들로 적용할 수 있습니다.
즉, setup()이라는 일종의 훅에서 초기화를 진행합니다.
setup에서 반응형 데이터를 바인딩해주고, 기존에는 computed에 따로 명시되어 있던 computed 속성들을 정의해줄 수 있습니다.
vue-next에서는 더 나은 타입스크립트의 지원, this 바인딩의 변화, portal의 등장, fragment를 통한 불필요한 wrapper용 root element 제거 등 여러 변화가 제시되었습니다. 하지만 이 글은 vue composition API에 집중한 글이기 때문에 생략하도록 하겠습니다!
구구절절 Composition API가 이렇게 좋아요~ 라는 설명은 각설하고, 직접 한번 적용해보겠습니다.
준비물은 Vue2로 작성되어 있는 기존의 코드입니다. 글을 쓰는 김에 기존 프로젝트의 Vue2 코드를 리팩토링하기로 마음먹고 진행해보겠습니다. (css는 생략!)
(Deprecated 예정인 코드..)
<template>
<div ref="container" class="p-menu-list">
(생략)
</div>
</template>
<script>
import PMenuItem from '@/components/molecules/menu-item/MenuItem.vue';
import PTooltipButton from '@/components/organisms/buttons/tooltip-button/TooltipButton.vue';
import PButton from '@/components/atoms/buttons/Button.vue';
import {
computed, getCurrentInstance, onMounted, onUnmounted, reactive, toRefs,
} from '@vue/composition-api';
const ACTIVATOR_MENU_SPACE = -8;
export default {
name: 'PMenuList',
events: ['change', 'show', 'hide', 'select'],
components: { PMenuItem, PTooltipButton, PButton },
props: {
listItems: {
type: Array,
default: () => ([]),
validator(listItems) {
return listItems.every((listItem) => {
const keys = Object.keys(listItem);
return keys.every(key => ['key', 'contents', 'indent', 'selected'].includes(key));
});
},
},
contents: {
type: String,
default: '',
},
tooltip: {
type: String,
default: '',
},
tooltipOptions: {
type: Object,
default: () => ({ offset: '12px' }),
},
},
data() {
return {
visible: false,
selectedIdx: null,
};
},
computed: {
activatorElement() {
return this.$refs.container;
},
position() {
return `${this.activatorElement.clientWidth + ACTIVATOR_MENU_SPACE}px`;
},
},
created() {
this.setSelectedIdx();
document.addEventListener('click', this.hide, true);
document.addEventListener('click', this.hide, false);
},
destroyed() {
document.removeEventListener('click', this.hide, true);
document.removeEventListener('click', this.hide, false);
},
methods: {
setSelectedIdx() {
this.listItems.some((item, idx) => {
if (item.selected) this.selectedIdx = idx;
return item.selected;
});
},
show() {
this.visible = true;
},
hide() {
this.visible = false;
},
toggle() {
this.visible = !this.visible;
},
select(item, idx, e) {
this.hide();
if (this.selectedIdx !== null) {
this.$set(this.listItems[this.selectedIdx], 'selected', false);
this.$set(this.listItems[idx], 'selected', true);
this.selectedIdx = idx;
}
this.$emit('select', item, idx, e);
},
},
};
</script>
기존에 이렇게 일반적인 vue2로 작성된 코드가 있습니다. 이 코드를 재작성해보도록 하겠습니다.
우선, composition api를 설치해줍니다!(vue3에는 내장되어있을테지만, 아직 vue-3는 베타 단계이기 때문에 미리 써볼 땐 설치를 해줍니다. ⚙️)
npm install @vue/composition-api
# or
yarn add @vue/composition-api
그 후, 아래와 같이 코드 작성에 필요한 api들을 import합니다.
import {
computed, getCurrentInstance, onMounted, onUnmounted, reactive, toRefs,
} from '@vue/composition-api';
이제 본격적으로 마이그레이션해보겠습니다! 🚚 (제가 마이그레이션할 때 편한 주관적인 순서입니다..😅)
export default {
name: 'PMenuList',
events: ['change', 'show', 'hide', 'select'],
components: { PMenuItem, PTooltipButton, PButton },
props: {
listItems: {
type: Array,
default: () => ([]),
validator(listItems) {
return listItems.every((listItem) => {
const keys = Object.keys(listItem);
return keys.every(key => ['key', 'contents', 'indent', 'selected'].includes(key));
});
},
},
contents: {
type: String,
default: '',
},
tooltip: {
type: String,
default: '',
},
tooltipOptions: {
type: Object,
default: () => ({ offset: '12px' }),
},
},
setup(props, context) {
const vm = getCurrentInstance();
const state = reactive({
visible: false,
selectedIdx: null,
container: null,
});
const position = computed(() => `${state.container ? state.container.clientWidth + ACTIVATOR_MENU_SPACE : undefined}px`);
}
return {
...toRefs(state),
position,
};
name, components, props는 그대로 두고 기존의 data()에 있던 초기화 속성들을 setup 안으로 넣어줍니다. setup은 props, context를 매개변수로 받고 context는 attrs, slots, emit, parent, root 속성을 갖습니다. (context.root.$route, context.emit 등등으로 쓰입니다..)
state를 정의하는 방법은 ref로 할 수도 있고, reactive로 할 수도 있는데 저는 여러 개의 reactive한 값들을 정의하고 초기화해줄때는 reactive가 편리해서 reactive로 하는 편입니다.
reactive는 객체를 받지만, ref는 여러 타입을 받을 수 있고 ref는 .value로 접근해야 한다는 차이점이 있습니다.
예를 들어,
const capacity = ref(4);
const attending = ref(["Tim", "Bob", "Joe"]);
const spacesLeft = computed(() => {
return capacity.value - attending.value.length;
});
요렇게도 할 수 있고,
const event = reactive({
capacity: 4,
attending: ["Tim", "Bob", "Joe"],
spacesLeft: computed(() => { return event.capacity - event.attending.length; })
});
요렇게도 할 수 있습니다. (전 후자를 선호하고, interface를 이용합니다)
reactive로 초기화해준 이후에는 toRefs를 이용해서 destructuring을 합니다. 일반적인 destructuring을 이용하면 반응형으로 동작하지 않기 때문입니다.
기존의 methods에 명세되어 있던 함수들을 꺼내줍니다.
(생략)
setup(props, context) {
const vm = getCurrentInstance();
const state = reactive({
visible: false,
selectedIdx: null,
container: null,
});
const position = computed(() => `${state.container ? state.container.clientWidth + ACTIVATOR_MENU_SPACE : undefined}px`);
const setSelectedIdx = () => {
props.listItems.some((item, idx) => {
if (item.selected) state.selectedIdx = idx;
return item.selected;
});
};
const show = () => {
state.visible = true;
};
const hide = () => {
state.visible = false;
};
const toggle = () => {
state.visible = !state.visible;
};
const select = (item, idx, e) => {
hide();
if (state.selectedIdx !== null) {
vm.$set(props.listItems[state.selectedIdx], 'selected', false);
vm.$set(props.listItems[idx], 'selected', true);
}
vm.$emit('select', item, idx, e);
};
return {
...toRefs(state),
setSelectedIdx,
show,
hide,
toggle,
select,
position,
};
기존 Vue2에서는 props든 data든 상관 없이 모두 this에 바인딩 되었는데, Composition API를 활용하면 위와 같이 state의 속성은 state에, props의 속성은 props에 바인딩되서 보다 명시적인 것을 알 수 있습니다.
methods를 바꿀 때 가장 중요한 것은 꼭 return을 해주기!입니다.
vm = getCurrentInstance()는 composition api에서 추가된 함수인데, 현재 vue instance를 참조할 수 있도록 해줍니다.
(생략)
onMounted(() => {
setSelectedIdx();
document.addEventListener('click', hide, true);
document.addEventListener('click', hide, false);
});
onUnmounted(() => {
document.removeEventListener('click', hide, true);
document.removeEventListener('click', hide, false);
});
return {
...toRefs(state),
setSelectedIdx,
show,
hide,
toggle,
select,
position,
};
앞서 설명에서 말했듯이 created, destroyed라는 라이프 사이클이 onMounted, onUnmounted로 바뀌었기 때문에 해당 사항을 적용해줍니다!
이제 마이그레이션이 끝났습니다! 그런데 마이그레이션을 하는 도중 한 가지 의문이 생기지 않으셨나요?
바로
computed: {
activatorElement() {
return this.$refs.container;
},
position() {
return `${this.activatorElement.clientWidth + ACTIVATOR_MENU_SPACE}px`;
},
}
요 코드의 activatorElement가 어디로 사라진건데?! 라는 의문입니다.
사실 저 activatorElement는 그저 html의 <div ref="container">
라는 DOM을 참조하기 위해서 사용되었던 코드입니다.. 😅
그런데 setup 내의 state에서 초기화를 해주면 해당 DOM에 접근할 수 있기 때문에 해당 코드가 삭제되었습니다. 대신 state내에 container: null,
이라는 속성이 추가되고 position에서 state.container
로 접근할 수 있습니다! 🤓
다소 장황한 설명이었는데 잘 따라오셨나요~? 기존에 가지고 계신 Vue2 코드에 적용하신다면 보다 편하게 적용할 수 있을거라고 생각합니다!
제가 느끼기에 Composition API는
- 로직을 모아줌으로써 가독성 향상
- 죄다 this에 때려박는 바인딩에서의 탈피 ✨ (props가 props라고 왜 말을 못하고 다 this였니..)
- this.$refs에서 벗어난 DOM 접근
- 이 글엔 명시하진 않았지만 Typescript 적용에 용이
이런 장점들을 가지고 있다고 생각합니다! 다들 Composition API와 함께 Vue-next로 가요~ 🚀
[API Reference] https://composition-api.vuejs.org/api.html#setup
[Vue 3에 도입될 composition api 간단 리뷰] https://velog.io/@ashnamuh/Vue-3%EC%97%90-%EB%8F%84%EC%9E%85%EB%90%A0-composition-api-%EA%B0%84%EB%8B%A8-%EB%A6%AC%EB%B7%B0
[VUE 3 COMPOSITION API: REF VS REACTIVE] https://www.danvega.dev/blog/2020/02/12/vue3-ref-vs-reactive/
[VUE 3 COMPOSITION API
CHEAT SHEET] https://www.vuemastery.com/pdf/Vue-3-Cheat-Sheet.pdf
(이거 꼭 보세요!!)
[[Vue.js] Composition API 살펴보기] https://geundung.dev/102
[How to use the new composition API in Vue] https://medium.com/javascript-in-plain-english/how-to-use-composition-api-in-vue-967fc9b8393c