[Vue3&TypeScript] watch() 안에서 발생하는 오류 해결방법 : Uncaught (in promise) Maximum recursive updates exceeded in component . This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
뷰에서 watch
를 꼭 써야 하는 건 아니지만, 팝업 열릴 때만 로직 실행하려면 watch
는 적절한 수단이다. 하지만 문제가 발생할 수 있다. forEach
안에서 반응형 객체의 개별 속성에 동적 할당하는 패턴은 무한 루프나 성능 문제 유발 가능성 있다.
accessData:1 Uncaught (in promise) Maximum recursive updates exceeded in component <DataTable>. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
위 오류는 Vue.js에서 무한 루프가 발생했을 때 나타나는 전형적인 에러이다. Vue.js (또는 Vue 기반 라이브러리 like PrimeVue 등)의 컴포넌트에서 reactive 데이터의 무한 루프 업데이트가 발생하고 있다는 뜻이다.
원인 정리
- watcher, computed, template, updated hook 등에서 데이터를 변경하고, 그 변경이 다시 해당 watcher나 computed를 트리거함.
- computed 속성이 내부에서
ref
값을 수정하거나reactive
상태를 직접 변경할 경우. - template에서 어떤 데이터를 변경하는 로직이 indirect하게 다시 그 데이터에 영향을 미침.
- 상태 공유(Store, Pinia 등) 사용 시
v-model
같은 쌍방향 바인딩과 맞물려 무한 업데이트.
이 에러는 watcher, computed, 또는 template 렌더링 중 reactive
한 데이터를 갱신하면 자기 자신을 계속해서 재실행하는 루프에 빠질 때 발생한다. 예시 코드는 다음과 같다.
watch(() => myValue.value, (newVal) => {
myValue.value = newVal + 1; // ❌ 무한 루프
});
또는
<template>
<DataTable :value="rows" />
</template>
<script setup>
const rows = ref([]);
rows.value = someComputation(rows.value); // ❌ 무한 루프
</script>
아래 코드는 Vue의 반응형 시스템과 충돌하면서 무한 루프나 렌더링 오류가 발생할 수 있다. 특히 selectedUserData
가 ref({})
로 되어 있고 value[xxx] = ...
식으로 속성 추가(=set) 를 반복하면서 Vue가 내부적으로 추적하지 못하거나, 무한 렌더링을 유발할 수 있다.
mainStore.getUserList.forEach(item => {
selectedUserData.value[item.userId] = { ... }
});
해결 방법
1. watch
또는 computed
에서 값을 변경금지
watch
나 computed
내부에서 해당 값 자체를 직접 갱신하는 것은 피해야 합니다.
2. ref
또는 reactive
값의 변경이 렌더링을 계속 유발하는 구조가 아닌지 점검
특히 DataTable
에 전달하는 데이터가 그 내부에서 다시 수정되면 무한 루프가 된다.
진단 포인트
아래 중 어떤 코드가 있는지 점검:
체크리스트 | 예시 |
---|---|
❓ rows.value 또는 accessData 를 watch 하면서 내부에서 값을 다시 수정하고 있는가? | watch(accessData, () => accessData.value.push(...)) |
❓ DataTable 에 넘기는 값이 computed 인데 내부에서 해당 computed 를 수정하는가? | :value="computedData" 내부에서 computedData 를 다시 갱신 |
❓ v-model 이나 이벤트 바인딩을 통해, 값 변경 시 다시 emit → 다시 바인딩되는 구조인가? | @update:modelValue="(val) => modelValue = val" |
무한루프 예시
const accessData = ref([]);
watch(accessData, () => {
accessData.value = accessData.value.map(item => ({ ...item, updated: true }));
});
수정 예시
watch(accessData, (newVal, oldVal) => {
// 값이 달라졌을 때만 반응
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
accessData.value = newVal.map(item => ({ ...item, updated: true }));
}
});
문제 해결을 위해, 전체 객체를 생성해 한 번에 할당하는 구조로 리팩토링하는 것이 좋다.
<script setup lang="ts">
...이상 생략
watch(
() => props.isOpen,
async (value) => {
if (value) {
if (props.userId) {
await mainStore.getUserList(props.userId);
await nextTick();
if (Array.isArray(mainStore.userList)) {
const newMap: typeof selectedUserData.value = {};
mainStore.userDataList.forEach(item => {
newMap[item.userId] = {
...item,
contractNo: props.userId ?? '',
loginYn: false,
delYn: false
};
});
// 전체를 한 번에 바꿔서 반응형 시스템 충돌 방지
selectedUserData.value = newMap;
}
}
}
}
);
</script>
// mainStore.ts
const userDataList = ref<UserType[]>([]);
const readonlyUserDataList = computed(() => [...userDataList .value]);
const getUserList= async (userId: string) => {
try {
const { data } = await api.get(`/api/test/userList/${userId}`);
userDataList.value = data.contents; //api 리턴 방식에 따라 바뀜
} catch (e) {
console.error(e);
}
};