Vue 컴포넌트는 어떻게 작성하는게 현시점 de facto일까?
Options API
Vue 2 에서는 아래와 같이 작성하는 Options API가 주류였다.
<script>
export default {
props: {
// 부모로부터 넘겨받는 값들
},
methods: {
// 메소드들
},
computed: {
// 계산된 값들
},
data: () => {
// 데이터
},
mounted() {
},
}
</script>
여기에는 다음과 같은 단점들이 있었다.
-
컴포넌트 내에 특정 비즈니스 로직을 구현하는 경우, 이 로직들이 각각
methods,computed,data등 각 속성별로 분리되어, 덩어리째 재사용이 어려움.
이 그림은 Options API의 단점을 명확하게 보여준다.
-
this의 사용. 나는 코딩을 할 때 this나 self 등의 개념을 사용하지 않는 것이 좋다고 생각한다. Python, Java, Ruby 등 이것이 혼란을 줄 여지가 없는 언어들의 경우는 모르겠으나, JS의 경우는 그것이 호출되는 컨텍스트에 따라 가리키는 대상이 바뀔 수 있기 때문에 (e.g. 화살표 함수는 컨텍스트를 만들지 않음) 객체 내에서 자기 자신을 인용할지라도 인스턴스의 이름을 항상 적는 것이 좋다고 생각한다. 이것은 추후 코드를 리팩토링하며 코드를 이리저리 옮기는 과정에서 this가 무엇을 가리키는지 신경 쓸 필요가 없도록 만들어준다. 또, computed같은 경우 정의할 때는 함수로 정의하나 호출할 때는 그냥this.someVal과 같이 '값'을 호출하는 방식으로 사용하게 되니, 사실 초심자 입장에서는 혼란스럽다.
Options API는 Composition API가 등장하기 전의 과도기적 형태였다고 생각한다.
Composition API
Vue 3부터는 Composition API가 공식적으로 권장사항이 되었다.
<script>
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const myData = ref()
const myComputedVal = computed(() => { ... })
const init = () => {}
onMounted(init)
},
}
Composition API는 이처럼 'vue' 패키지에서 반응형 변수를 선언할 수 있도록 해주는 ref, computed나 onMounted와 같은 lifecycle hook을 import하여 setup() 안에 구현하는 방식이다.
이것의 가장 큰 장점은 ref, computed, onMounted, watch 등의 기능들이 vue 패키지를 통해 제공되기 때문에, .vue 파일 뿐 아니라 일반적인 .js 파일들에서도 사용할 수 있게 되었다는 것이다! 이처럼 재사용 가능한 로직들의 구현을 .vue 파일 외부로 빼낼 수 있게 되었기 때문에, .vue 파일들은 좀 더 <template>과 <style> 등 눈에 보이는 부분에 집중하고, 로직들은 외부로 묶는 형태의 프로젝트 구성이 용이해졌다.
누가봐도 리액트의 훅에서 영감을 받은 것이 명확해보이는 이 스펙은 Vue에서는 composable이라 불린다.
// my-hook.js
import { computed } from 'vue'
export const useMyHook = () => {
const todayISO8601 = computed(() =>
(new Date()).toISOString().split('T')[0]
)
return {
todayISO8601,
}
}
// MyComponent.vue
import useMyHook from '@/hooks/my-hook'
<template>
<div>{{ todayISO8601 }}</div>
</template>
<script>
export default {
setup() {
const { todayISO8601 } = useMyHook()
return {
todayISO8601,
}
},
}
</script>
상태관리나 생명주기 등과 무관한 단순 기능에 해당하는 함수들은 보통 helper라고 구분하면 되겠고, ref, computed 또는 lifecycle 함수나 상태 등이 엮이는 함수들은 composable(= hook)로 구분할 수 있다.
<script setup>
그런데 <script> 내부에서 매번 export default { setup() {} }을 작성하고, 템플릿에 노출할 값들을 명시적으로 return하는 것도 사실 귀찮은 보일러플레이트이다. 그래서 그것에 대한 설탕으로, <script setup>이라는 것이 존재한다.
<template>
<div>{{ todayISO8601 }}</div>
</template>
<script setup>
import useMyHook from '@/hooks/my-hook'
const props = defineProps(...)
const emit = defineEmits([...])
const { todayISO8601 } = useMyHook()
</script>
위 코드에서 알 수 있듯이, 번거로운 보일러플레이트가 꽤나 간소화된다. 또한 선언한 변수나 함수들은 별도의 return 없이 자동으로 템플릿에 노출되어 사용할 수 있게 된다. 그리고 prop, emit은 defineProps, defineEmits를 통해 정의하는데, 이런 함수들은 딱히 명시적으로 import하지 않아도 된다.
Typescript
처음 프로젝트를 생성할 때부터 Typescript 기반으로 생성했다면 바로 사용할 수 있고, 기존 프로젝트에서 추가하고 싶은 경우라면 터미널에서 간단히
vue add typescript
를 하면 된다. (vue-cli가 글로벌로 설치되어 있는 경우 가정) 타입스크립트를 설치했다면 이후부터는
<script lang="ts">과 같이 lang="ts"만 붙이면 된다. 물론 당연하게도 <script setup lang="ts">도 가능하다.
내가 새로 Vue 프로젝트를 만든다면?
- Vite
- Typescript
- script setup
- Vuex (X) Pinia (O)
요렇게 scaffolding 할 것 같다. 현 회사에서의 3년 넘은 Vue 3 프로젝트 역시 최근에 script setup + typescript로의 전환을 마쳤다. 너무나 작업량이 많아 엄두를 내지 못 하고 있었는데 github copilot의 도움으로 비교적 빠르게 마칠 수 있었다.