본문으로 건너뛰기
개발 머꼬
개발 노트Vue
hohyeon.dev18

컴포저블을 구조 분해했더니 반응성이 끊긴 이유

  • #Engineering Note
  • #Vue

문제 발생

마우스 좌표를 다루는 로직을 컴포저블로 빼고 구조 분해했는데 값이 갱신되지 않았습니다.

// ❌ 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 뒤에 호출해도 됩니다. 컴파일러가 활성 인스턴스 컨텍스트를 자동으로 복원합니다.

해결 방안

  1. 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);
  1. 입력은 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 안에서 불러야 그 값이 의존성으로 추적됩니다.

  1. 이름은 use + camelCase입니다. 규약이자, 훅 호출 규칙이 적용되는 함수임을 알리는 신호입니다.

  2. 정리를 잊지 않습니다. 이벤트 리스너, 구독, 타이머는 onUnmounted()에서 해제합니다. 컴포저블이 여러 컴포넌트에서 쓰이므로 누수가 곱해집니다.

  3. DOM을 만지는 부수 효과는 onMounted() 안에 둡니다. SSR에서 setup 본문은 서버에서도 실행됩니다.

  4. 콜백 안에서 부르지 않습니다.

setTimeout(() => {
  const { data } = useFetch(url);   // ❌ 활성 인스턴스 없음
}, 1000);

필요한 값은 setup에서 미리 만들어 두고, 콜백에서는 그 ref를 씁니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.