현대 프론트엔드 프레임워크는 테스트에서 실패하고 있다

발행: (2025년 12월 28일 오전 04:47 GMT+9)
13 min read
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text of the post (the markdown content) in order to do so. Could you please paste the article’s content here? Once I have it, I’ll provide a Korean translation while preserving the original formatting, code blocks, URLs, and the source line exactly as you requested.

TWD 출시 후 관찰

TWD를 출시한 지 몇 주 후—테스트를 개발 워크플로에 직접 통합하도록 설계된 도구—다양한 프론트엔드 프레임워크용 테스트 레시피를 만들기 시작했습니다. 그 과정에서 몇 가지 패턴이 매우 뚜렷하게 드러났습니다.

SPA‑중심 애플리케이션

  • 프론트엔드 전용 애플리케이션(SPA)은 TWD와 Vitest 같은 전통적인 러너를 사용하면 비교적 쉽게 테스트할 수 있습니다.
  • 실제 사용자 인터랙션을 검증하고, 테스트를 UI에 가깝게 유지하며, 빠르게 반복할 수 있습니다.

SSR 프레임워크

  • Next.js, Astro, TanStack, Qwik, Solid과 같은 프레임워크는 신뢰할 수 있고 유지 보수 가능한 방식으로 애플리케이션을 테스트하는 방법에 대한 가이드를 거의 제공하지 않습니다.
  • 대부분은 테스트를 Playwright나 Cypress와 같은 외부 도구에 단순히 위임하고(예: Next.js 문서), 테스트를 프레임워크 자체의 일부가 아니라 사후 고려 사항으로 취급합니다.

SSR이 테스트를 혼란스럽게 만드는 이유

  • SSR은 백엔드와 프론트엔드 사이의 경계를 흐릿하게 만듭니다. 이론적으로는 강력해 보이지만, 실제로는 심각한 테스트 문제를 야기합니다.
  • 프론트엔드와 백엔드 동작을 모두 테스트해야 하지만, SSR 프레임워크는 이를 위한 명확하거나 의견이 제시된 방법을 제공하지 않습니다. 문서는 보통 다음 세 가지 중 하나에 해당합니다:
  1. 전혀 없음
  2. 잠깐 언급
  3. Playwright나 Cypress와 같은 E2E 도구에 외주

“빠르게 빌드하는 방법은 여기 있습니다. 테스트는 여러분의 문제입니다.”

기업 입장—특히 장기적인 제품을 구축하는 경우—에서는 이것이 큰 경고 신호입니다. 여러분이 원하는 것은:

  • 작성하기 쉬운 테스트
  • 실제로 문제가 발생했을 때 신뢰성 있게 실패하는 테스트
  • 버그 재현에 도움이 되는 테스트
  • 제품과 함께 진화하는 테스트

대부분의 SSR 프레임워크는 이를 제공하지 못합니다. 개발 속도를 최적화하지만, 첫 날부터 테스트 부채가 쌓이기 시작합니다.

SSR 환경에서 프론트엔드 코드 단위 테스트의 현실

  • 기술적으로는 가능하지만, 실제로는 단위 테스트가 “테스트 자체를 위한 테스트”가 되기 쉽습니다.
  • 흔히 겪는 함정:
    • 시스템의 절반을 모킹
    • 구현 세부 사항을 테스트
    • 실제 사용자 인터랙션을 테스트하지 않음

결국 Playwright로 돌아가게 되는데, 이는 새로운 문제들을 야기합니다:

  • 테스트가 앱 외부에 존재
  • 느리다
  • 디버깅이 어려워
  • 개발 워크플로의 일부처럼 느껴지지 않는다

이 패턴은 Next.js 애플리케이션에서 매우 흔합니다. 지금까지 Next.js에 대해 다음 조건을 모두 만족하는 진정으로 견고한 테스트 설정을 본 적이 없습니다:

  • 의미 있는 문제가 발생했을 때 오직 그때만 실패
  • 구현 세부 사항을 피함
  • 작성 및 유지 보수가 쉬움

“견고함”이란, 단순히 커버리지를 높이는 것이 아니라 실제로 제품을 보호하는 테스트를 의미합니다.

눈에 띄는 예외: React Router

React 생태계에서 React Router(특히 createRoutesStub)가 돋보입니다.

createRoutesStub이 제공하는 것

  • 로드러와 액션이 포함된 목(Mock) 라우트
  • 페이지별로 다양한 시나리오 생성
  • 프론트엔드 동작을 현실감 있게 테스트
  • 로드러와 액션을 순수 함수로 별도 테스트

이러한 관심사의 분리는 많은 프레임워크가 구현하려고 시도하지만, React Router는 이를 제대로 구현했습니다.

테스트에 대한 장점

  • 간단함 – API가 사용하기 쉽습니다.
  • 명시적 – 무엇이 스텁되는지 명확히 확인할 수 있습니다.
  • 예측 가능 – 동작이 결정론적입니다.

게다가 SPA 데이터 모드에서 프레임워크(SSR) 모드로의 마이그레이션이 점진적이며 위험도가 낮습니다.

여러 대안을 조사하고 테스트가 제품의 가장 중요한 측면 중 하나라는 점을 고려했을 때, React Router는 오늘날 제가 개인적으로 선택할 유일한 SSR‑지원 솔루션입니다.

createRoutesStub와 TWD 사용

TWD를 구축하면서 createRoutesStub가 얼마나 강력한지 알게 되었고, 이를 다른 방식으로 활용하기로 했습니다.

접근 방식

라우트를 개별적으로 테스트하는 대신에 우리는:

  1. 전용 테스트 라우트 추가
  2. 실제 애플리케이션 내부에서 TWD 테스트 실행
  3. 라우트 스텁을 사용해 페이지 렌더링
  4. 각 테스트마다 로더, 액션, 시나리오 제어

설정

// src/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("todos", "routes/todolist.tsx"),
  route("testing", "routes/testing-page.tsx"),
] satisfies RouteConfig;
// app/routes/testing-page.tsx
let twdInitialized = false;

export async function clientLoader() {
  if (import.meta.env.DEV) {
    const testModules = import.meta.glob("../**/*.twd.test.{ts,tsx}");
    if (!twdInitialized) {
      const { initTWD } = await import('twd-js/bundled');
      initTWD(testModules, {
        serviceWorker: false,
      });
      twdInitialized = true;
    }
    return {};
  } else {
    return {};
  }
}

export default function TestPage() {
  return (
    <div data-testid="testing-page">
      <h1>TWD Test Page</h1>
      <p>This page is used as a mounting point for TWD tests.</p>
    </div>
  );
}
// test-utils/setupReactRoot.ts
import { createRoot } from "react-dom/client";
import { twd, screenDomGlobal } from "twd-js";

let root: ReturnType<typeof createRoot> | undefined;

export async function setupReactRoot() {
  if (root) {
    root.unmount();
    root = undefined;
  }

  // Navigate to the empty test page
  await twd.visit('/testing');

  // Get the container from the test page
  const container = await screenDomGlobal.findByTestId('testing-page');
  root = createRoot(container);
  return root;
}
// example.test.tsx
import { createRoutesStub } from "@react-router/dev";
import { useLoaderData, useParams, useMatches } from "react-router-dom";
import { Home } from "../routes/home";
import { setupReactRoot } from "./test-utils/setupReactRoot";

describe("Hello World Test", () => {
  // root instance to re‑render the stub
  let root: ReturnType<typeof createRoot> | undefined;

  beforeEach(async () => {
    // preparing the root instance
    root = await setupReactRoot();
  });

  it("should render home page test", async () => {
    // mocking the component
    const Stub = createRoutesStub([
      {
        path: "/",
        Component: () => {
          const loaderData = useLoaderData();
          const params = useParams();
          const matches = useMatches() as any;
          return (
            <div>
              {/* Your test UI here */}
            </div>
          );
        },
        loader() {
          return { title: "Home Page test" };
        },
      },
    ]);

    // Render the Stub
    root!.render(<Stub />);
    await twd.wait(300);

    // Check for the element within our test container
    // We scope the search to the container to be safe,
    // or just search globally since …
  });
});

Note: 위 테스트는 스텁된 라우트를 렌더링하고, 로더 데이터를 제어하며, UI 출력을 검증하는 방법을 보여줍니다—모두 실제 애플리케이션 컨텍스트 안에서 TWD를 테스트 러너로 사용합니다.

TL;DR

  • SSR 프레임워크는 테스트를 뒷전으로 미루는 경우가 많아, 테스트 스위트가 깨지기 쉽고 유지보수가 어렵습니다.
  • **React Router의 createRoutesStub**은 라우팅 로직과 UI를 격리된 상태에서 테스트하면서도 실제 앱 안에서 실행할 수 있는 깔끔하고 의견이 반영된 방법을 제공합니다.
  • 전용 테스트 라우트에 TWD를 삽입하면 빠르고 통합된 워크플로우를 얻을 수 있어, 개발 프로세스의 자연스러운 확장처럼 느껴집니다.

SSR 스택 위에 장기적인 제품을 구축한다면, 초기부터 테스트 부채를 방지하기 위해 이 패턴을 도입하는 것을 고려해 보세요.

예시: twd + React Router 통합

// test file (e.g., HomePage.test.ts)
import { screenDom } from 'twd';
import { renderWithRouter } from './test-utils';

test('renders home page heading', async () => {
  renderWithRouter();

  // The harness is empty otherwise
  const h1 = await screenDom.findByRole('heading', { level: 1 });
  twd.should(h1, 'have.text', 'Home Page test');
});

테스트가 실행되면 라우트가 실제 애플리케이션 안에서 렌더링되어 완전히 인터랙티브하게 동작하며, 모든 어설션이 통과합니다.

올바른 전략 선택

  • 속도에 최적화된 도구를 계속 사용하지만 테스트 부채가 쌓이는 경우
  • 또는 테스트를 일상 개발의 일부로 만드는 접근 방식을 채택하는 경우

이 설정이 SSR 애플리케이션에 잘 맞는 이유

  • 실제 UI 테스트 – 컴포넌트가 실제 브라우저 환경에서 실행됩니다.
  • 유지 보수성 및 디버깅 용이성 – 테스트가 실제 사용자 흐름을 그대로 반영합니다.
  • 장기적인 제품 – 테스트 스위트가 코드베이스와 함께 성장하면서도 깨지기 쉽지 않습니다.
  • 빠른 피드백 – 실패가 빠르게 드러나 개발 루프를 촘촘히 유지할 수 있습니다.

복잡한 요구 사항을 가진 대규모 애플리케이션을 다루는 경우, 이 접근 방식은 기존 SSR 테스트 전략보다 훨씬 더 확장성이 뛰어납니다.

Back to Blog

관련 글

더 보기 »

Next.js에서 Hydration 오류 수정

수화 오류의 일반적인 원인 브라우저/환경 문제 - 속성을 주입하는 브라우저 확장 프로그램, password managers, ad blockers, accessibility tools - Br...

Next js에서 번들 크기를 줄이는 방법

제가 처음 Next.js를 사용하기 시작했을 때, 기본 설정만으로도 얼마나 빠른지 정말 좋았습니다. 프로젝트가 커지면서 bundle size가 계속 증가해 로드가 느려졌습니다.