
[Vue 3] Composition API - Composables
Vue.js에서 composable이란 Composition API를 사용해 로직을 구현한 함수를 말합니다. 자주 사용할 법한 로직들을 빼내서 구현한다는 점은 일반함수랑 같지만 Composition API의 ref나 Lifecycle hook 등을 사용할 수 있다는 점 덕분에 컴포넌트에서 재활용하기 좋다는 장점이 있습니다. 자세히 알아볼까요?
Composable
공식 홈페이지에 의하면 Composoable
이란 상태를 가진 로직을 캡슐화하고 재활용할 수 있게 Composition API를 이용하는 함수입니다. 말이 어렵죠?
그래서 Composable이 뭔데요?
쉽게 말하면 다른 js 파일에다가 함수를 만들고 이 함수를 컴포넌트에 가져와서 사용합니다. 입력을 넣으면 단순히 똑같은 출력만 나온다면 그냥 함수라 불렀겠죠. Composable이 다른 점은 이 함수의 내용이 Composition API처럼 사용된다는 점입니다.
여전히 어렵네요.
그래서 예시를 들어보겠습니다. 화면에서 마우스의 현재 위치 값을 가져와서 이를 화면에 띄우고 싶습니다. 컴포넌트만 사용했다면 이런 식으로 구현했을 겁니다.
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
그런데 이 마우스 좌표를 구하는 방식을 다른 컴포넌트에서도 많이 사용해서 update 함수를 밖으로 빼기로합니다. 그런데 update 함수는 utils 함수로 빼낸다면 x와 y라는 값을 모르는상태가 됩니다.
function update(event) {
// x, y가 정의되지 않은 값이 됩니다.
x.value = event.pageX
y.value = event.pageY
}
export { update };
이 값을 기억할 수 있도록 클로저 함수를 사용해 x와 y라는 기억하게 해줘야합니다.
export function update(x, y) {
return function _func(event) {
x.value = event.pageX;
y.value = event.pageY;
};
}
이렇게 해서 어찌저찌 update 함수를 재활용할 수 있게 만든거 같습니다.
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { update } from "./utils/mouse";
const x = ref(0);
const y = ref(0);
const updateFunc = update(x, y);
onMounted(() => window.addEventListener("mousemove", updateFunc));
onUnmounted(() => window.removeEventListener("mousemove", updateFunc));
</script>
<template>마우스 좌표: {{ x }}, {{ y }}</template>
근데 구현하다보니 한 가지 사실을 깨닫습니다.
x랑 y도 똑같이 만들고 window.addEventListener도 매번 쓰잖아?
네 결국 update 함수만 달라졌을 뿐 위 코드를 그대로 복사붙이기해서 다른 컴포넌트에서도 쓰고 있습니다.
Composable은 일반 함수랑 다릅니다
마우스 위치를 찾는 로직 전체를 있는 그대로 함수로 만들 수 있다면 얼마나 좋을까요? Composable은 Composition API를 이용해서 함수로 만들 수 있습니다.
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 유일하게 다른 점
return { x, y }
}
컴포넌트는 그저 Composable 함수를 가져와 사용만 하면 됩니다.
<script setup>
import { useMouse } from "./mouse.js";
const { x, y } = useMouse();
</script>
<template>마우스 좌표: {{ x }}, {{ y }}</template>
x, y 값은 이미 반응형으로 만들어서 반환한 것이기 때문에 이 컴포넌트에서도 반응형 상태로 사용할 수 있습니다. 코드가 확실히 간단해졌죠? useMouse 함수를 사용하면 다른 컴포넌트에서도 이벤트 등록이나 변수를 만들필요없이 반응형 x, y값을 가져올 수 있습니다.
Composable 끼리도 재활용 가능
보통 addEventListener
를 mounted 훅에서 썼다면 removeEventListener
를 unmounted 훅에서 써야합니다. 이러한 Composable 함수 내에 발생하는 반복되는 코드도 Composable 함수로 재활용할 수 있습니다.
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
mounted 에는 이벤트를 추가하고 unmounted에 이벤트를 제거해줍니다.
import { ref } from "vue";
import { useEventListener } from "./common";
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
// Composable 사용!
useEventListener(window, "mousemove", update);
return { x, y };
}
useMouse
함수에서 이벤트 등록이 간단해졌네요!
일반함수처럼 사용하기 (unref)
함수는 입력값을 받기도 합니다. target을 태그로 받고 message 값을 내용으로 채우는 composable 함수를 만들었다고 해보겠습니다.
export function useMessage(target, message) {
target.value.innerHTML = message;
}
target
은 template ref로 가져와서 해당 DOM에 message
값을 넣었습니다.
<script setup>
import { ref, onMounted } from "vue";
import { useMessage } from "./utils/common.js";
const msg = ref(null);
onMounted(() => {
useMessage(msg, "안녕하세요");
});
</script>
<template>메시지 : <span ref="msg"></span></template>
여기까지는 동작이 잘되는거 같습니다. 그런데 다른 사람이 이 함수의 target
값에 querySelector로 가져온 값을 넣는다면 문제가 생깁니다.
<script setup>
import { onMounted } from "vue";
import { useMessage } from "./utils/common.js";
onMounted(() => {
// ref 대신 querySelector
const msg = document.querySelector(".msg");
useMessage(msg, "안녕하세요");
});
</script>
<template>메시지 : <span class="msg"></span></template>
왜냐하면 내부에서는 ref 값을 예상하고 target.value.innerHTML
으로 구현했지만, querySelector로 가져온 값은 value
라는 값이 없기 때문입니다. 이 때 사용하기 좋은 것이 unref
입니다.
import { unref } from "vue";
export function useMessage(target, message) {
unref(target).innerHTML = message;
}
만약 Composable, 컴포넌트 둘 다 mounted 훅을 구현한 경우 Composable이 먼저 실행되고 컴포넌트가 실행됩니다.
ref인지 확인하는 isRef
예를 들어 arg
이 들어오고 이 값을 이용해서 무언가 복잡한 작업을 한다고 해보겠습니다.
export function useSomething(arg) {
// arg를 이용한 복잡한 코드
}
그런데 이 작업을 arg 값이 바뀔 때마다 하고 싶습니다. 보통 이 경우 watchEffect
를 사용하면 됩니다.
import { watchEffect } from 'vue';
export function useSomething(arg) {
function doSomething() {
// arg를 이용한 복잡한 코드
}
watchEffect(doSomething);
}
그런데 arg
가 만약 반응형 상태가 아니라면 watchEffect
는 오버헤드가 발생합니다. 그래서 arg가 반응형 상태인지 여부에 따라 watchEffect를 사용해야합니다. 이 때 사용하는 것이 isRef
입니다.
import { watchEffect } from 'vue';
export function useSomething(arg) {
function doSomething() {
// arg를 이용한 복잡한 코드
}
if (isRef(arg)) {
watchEffect(doSomething);
} else {
doSomething();
}
}
예시들을 보다보면 useXXX 로 명칭을 짓는 것을 볼 수 있습니다. 이는 Composable 함수의 이름을 지을 때 관례로 보시면 됩니다. 그리고 ref만을 사용하는 것도 isRef나 unref와 같은 함수를 이용하기에 좋기 때문입니다.
Options API에서는 못 써요?
당연히 쓸 수 있죠! setup
훅을 통해서요!
import { useMouse } from './utils/mouse.js'
export default {
setup() {
const { x, y } = useMouse();
return { x, y };
},
mounted() {
// setup()에서 return한 값은 `this`에서 접근할 수 있습니다.
console.log(this.x)
}
}
사용 시 주의사항
onMounted
와 같이 마운트된 이후 DOM 수정을 해야합니다.onMounted
에서 이벤트 리스너를 만들었다면 반드시 onUnmounted
에서 제거해줘야합니다. 아니면 위 예제(useEventListener
)처럼 만드는 것도 좋습니다.다른 기술들과 비교
Composable 뿐만 아니라 재활용을 위한 기술들은 많이 있습니다. 대표적으로 Vue2에서는 mixins이 있었고, 컴포넌트도 재활용을 위한 기술이라 할 수 있죠. 몇 가지들과 비교해보겠습니다.
믹스인 (Mixin)
Vue2일 때에는 Options API를 주로 사용했기 때문에 이 컴포넌트 로직을 재활용하기 위해서 믹스인(Mixin)
을 사용했었습니다. Options API 처럼 data, computed, watch 등을 사용할 수 있습니다.
const mixin = {
data: function () {
return {
firstName: '죠타로',
lastName: '쿠죠'
}
},
computed: {
fullName() {
return this.firstName + ' ' this.lastName;
}
}
}
new Vue({
mixins: [mixin],
})
재활용 가능함에도 불구하고 공식 홈페이지에서는 Mixin 보다 Composable을 추천하는 이유가 무엇일까요?
mixin 기능은 아직 사용할 수 있지만 Vue3를 사용한다면 Composition API와 composable에 익숙해지는 게 좋습니다.
컴포넌트 (Component)
로직 자체가 아닌 컴포넌트를 이용해서도 로직을 재활용할 수 있습니다. DOM을 그리지 않거나 렌더리스 컴포넌트 패턴을 사용하면 되니까요. 그럼에도 불구하고 컴포넌트를 사용하는 것은 여전히 오버헤드가 발생할 수 있는 요소입니다. 컴포넌트를 사용했기 때문에 로직을 재활용하면 할 수록 생성되는 인스턴스들이 많을겁니다. 따라서 로직만을 재사용한다면 composable, 시각적인 요소도 같이 재활용해야한다면 컴포넌트를 사용하는 것이 좋습니다.
리액트 훅 (React Hook)
Vue Composable과 React Hook은 확실히 비슷한 점이 많습니다. useXX라는 명칭이나 로직을 재활용할 수 있고 사용법마저 비슷합니다. 그럼에도 불구하고 차이점은 존재한다고 합니다. 아쉽게도 저는 React에 대해서 잘 알지 못하기 때문에 공식문서에서 말하는 차이점을 이해하지 못했습니다. 후에 리액트를 공부하게 된다면 정리하도록 하겠습니다.
커스텀 디렉티브 (Custom Directive)
컴포넌트와 composable이라는 재사용성을 위한 좋은 기술들이 있지만, 단순히 DOM에 접근해서 수정하는 정도라면 커스텀 디렉티브를 사용하는 것도 좋은 방법입니다.