[JavaScript 시리즈 18편] 작은 DOM 코드를 테스트하는 방법

English version

DOM을 직접 조작하는 함수는 버그를 만들기 쉽습니다. 이번 글에서는 jsdom과 간단한 시뮬레이션으로 UI 로직을 검증하는 방법을 소개합니다. 핵심은 “jsdom + 사용자 시나리오 테스트”이고, BrowserMock은 선택 도구입니다.

이번 글에서 새로 나오는 용어

  1. jsdom: Node.js 환경에서 DOM API를 흉내 내 테스트를 돌릴 수 있게 해 주는 라이브러리입니다.
  2. 사용자 시나리오 테스트: 실제 사용자가 클릭·입력하는 과정을 코드로 재현해 검증하는 방법입니다.
  3. data-testid: 테스트에서 요소를 안정적으로 찾기 위해 붙이는 data- 속성입니다.
  4. BrowserMock: HTML 문자열만으로 가짜 브라우저 환경을 만들어 주는 경량 도구입니다.
  5. CI(Continuous Integration): 코드를 머지하기 전에 자동으로 테스트를 실행하는 파이프라인입니다.

핵심 개념

  • jsdom(JS DOM, Node용 가짜 브라우저): Node.js 환경에서 DOM API를 흉내 내는 라이브러리입니다. 렌더 함수나 이벤트 핸들러를 테스트할 때 사용합니다.
  • 사용자 시나리오 테스트(User Scenario Test, 사용자 행동 재현): 사용자가 버튼을 클릭하고 입력을 제출하는 과정을 코드로 재현합니다.
  • BrowserMock/가상 DOM(Virtual DOM Mock, HTML 시뮬레이터): 실제 브라우저 없이도 HTML 구조를 검사할 수 있는 도구입니다.
  • 테스트 훅 데이터 속성(Test Hook Data Attribute, 안정적 선택자): data-testid 같은 속성으로 요소를 안정적으로 찾습니다.

코드로 확인하기

먼저 테스트 환경을 구성할 도구를 설치합니다. 설치 전, “테스트가 없다면 무엇이 불편한가?”를 짧게 느껴봅니다.

renderGreeting(root, "민지");
console.log(root.innerHTML);

➡️ 매번 콘솔로 눈으로 확인해야 한다는 사실이 귀찮게 느껴졌다면, 테스트 자동화의 필요성을 이미 체감한 것입니다.

npm install --save-dev vitest jsdom

간단한 렌더 함수를 만들어 테스트 대상을 정의합니다.

// greet.js
export function renderGreeting(root, name) {
  root.innerHTML = `<p data-testid="greeting">${name}님 안녕하세요</p>`;
}

jsdom으로 DOM을 흉내 내고 함수를 검증합니다.

// greet.test.js

describe("renderGreeting", () => {
  it("이름을 포함한 인사말을 표시한다", () => {
    const dom = new JSDOM(`<!doctype html><div id="root"></div>`);
    const root = dom.window.document.getElementById("root");
    renderGreeting(root, "민지");
    const text = root.querySelector('[data-testid="greeting"]').textContent;
    expect(text).toBe("민지님 안녕하세요");
  });
});

jsdom을 사용하면 Node.js 환경에서도 DOM API를 사용할 수 있습니다. 즉, “브라우저 없이도 DOM을 다룬다”는 장점이 핵심입니다.

이제 이벤트가 포함된 카운터 컴포넌트를 만들어 봅니다.

// counter.js
export function initCounter(root) {
  let value = 0;
  root.innerHTML = `
    <div>
      <output data-testid="value">0</output>
      <button data-testid="inc">+</button>
    </div>
  `;
  root.querySelector('[data-testid="inc"]').addEventListener("click", () => {
    value += 1;
    root.querySelector('[data-testid="value"]').textContent = String(value);
  });
}

사용자 시나리오를 이벤트로 재현해 기능을 검증합니다.

// counter.test.js

describe("initCounter", () => {
  it("버튼 클릭 시 값이 증가한다", () => {
    const dom = new JSDOM(`<!doctype html><div id="app"></div>`);
    const app = dom.window.document.getElementById("app");
    initCounter(app);
    const button = app.querySelector('[data-testid="inc"]');
    const value = app.querySelector('[data-testid="value"]');
    button.dispatchEvent(new dom.window.Event("click"));
    expect(value.textContent).toBe("1");
  });
});

사용자 시나리오를 이벤트로 재현해 기능을 검증합니다. 한 문장으로 요약하면 “버튼을 눌렀다고 가정하고, 값이 바뀌었는지 assert 한다”입니다.

BrowserMock처럼 가벼운 도구로도 DOM 구조를 흉내 낼 수 있습니다. BrowserMock은 “테스트 프레임워크 없이 HTML을 빠르게 그려 보는 선택 도구”로 생각하면 됩니다.

// browser-mock 예시

const { document, window } = mock("<form><input name=\"title\" /></form>");
const input = document.querySelector("input");
input.value = "회의 준비";
document.querySelector("form").dispatchEvent(new window.Event("submit"));

BrowserMock처럼 간단한 HTML 문자열로 DOM을 만들 수 있는 도구를 사용하면 테스트 세팅이 빨라집니다.

BrowserMock은 “UI 상태를 빠르게 그림으로 확인”하는 용도로 좋습니다. jsdom 테스트 코드보다 짧고, 다음 순서를 반복하면 됩니다.

  1. HTML 조각을 문자열로 만든다.
  2. 초기화 함수를 실행한다.
  3. 이벤트를 발생시켜 상태 변화를 재현한다.
  4. document.body.innerHTML이나 특정 노드의 텍스트를 출력해 캡처한다.
http://localhost:5173/counter
TEST

Test Preview

테스트가 검증해야 하는 화면 상태

테스트는 추상적인 규칙이 아니라, 클릭 전과 클릭 후에 무엇이 바뀌는지를 확인하는 작업입니다.

jsdomdata-testidclick

Before Click

초기 상태

output 값이 0으로 보임.

After Click

클릭 1회

값이 1로 바뀌고 버튼은 그대로 유지.

Assertion

테스트 포인트

data-testid='value'의 텍스트가 1인지 확인.


const { document, window } = mock(`<div id="app"></div>`);
initCounter(document.getElementById("app"));
const button = document.querySelector('[data-testid="inc"]');
button?.dispatchEvent(new window.Event("click"));
console.log(document.querySelector('[data-testid="value"]').textContent);

이렇게 캡처한 HTML을 스크린샷과 함께 리뷰 문서에 붙이면 “BrowserMock이 너무 추상적이지 않을까?” 하는 걱정을 줄일 수 있습니다. BrowserMock은 선택이지만, 동료에게 “이 HTML 상태가 맞나요?”라고 물을 때 매우 빠른 조력자입니다.

기대 출력 확인

테스트 글에서는 npm test 결과도 같이 보는 편이 좋습니다.

:::terminal{title="Vitest 실행 결과 예시", showFinalPrompt="false"}

[
  { "cmd": "npm test", "output": "✓ greet.test.js (1)\n✓ counter.test.js (1)\n\nTest Files  2 passed\nTests       2 passed\nDuration    0.83s", "delay": 500 }
]

:::

  • 확인할 점: 어떤 테스트 파일이 통과했는지 보이는지
  • 확인할 점: 실패 시 어느 파일이 깨졌는지 바로 찾을 수 있는지
  • 확인할 점: 기대한 DOM 상태와 테스트 결과가 연결되는지

왜 중요한가

  • DOM 코드는 눈으로만 확인하면 놓치는 버그가 많습니다. 테스트를 작성하면 리팩터링 시 안심할 수 있습니다.
  • jsdom 테스트는 CI(자동화된 검사)에서 빠르게 실행됩니다.
  • 사용자 시나리오를 코드로 남기면 QA 팀에 전달할 Step-by-step 문서를 자동으로 얻을 수 있습니다.

실습

  • 핵심 따라 하기: renderGreeting처럼 간단한 렌더 함수 테스트를 jsdom으로 작성합니다.
  • 선택 확장하기: 이벤트가 포함된 컴포넌트를 테스트하고, 클릭·입력·폼 제출 시나리오를 코드로 재현합니다.
  • 디버깅: 의도적으로 DOM 선택자를 틀리게 만든 뒤 테스트가 실패하는지 확인하고 수정합니다.
  • 완료 기준: 최소 두 개 테스트가 통과하고, npm test 명령을 실행해 CI에서 성공했다면 실습이 끝납니다.

마무리

작은 UI라도 테스트를 작성하면 자신 있게 배포할 수 있습니다. 다음 글에서는 배포와 환경 구성 기초를 살펴봅니다.

💬 댓글

이 글에 대한 의견을 남겨주세요