composables은 Vue3 앱에서 비즈니스 로직을 구성하는 가장 좋은 방법이다.
반복적, 재사용을 쉽게 할 수 있으며, 논리적으로 작은 부분을 추출할 수 있다. 코드를 더 쉽게 작성하고 읽을 수 있다.
영어로 된 블로그 글을 참고/번역(번역이라고 하기에는 민망한...)하였습니다. 블로그 글의 부제들을 기준으로 내가 경험적인 부분을 토대로 내용을 추가하였습니다. 개인적인 생각이 많이 들어가있으므로 참고하시기 바랍니다.
공식문서에 따르면 Vue Composition API를 활용하여 stateful login을 캡슐화하고 재사용하는 함수이다. 반응성을 사용하는 모든 코드를 composible로 사용할 수 있다.
대부분의 composables에는 한 두개의 필수 입력값을 가지며, 옵셔널한 선택 전달인자도 존재한다.
vue에서 가장 흔하게 볼 수 있는 computed와 watch options를 대표적으로 예를들 수 있다.
computed의 Type은 다음과 같다.
// read-only
function computed<T>(
getter: () => T,
// see "Computed Debugging" link below
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// writable
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
vue를 처음 사용하시는 분들은 computed()를 단순 계산, read-only의 방식으로만 사용한다. 예시도 많이 없을분더러 가장 많이 사용하는 코드이기도 하다. 하지만 vue 개발을 오래하다보면 어느 순간 더러운 watch 체이닝을 지양하기 위해 computed에 setter를 달게 되는 본인의 모습이 보이게 될 것이다. 이 때, computed의 getter/setter를 사용할 때, 파라미터가 콜백함수 하나가 아닌 get, set을 키로 들고있는 객체를 넘겨준다.
공식문서의 예제는 다음과 같다.
Creating a readonly computed ref:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
Creating a writable computed ref:
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watch의 Type은 다음과 같다.
interface ComponentOptions {
watch?: {
[key: string]: WatchOptionItem | WatchOptionItem[]
}
}
type WatchOptionItem = string | WatchCallback | ObjectWatchOptionItem
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
type ObjectWatchOptionItem = {
handler: WatchCallback | string
immediate?: boolean // default: false
deep?: boolean // default: false
flush?: 'pre' | 'post' | 'sync' // default: 'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
이 중 deep, immediate 옵션을 사용해본 경험이 다들 있을 것이다. watch의 첫 번째 인자로 핸들러 함수를, deep, immediate 등의 옵션을 두 번째 인자로 넘겨준다. 여러 개의 옵션을 동시에 주기 위해 객체로 이 옵션들을 감싸 넘겨주곤 한다. 이처럼 필요한 옵션이 존재할 때, object 형태로 파라미터를 넘기는 것을 options object parameter라고 한다.
VueUse 중 useTitle의 코드는 다음과 같다.
https://github.com/vueuse/vueuse/blob/e484c4f8e4320ff58da95c2d18945beb83772b72/packages/core/useTitle/index.ts
타이틀명이라는 필수 전달인자와 그에 따른 옵션들을 옵셔널한 객체로 두번째 전달인자로 넘기는 방식으로 구현되어있다.
실제로 코드 구현을 함에 있어 팀바팀 사바사에 따라 다르지만 어떠한 함수를 생성하는데 있어 전달인자를 몇 개로 할 것인지, 프리미티브 타입으로 또는 레퍼런스 타입으로 할 것인지는 개인 취향이라고 생각한다.
하지만 필수가 아닌 요소에 대해 객체로 감싸는 부분은 옳은 방법이라고 생각한다.
객체가 아니라면 추가되는 파라미터에 대해 계속 추가해줘야하는 번거로움이 존재하며 유지보수성이 낮아진다. 또한, 객체 타입으로 인해 쉽게 구조화할 수 있으며, 유연하면서도 더 깊은 뎁스를 가질 수 있고, 객체의 default값을 쉽게 지정할 수 있으며, 객체의 구조분해할당을 통해 코드도 더 단순화 시킬 수 있다는 장점이 존재한다고 생각한다.
vue3의 composition api 방식을 사용하면서 가장 많이 사용되는 reactive API는 단연 ref, reactive라고 생각한다. 가장 core 핵심적이며, ref로 인해 computed로 파생할 수 있으며 반응성을 지니게 만들어주는 기본적인 API라고 생각한다.
unref()
라는 반응성(?) API를 본 적있는가? 지금까지 꽤 많이 Vue로 개발해왔지만 직접적으로 unref()를 호출해서 사용해본 적은 없다.
ref() 코드는 다음과 같다.
export declare type UnwrapRef<T> = T extends ShallowRef<infer V> ? V : T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>;
export declare function ref<T extends object>(value: T): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>;
삼항연산자를 사용하면서 Ref 타입이라면 T 그대로를, 아닌 경우에는 반응형을 벗긴 후에 Ref를 감싸서 리턴해준다.
const a = ref(0);
a === ref(a); // true
const b = 1;
b === unref(b); // true
JS에서 삼항연산자나 nullish 병합연산자를 사용해서 조건을 만들어 언제나 유연한 전달인자를 통해 지능적으로 사용하여 개발할 수 있도록 도와준다. 반응형인 변수에 반응형 함수 ref()를 감싸더라도 두 값이 동일한 값을, 비반응형에 unref()를 감싸더라도 두 값이 동일한 값을 나오게 하는 결과가 나타난다.
Vue 3의 composition api 방식에서는 return할 때 단일 값 또는 객체를 반환할 수 있다. 이전에는 파라미터를 동적으로 여러 개와 여러 타입을 다뤘다면, 이번에는 리턴 값을 단일 값 또는 객체를 반환한다. 파라미터와 리턴은 반대의 관계로서 쉽게 말하면 인풋/아웃풋이지만, 둘다 같은 논리로 접근하는 느낌이다.
파라미터의 타입에 따라 리턴 타입이 동적으로 달라지는 것은 VueUse의 useInterval을 예로 들 수 있다.
문서 : https://vueuse.org/shared/useInterval/
깃헙 : https://github.com/vueuse/vueuse/blob/main/packages/shared/useInterval/index.ts
코드는 다음과 같다.
export function useInterval(interval: MaybeRef<number> = 1000, options: IntervalOptions<boolean> = {}) {
const {
controls: exposeControls = false,
immediate = true,
} = options
// ...
if (exposeControls) {
return {
counter,
...controls,
}
}
else {
return counter
}
}
공식문서의 예제 코드를 보면 더 와닿을 것이다.
파라미터를 200만 넣은 경우
import { useInterval } from '@vueuse/core'
// count will increase every 200ms
const counter = useInterval(200)
파라미터를 200과 옵션 객체를 넣은 경우
const { counter, pause, resume } = useInterval(200, { controls: true })
if나 삼항연산자와 같은 조건에 의해 다른 타입으로 리턴하게 된다. 컴포저블을 보다 유연하게 사용하게 되며 개발을 하는 파일럿에 따라 복잡도가 달라질 것 같다.