공부 및 일상기록

Vitest + userEvent 테스트 중 innerHTML로 인한 DOM 재생성 문제 해결기 본문

개발/Javascript

Vitest + userEvent 테스트 중 innerHTML로 인한 DOM 재생성 문제 해결기

낚시하고싶어요 2025. 7. 31. 06:19

클린코드 과제를 진행하면서 이제야 basic과제를 끝냈다고 생각한 순간 테스트케이스 하나가 약 한 시간 동안 내 발목을 잡았다.

그래서 그 문제를 적어보려고 한다.

 

문제 상황 : 장바구니에서 +/- 를 각각 클릭 후 텍스트가 잘 반영되는지 확인하는 테스트코드

// 문제가 발생한 테스트 코드
it('+/- 버튼으로 수량 조절', async () => {
          sel.value = 'p1';
          addBtn.click();

          const increaseBtn = cartDisp.querySelector('.quantity-change[data-change="1"]');
          const decreaseBtn = cartDisp.querySelector('.quantity-change[data-change="-1"]');

          // 증가
          await userEvent.click(increaseBtn);
          expect(cartDisp.querySelector('.quantity-number').textContent).toBe('2');

          // 감소
          await userEvent.click(decreaseBtn);
          expect(cartDisp.querySelector('.quantity-number').textContent).toBe('1');
        });

 

 

addBtn.click()을 통해 장바구니에 상품을 추가한 뒤,
+ 버튼을 누르면 수량이 2로 증가되는데,
이어 - 버튼을 누르면 다시 1이 되지 않고 여전히 2인 상태로 유지되는 문제가 발생했다.

 

의심한 원인

  1. userEvent 비동기 처리 문제인가?
  2. renderQuantity에서 수량 표시가 잘못되었나?
  3. getElementById로 찾은 요소가 캐시되어 있었나?

문제 해결을 위한 디버깅 과정 요약

1. userEvent 비동기 처리 문제인가?

  • await userEvent.click(...)이 제대로 동작하지 않는다고 의심함.
  • 하지만 콘솔 로그 시 증가 전 → 증가 후 값이 정상적으로 바뀌는 것을 확인했고,
    비동기 타이밍 이슈는 아님을 빠르게 배제함.
  • 그리고 혹시 몰라서 waitFor까지 사용해보았음.

2. renderQuantity()에서 수량 표시가 잘못되었나?

  • 실제 UI에서는 수량이 정상적으로 반영되는데, 테스트에서는 감소 후에도 2로 유지됨.
  • 수량 표시 관련 렌더링 로직이 잘못된 게 아닌지 의심하여 수동 테스트와 비교.
  • UI 상에서는 1 → 2 → 1이 자연스럽게 반영되었기에, 렌더링 로직 자체는 문제 없음.

3. getElementById 또는 querySelector로 찾은 요소가 캐시되었나?

  • increaseBtn, decreaseBtn을 테스트 상단에서 변수로 저장해둠.
  • 그런데 수량 변경 후에도 버튼은 변하지 않았다고 가정했으나,
  • 중간에 innerHTML로 cart DOM 전체가 재렌더링되며 기존 버튼이 사라지고 새로운 버튼이 생성되는 구조였음.
    • 위 문제는 console.log()로 새로운 돔인지 아닌지 클릭해서 눈치챘다..
    • 분명 같은 함수의 같은 로그인데 addBtn을 누를 때와 +/- 버튼을 누를 때 다른줄로 표시되서 눈치챔..

✅ 그래서 어떻게 해결 하였나?

  • increaseBtn, decreaseBtn, quantityNumber를 매 클릭 직전에 querySelector로 다시 가져오는 방식으로 수정
  • 즉, 캐시하지 않고 항상 최신 DOM 상태에서 다시 요소를 가져오도록 수정
describe("6.2 수량 변경", () => {
        const getIncreaseBtn = () =>
          cartDisp.querySelector('#p1 .quantity-change[data-change="1"]');

        const getDecreaseBtn = () =>
          cartDisp.querySelector('#p1 .quantity-change[data-change="-1"]');

        it("+/- 버튼으로 수량 조절", async () => {
          sel.value = "p1";
          addBtn.click();

          const getQty = () => cartDisp.querySelector(".quantity-number");

          // 증가
          await userEvent.click(getIncreaseBtn());
          expect(getQty().textContent).toBe("2");

          // 감소
          await userEvent.click(getDecreaseBtn());
          expect(getQty().textContent).toBe("1");
        });

 

👉 getIncreaseBtn과 getDecreaseBtn을 변수에 미리 할당해두면, 해당 버튼 요소가 DOM 변경 이후에도 갱신되지 않고 캐시된 채 유지된다.
특히 innerHTML로 DOM 전체가 교체되는 상황에서는 기존 요소 참조가 무효화되어 클릭이 정상 동작하지 않게 되는것이다.

따라서, 클릭 시점에 함수를 실행하여 최신 DOM 요소를 다시 가져오도록 한 것이 해결의 포인트다!!!

 

 

 


이렇게 철저히 하나씩 원인을 제거해가며,
우연치 않게 클릭이 무엇이 다른가 하고 콘솔을 찍어보며 누르다가 DOM이 달라진다는것을 발견했고,

그것을 힌트로 테스트코드를 수정했더니 해결!

이렇게 간단해 보이지만 사실 엄청나게 많은 스트레스와 한숨이 포함된 과정이 있었다.
그래도 테스트에서 DOM을 조작하거나 재렌더링할 때는 항상 현재 요소를 매번 새로 참조해야 함을 다시 한번 깨달았다.