watch가 한 번도 실행되지 않아서 보니 감시 대상이 값이었던 문제
문제 발생
watch를 걸었는데 콜백이 한 번도 실행되지 않았습니다.
const state = reactive({ count: 0 });
watch(state.count, (count) => {
console.log(`Count is: ${count}`); // 영원히 실행되지 않음
});오류도 경고도 없이 조용했습니다.
원인 분석
watch의 첫 인자는 반응형 소스여야 합니다. 공식 문서 기준으로 가능한 것은 ref(계산된 ref 포함), reactive 객체, getter 함수, 그리고 이들의 배열입니다.
state.count는 이 중 아무것도 아닙니다. 이미 평가되어 숫자 0이 넘어간 것입니다. 문서도 이 형태를 두고 "reactive 객체의 속성을 이렇게 감시할 수는 없다"고 명시합니다. 숫자에는 추적할 것이 없으므로 아무 일도 일어나지 않습니다.
해법은 getter로 감싸는 것입니다. getter 자체가 반응형 소스이고, 그 안에서 state.count에 접근하는 순간 Vue가 의존성을 기록합니다.
여기서 함께 알아둘 것이 세 가지 더 있습니다.
reactive 객체를 그대로 감시하면 암묵적으로 깊은 감시가 됩니다. 중첩 속성 변경에도 콜백이 실행되고, 이때 newValue와 oldValue는 같은 객체입니다. 이전 값을 비교하려던 코드가 항상 같다고 판단하는 원인입니다.
깊은 감시는 비쌉니다. 문서가 경고합니다 — 중첩된 모든 속성을 순회해야 하므로 큰 데이터 구조에서는 비용이 큽니다. Vue 3.5부터는 deep에 숫자를 줘서 순회 깊이를 제한할 수 있습니다.
watchEffect는 비동기에서 의존성을 놓칩니다. 문서 기준 watchEffect는 동기 실행 중에만 의존성을 추적하므로, 첫 await 이전에 접근한 속성만 추적됩니다.
해결 방안
- 속성은 getter로 감쌉니다.
watch(
() => state.count,
(count, previous) => {
console.log(`Count is: ${count} (이전 ${previous})`);
},
);- 중첩 변경까지 봐야 하면
deep을 명시합니다. 암묵적 깊은 감시에 기대지 않으면 의도가 코드에 남습니다.
watch(() => state.filters, onFiltersChanged, { deep: true });
watch(() => state.tree, onTreeChanged, { deep: 2 }); // 3.5+- 이전 값이 필요하면 원시 값이나 사본을 감시합니다. 객체를 그대로 감시하면
newValue === oldValue입니다.
watch(() => state.filters.keyword, (next, previous) => { /* 둘이 다름 */ });await이후의 값을 추적해야 하면watch를 씁니다.
// ❌ todoId 변경이 반영되지 않을 수 있음
watchEffect(async () => {
const response = await fetch(`/api/${todoId.value}`);
data.value = await response.json();
});
// ✅ 감시 대상이 명시적
watch(todoId, async (id) => {
const response = await fetch(`/api/${id}`);
data.value = await response.json();
});- 갱신된 DOM을 읽어야 하면
flush: "post"입니다. 기본값에서는 콜백이 해당 컴포넌트의 DOM 갱신 전에 실행됩니다.
watch(source, callback, { flush: "post" });
// 또는
import { watchPostEffect } from "vue";
watchPostEffect(() => { /* Vue가 DOM을 갱신한 뒤 실행 */ });-
flush: "sync"는 신중하게 씁니다. 문서가 경고하듯 배칭이 없어 반응형 변경마다 매번 실행되므로, 배열처럼 짧은 시간에 여러 번 바뀌는 데이터에는 쓰지 않습니다. -
파생 값이면
watch가 아니라computed입니다. 다른 값을 계산해 두는 것이 목적이라면 감시자는 과합니다.
const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0));
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.