Published on

프론트엔드 테스트 도구의 이해와 종류

Authors
  • avatar
    Name
    Justart
    Twitter

1. 기본 개념

테스트는 단순한 코드 검증 이상의 가치를 제공합니다. 이를 통해 얻을 수 있는 주요 장점은 다음과 같습니다.

코드 안정성

테스트는 리팩터링이나 버그 수정 후에도 기존 기능이 정상적으로 작동하는지 검증할 수 있게 해줍니다. 이를 통해 코드를 수정하면서도 기능이 깨지지 않도록 보장할 수 있습니다.

개발 속도 향상

수동 테스트를 반복할 필요 없이, 자동화된 테스트를 통해 빠르게 기능을 검증할 수 있습니다. 테스트가 자동으로 실행되므로 개발자는 더 많은 시간과 에너지를 새로운 기능 개발에 집중할 수 있습니다.

배포 신뢰성 증가

자동 테스트를 통과한 코드만 배포되도록 설정하면, 배포 과정에서 발생할 수 있는 오류를 줄일 수 있습니다. 테스트가 성공적으로 완료되면, 그만큼 배포의 신뢰성이 높아져 운영 환경에서 발생할 수 있는 문제를 예방할 수 있습니다.

협업 품질 개선

잘 작성된 테스트는 코드의 의도를 명확하게 전달하며, 사양서 대신 기능이 어떻게 동작해야 하는지를 보여줍니다. 이를 통해 팀원 간의 이해도를 높이고, 협업 시 발생할 수 있는 혼동을 줄일 수 있습니다.


테스트 주도 개발(Test-Driven Development, TDD)의 3단계 사이클

1. Red

  • 실패하는 테스트 코드를 먼저 작성합니다.
  • 아직 기능이 없기 때문에 당연히 실패합니다.
// 예: 문자열을 뒤집는 함수를 만든다고 가정
test('reverses string', () => {
  expect(reverseString('hello')).toBe('olleh') // ❌ 실패
})

2. Green

  • 테스트를 통과시키기 위한 최소한의 코드를 작성합니다.
function reverseString(str) {
  return str.split('').reverse().join('')
}

3. Refactor

  • 테스트를 통과하는 코드를 리팩터링합니다.
  • 이 단계에서는 동작을 바꾸지 않되, 구조 개선만 합니다.

2. 테스트의 종류

테스트 종류설명예시 라이브러리
Unit Test (단위 테스트)컴포넌트, 함수 같은 작은 단위가 의도대로 작동하는지 테스트Jest, Vitest
Integration Test (통합 테스트)여러 컴포넌트/모듈이 함께 동작할 때 제대로 연결되고 작동하는지 테스트React Testing Library, Playwright
E2E Test (엔드 투 엔드 테스트)사용자 관점에서 실제 브라우저를 통해 전체 흐름(페이지 이동, 버튼 클릭 등)을 테스트Playwright, Cypress

NOTE

참고로, Storybook은 CDD (Component-Driven Development)로 컴포넌트 중심 개발이다.

UI 컴포넌트 개발 및 문서화 도구라고 보면 된다.

Controls 애드온(Addon)을 이용하여 Unit Test, Interaction Test에 활용 할 수는 있다. (Storybook Addon)

3. 테스트 종류에 대한 자세한 설명

각 테스트의 목적과 특징을 장바구니 기능에 대한 코드 예시와 함께 설명드리겠습니다.

3-1. 단위 테스트(Unit Test)

단위 테스트는 개별 함수나 컴포넌트의 동작을 독립적으로 검증하는 테스트입니다. 코드의 기본 기능이 올바르게 동작하는지 확인하는 것이 주요 목적입니다.

특히, 리액트 환경에서는 유닛 테스트는 "비즈니스 로직"이나 "상태 변화" 같은 행동을 검증하는 데 집중해야 합니다.

VitestJest와 같은 라이브러리를 주로 사용합니다. (주로 React Testing Library 와 함께 사용한다)

TIP

빠른 테스트를 선호하고, Vite 환경이라면 Vitest를 사용해보세요.
다양한 환경에서의 안정적인 테스트를 선호한다면 Jest를 사용해보세요.

// cartUtils.test.ts
import { addToCart, calculateTotal } from './cartUtils'

describe('장바구니 유틸리티', () => {
  test('상품을 장바구니에 추가', () => {
    const cart = []
    const product = { id: 1, name: '테스트 상품', price: 10000 }
    const updatedCart = addToCart(cart, product)

    expect(updatedCart).toHaveLength(1)
    expect(updatedCart[0]).toMatchObject(product)
  })

  test('장바구니 총액 계산', () => {
    const cart = [
      { id: 1, name: '상품1', price: 10000, quantity: 2 },
      { id: 2, name: '상품2', price: 15000, quantity: 1 },
    ]

    const total = calculateTotal(cart)
    expect(total).toBe(35000) // (10000 * 2) + 15000
  })
})

3-2. 통합 테스트(Integration Test)

통합 테스트는 여러 컴포넌트가 함께 동작하는지 검증하는 테스트입니다. 컴포넌트들 간의 상호작용이 제대로 이루어지는지 확인하는 것이 주요 목적입니다.

특히, 리액트 환경에서는 통합 테스트의 목적은 여러 컴포넌트나 모듈이 잘 협력하는지 보는 것이지, 각 컴포넌트의 단순한 기능만 확인하는 것이 아닙니다. 통합 테스트는 흐름 중심이어야지, 모든 작은 상태까지 검증하려고 하면 안 됩니다. 이 부분은 유닛테스트로 넘겨야합니다.

React Testing LibraryPlaywright와 같은 라이브러리를 주로 사용합니다.

TIP

컴포넌트 간의 상호작용을 테스트하는 데 초점을 맞추고 싶다면 React Testing Library를 사용해보세요.
실제 브라우저 환경에서의 통합 테스트를 원한다면 Playwright를 사용해보세요.

// cartIntegration.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { CartProvider, useCart } from './CartContext';
import ProductList from './ProductList';
import Cart from './Cart';

const TestComponent = () => {
  const { cart } = useCart();
  return (
    <div>
      <ProductList />
      <Cart />
      <div data-testid="cart-count">{cart.length}</div>
    </div>
  );
};

describe('장바구니 통합 테스트', () => {
  test('상품을 장바구니에 추가하면 카트에 반영되어야 함', () => {
    render(
      <CartProvider>
        <TestComponent />
      </CartProvider>
    );

    // 상품 추가 버튼 클릭
    const addButton = screen.getAllByText('장바구니 추가')[0];
    fireEvent.click(addButton);

    // 카트에 상품이 추가되었는지 확인
    const cartCount = screen.getByTestId('cart-count');
    expect(cartCount).toHaveTextContent('1');

    // 카트 컴포넌트에 상품이 표시되는지 확인
    expect(screen.getByText('테스트 상품')).toBeInTheDocument();
  });
});

3-3. E2E 테스트(End-to-End Test)

E2E 테스트는 실제 사용자 관점에서 전체 흐름을 테스트하는 테스트입니다. 실제 사용 환경에서 발생할 수 있는 문제를 발견하는 것이 주요 목적입니다.

E2E 테스트는 실제 사용자 관점에서 전체 흐름을 테스트하는 것이지, 개별 컴포넌트의 구현, 세부사항을 검증하는 것이 아닙니다. 이 부분은 통합 테스트나 단위 테스트로 넘겨야합니다.

PlaywrightCypress와 같은 라이브러리를 주로 사용합니다.

TIP

빠르게 시작하고 싶고, 디버깅 하기 쉬운 환경이라면 cypress를 사용해보세요.
Cross-browser 테스트가 중요한 프로젝트라면 Playwright를 사용해보세요.

// cart.e2e.ts
import { test, expect } from '@playwright/test'

test('사용자가 상품을 장바구니에 추가하고 주문하는 시나리오', async ({ page }) => {
  // 1. 메인 페이지로 이동
  await page.goto('https://example-shop.com')

  // 2. 상품 클릭
  await page.click('text=테스트 상품')

  // 3. 장바구니에 추가
  await page.click('button:has-text("장바구니에 추가")')

  // 4. 장바구니로 이동
  await page.click('a:has-text("장바구니")')

  // 5. 장바구니에 상품이 추가되었는지 확인
  await expect(page.locator('.cart-item')).toHaveCount(1)
  await expect(page.locator('.cart-item-name')).toHaveText('테스트 상품')

  // 6. 주문하기 버튼 클릭
  await page.click('button:has-text("주문하기")')

  // 7. 결제 페이지로 이동했는지 확인
  await expect(page).toHaveURL(/checkout/)
  await expect(page.getByText('결제 정보')).toBeVisible()
})

React 테스트 최신 트렌드는 어떠한가?

✅ Vite + Vitest + React Testing Library(Integration) + Playwright (E2E)

이 조합이 현재 가장 빠르고, 개발자 친화적이며 , 실무에서도 빠르게 채택되고 있는 흐름입니다.


타입스크립트를 사용하는데 테스트 도구가 필요할까?

타입스크립트를 사용하더라도 테스트 도구는 여전히 필요합니다.

타입스크립트는 정적 타입 검사로 많은 버그를 사전에 막아줄 수 있지만, 실행 시간(runtime)의 로직 오류나 의도한 동작을 보장해주는 역할은 하지 못합니다.

테스트 도구는 그런 부분을 보완해줍니다.

구분타입스크립트테스트 도구
주요 기능컴파일타임의 타입 검사런타임에서의 로직 검증
발견 가능한 오류타입 불일치, 잘못된 함수 사용 등잘못된 결과, 사이드 이펙트, 통합 이슈 등
예시string을 기대하는데 number 전달로그인 함수가 실제로 토큰을 반환하는지 확인

예를 들어, 다음 코드가 있다고 해보겠습니다:

function add(a: number, b: number): number {
  return a - b // 실수로 뺄셈
}
  • 타입스크립트는 이 함수의 시그니처에 문제가 없기 때문에 오류를 발생시키지 않습니다.
  • 하지만 테스트 도구로 add(1, 2)의 결과가 3 이 아닌 1 임을 확인하고, 실패한 테스트를 통해 잘못된 로직을 잡아낼 수 있습니다.

요약하면, 타입스크립트는 테스트를 줄여주지만 완전히 대체하지는 못하며, 안정적인 코드 작성을 위해서는 여전히 유닛 테스트나 통합 테스트 도구가 중요합니다.

4. 테스트 도입 시 고려사항

테스트 커버리지 전략

모든 코드를 100% 테스트하는 것은 비효율적입니다. 핵심 비즈니스 로직과 사용자 흐름을 우선적으로 테스트하는 전략이 효과적입니다.

테스트 환경 구성

테스트 환경은 실제 환경과 최대한 유사하게 구성하되, 테스트의 안정성과 속도를 고려해야 합니다. 모킹(Mocking)과 실제 API 호출의 균형을 맞추는 것이 중요합니다.

유닛 테스트로 기본을 다진 후, 팀이 테스트에 익숙해지면 통합 테스트와 E2E 테스트를 점진적으로 추가하는 것이 좋습니다. 이렇게 하면 무리 없이 테스트 문화를 정착시킬 수 있습니다.

지속적 통합(CI)과의 연계

테스트를 GitHub Actions, Jenkins 등의 CI 도구와 연동하면 코드 변경 시마다 자동으로 테스트를 실행할 수 있습니다. 이를 통해 빠른 피드백 루프를 구축할 수 있습니다.