Front-End프로그래밍

[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 데이터의 무한 루프 업데이트가 발생하고 있다는 뜻이다.


원인 정리

  1. watcher, computed, template, updated hook 등에서 데이터를 변경하고, 그 변경이 다시 해당 watcher나 computed를 트리거함.
  2. computed 속성이 내부에서 ref 값을 수정하거나 reactive 상태를 직접 변경할 경우.
  3. template에서 어떤 데이터를 변경하는 로직이 indirect하게 다시 그 데이터에 영향을 미침.
  4. 상태 공유(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의 반응형 시스템과 충돌하면서 무한 루프나 렌더링 오류가 발생할 수 있다. 특히 selectedUserDataref({})로 되어 있고 value[xxx] = ... 식으로 속성 추가(=set) 를 반복하면서 Vue가 내부적으로 추적하지 못하거나, 무한 렌더링을 유발할 수 있다.

mainStore.getUserList.forEach(item => {
  selectedUserData.value[item.userId] = { ... }
});


해결 방법

1. watch 또는 computed에서 값을 변경금지

watchcomputed 내부에서 해당 값 자체를 직접 갱신하는 것은 피해야 합니다.

2. ref 또는 reactive 값의 변경이 렌더링을 계속 유발하는 구조가 아닌지 점검

특히 DataTable에 전달하는 데이터가 그 내부에서 다시 수정되면 무한 루프가 된다.


진단 포인트

아래 중 어떤 코드가 있는지 점검:

체크리스트예시
rows.value 또는 accessDatawatch하면서 내부에서 값을 다시 수정하고 있는가?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);
  }
};
error: Content is protected !!