컴포저블을 구조 분해했더니 반응성이 끊긴 이유
문제 발생
마우스 좌표를 다루는 로직을 컴포저블로 빼고 구조 분해했는데 값이 갱신되지 않았습니다.
// ❌ reactive를 반환
export function useMouse() {
const state = reactive({ x: 0, y: 0 });
return state;
}
const { x, y } = useMouse(); // 반응성 끊김setTimeout 안에서 컴포저블을 부른 곳에서는 아예 오류가 났습니다.
원인 분석
Vue 공식 문서의 컴포저블 규약이 두 가지를 짚습니다.
반환값은 ref를 담은 평범한 객체여야 합니다. reactive() 객체를 반환하면 구조 분해하는 순간 반응성 연결이 끊깁니다(reactive()의 문서화된 한계 그대로입니다). ref를 담은 일반 객체를 반환하면 구조 분해해도 각 ref가 살아 있습니다.
동기적으로 호출해야 합니다. 문서는 컴포저블을 <script setup>, setup() 훅, onMounted() 같은 라이프사이클 훅 안에서 동기적으로 호출해야 한다고 명시하고, 이유도 적습니다 — Vue가 현재 활성 컴포넌트 인스턴스를 알아야 라이프사이클 훅을 등록하고, 감시자를 연결해 정리와 메모리 누수 방지를 할 수 있기 때문입니다. setTimeout 콜백에는 활성 인스턴스가 없습니다.
예외가 하나 있습니다 — <script setup> 안에서는 await 뒤에 호출해도 됩니다. 컴파일러가 활성 인스턴스 컨텍스트를 자동으로 복원합니다.
해결 방안
- ref를 담은 객체를 반환합니다.
export function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (event) => {
x.value = event.pageX;
y.value = event.pageY;
};
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
return { x, y }; // 구조 분해해도 반응성 유지
}속성 접근 형태가 좋으면 쓰는 쪽에서 감싸면 됩니다.
const mouse = reactive(useMouse());
console.log(mouse.x);- 입력은 ref나 getter도 받게 하고
toValue()로 정규화합니다. 호출부가 상수를 줄지 반응형 값을 줄지 모릅니다.
import { toValue, watchEffect } from "vue";
export function useFetch(url) {
const data = ref(null);
watchEffect(() => {
fetch(toValue(url)).then((r) => r.json()).then((json) => (data.value = json));
});
return { data };
}toValue()를 watchEffect 안에서 불러야 그 값이 의존성으로 추적됩니다.
-
이름은
use+ camelCase입니다. 규약이자, 훅 호출 규칙이 적용되는 함수임을 알리는 신호입니다. -
정리를 잊지 않습니다. 이벤트 리스너, 구독, 타이머는
onUnmounted()에서 해제합니다. 컴포저블이 여러 컴포넌트에서 쓰이므로 누수가 곱해집니다. -
DOM을 만지는 부수 효과는
onMounted()안에 둡니다. SSR에서 setup 본문은 서버에서도 실행됩니다. -
콜백 안에서 부르지 않습니다.
setTimeout(() => {
const { data } = useFetch(url); // ❌ 활성 인스턴스 없음
}, 1000);필요한 값은 setup에서 미리 만들어 두고, 콜백에서는 그 ref를 씁니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.