JUNSEOK
06 · SW 공학·18분·4개 레슨

테스팅

단위·통합·E2E, TDD, 테스트 피라미드.

학습 목표

  • 테스팅 피라미드/트로피의 개념과 FE에서의 균형점을 이해한다
  • Jest/Vitest + Testing Library로 컴포넌트 테스트를 작성할 수 있다
  • MSW로 API를 모킹하고 통합 테스트 환경을 만들 수 있다
  • Playwright/Cypress로 E2E 테스트를 작성할 수 있다
  • 시각적 회귀·접근성·성능 테스트를 CI에 통합할 수 있다
  • **"좋은 테스트가 좋은 설계를 유도한다"**는 원칙을 체화한다

0. "테스트"가 뭔데 — 초심자용

0-1. 자동화된 확인

테스트 = "내 코드가 기대대로 동작하는지" 를 확인하는 코드.

수동 확인:

1. 브라우저 연다
2. 로그인 페이지 간다
3. 이메일 입력한다
4. 비밀번호 입력한다
5. 버튼 누른다
6. 대시보드가 나오는지 본다

자동 확인:

test('로그인 성공 시 대시보드로 이동', async () => {
  await login('a@x.com', 'password');
  expect(location.pathname).toBe('/dashboard');
});

한 번 쓴 테스트는 수천 번 공짜로 다시 확인 가능. 사람이 매번 눌러 확인할 필요 없음.

0-2. 세 종류의 테스트

종류무엇을 테스트특징
단위(Unit)함수 하나formatDate() 정확한가빠름, 많이 작성
통합(Integration)여러 조각의 상호작용컴포넌트 + API 호출중간 속도
E2E (End-to-End)전체 사용자 흐름로그인→주문→결제느림, 적게

이 비율을 어떻게 둘지가 Testing Pyramid / Trophy 이론.

0-3. 왜 테스트를 쓰는가 — 제대로 된 이유

"커버리지 채우려고" 라는 건 잘못된 동기. 진짜 이유:

  1. 회귀 방지: 예전에 고친 버그가 다시 돌아오지 않게
  2. 리팩토링 안전망: 구조 바꿔도 안 깨진다는 증명
  3. 설계 피드백: 테스트하기 어려운 코드 = 설계가 나쁜 코드
  4. 살아있는 문서: "이 함수/컴포넌트 이렇게 쓰는 거구나" 예시
  5. 릴리스 신뢰: 배포 시 무서워하지 않게 됨

0-4. "테스트 안 써도 잘만 돌아가는데?"

  • 사용자가 버그를 먼저 발견 → 신뢰 손상
  • 새 기능 추가 때마다 모든 경로 수동 확인 → 속도 저하
  • 리팩토링 공포 → 기술 부채 누적

개인 토이 프로젝트는 안 써도 된다. 팀·장기 프로젝트는 안 쓰면 죽는다.

0-5. "좋은 테스트"의 원칙 미리보기

  • 내부 구현이 아니라 동작을 테스트 (useState 호출 횟수 ❌, "버튼 누르면 카운트 증가" ✅)
  • 사용자 관점으로 (textContent 검사 ❌, getByRole('button', { name: '저장' }) ✅)
  • 실패 시 원인이 분명해야 한다
  • 빠르고 독립적 이어야 한다

Testing Library 같은 라이브러리가 이런 원칙을 강제한다. 이 장에서 실전 도구를 본다.

0-6. 이 장에서 배우는 도구들

도구역할
Jest / Vitest테스트 실행기 (test runner)
Testing Library컴포넌트 테스트 헬퍼
MSW (Mock Service Worker)API 모킹
Playwright / CypressE2E 브라우저 자동화
Chromatic / Percy시각적 회귀 테스트

0-7. 선행 장 연결

  • 6-4: 순수 함수가 테스트하기 압도적으로 쉬움 — FP 가 테스트를 유도
  • 6-2: SRP 를 따르는 코드가 테스트 용이 — 원칙이 테스트로 검증됨
  • 2-6: 비동기 코드 테스트 시 이벤트 루프 이해 필요

1. 왜 테스트를 쓰는가

1-1. 잘못된 이유

  • "커버리지 채우려고"
  • "방어적으로 모든 경우 검증하려고"
  • "회사가 시켜서"

1-2. 진짜 이유

  • 회귀 방지: 이전에 고친 버그가 다시 돌아오지 않게
  • 리팩토링 안전망: 구조를 바꿔도 동작이 깨지지 않음을 증명
  • 설계 피드백: 테스트가 어려운 코드 = 설계가 나쁜 코드
  • 문서: "이 함수/컴포넌트는 이렇게 쓰이는구나"
  • 릴리스 신뢰: 수동 QA 부담 감소

1-3. "테스트 안 써도 잘만 돌아가는데?"

  • 사용자가 버그를 먼저 발견 → 신뢰 손상
  • 새 기능 추가 때마다 직접 모든 경로를 수동 검증 → 속도 저하
  • 리팩토링 공포 → 기술 부채 누적

2. 테스팅 피라미드 vs 트로피

2-1. 전통 피라미드 (Mike Cohn)

        /\
       /E2E\         ← 적게
      /─────\
     /Integr.\       ← 보통
    /─────────\
   / Unit Tests\     ← 많이
  /─────────────\
  • 빠르고 값싼 Unit이 바닥, 느리고 비싼 E2E가 꼭대기

2-2. Testing Trophy (Kent C. Dodds, 2018)

       🏆
      ───
     / E2E \
    ───────
   /  통합  \       ← 무게 중심
  ─────────
 /  Unit    \
─────────────
    Static
    (타입/린트)

FE 세계에서 "통합 테스트"에 무게를 두는 경향. 이유:

  • UI는 "부품끼리의 상호작용"에서 가장 많은 버그가 발생
  • 모킹 범벅 유닛 테스트는 구현에 의존해 리팩토링을 방해
  • 타입 시스템(TS)이 유닛 수준 검증 상당 부분을 대체

현대 FE 권장 분포 (대략):

  • 정적 분석: 공짜 (TS + ESLint)
  • Unit: 30% (순수 함수, 커스텀 훅)
  • Integration: 50% (컴포넌트 + 상태 + API 모킹)
  • E2E: 20% (핵심 유저 플로우)

3. 도구 지형

카테고리도구
테스트 러너Jest, Vitest (Vite/ESM 네이티브)
DOM 환경jsdom, happy-dom (Vitest 기본)
Component 테스트Testing Library (@testing-library/react 등)
API 모킹MSW (Service Worker 가로채기)
E2EPlaywright, Cypress
시각 회귀Chromatic, Percy, Playwright toHaveScreenshot
컴포넌트 탐색Storybook
테스트 데이터Faker, @mswjs/data

Vitest vs Jest

  • Vitest: Vite 네이티브, ESM 친화적, watch 빠름, TS 설정 없이 바로 동작
  • Jest: 커뮤니티 크고 레퍼런스 풍부, CRA 프로젝트 기본
  • 신규 프로젝트는 Vitest 권장, 기존 Jest도 API 호환 높아 마이그레이션 어렵지 않다

4. Unit Test

4-1. 순수 함수 테스트

// money.ts
export function formatKRW(amount: number): string {
  return `₩${amount.toLocaleString('ko-KR')}`;
}
// money.test.ts
import { describe, it, expect } from 'vitest';
import { formatKRW } from './money';

describe('formatKRW', () => {
  it('formats integers with commas', () => {
    expect(formatKRW(1234567)).toBe('₩1,234,567');
  });

  it('handles zero', () => {
    expect(formatKRW(0)).toBe('₩0');
  });

  it('handles negative numbers', () => {
    expect(formatKRW(-500)).toBe('₩-500');
  });
});

AAA 패턴: Arrange → Act → Assert.

4-2. 커스텀 훅 테스트

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

it('increments count', () => {
  const { result } = renderHook(() => useCounter(0));

  act(() => { result.current.increment(); });

  expect(result.current.count).toBe(1);
});

4-3. 테스트 대상이 불순하면

  • 시간: vi.useFakeTimers() / jest.useFakeTimers()
  • 랜덤: 시드 고정 또는 모킹
  • 네트워크: MSW로 가로챔
  • 스토리지: 테스트마다 localStorage.clear()

5. Testing Library 철학

"Test the way your users use your app." — Kent C. Dodds

구현 세부(클래스명, 내부 state)를 테스트하지 말고, 사용자가 인식하는 것(텍스트, 역할, 라벨)을 테스트하라.

5-1. 좋은 쿼리 우선순위

  1. Accessibility 우선getByRole, getByLabelText, getByPlaceholderText
  2. Text 기반getByText, getByAltText
  3. Form 기반getByDisplayValue
  4. TestIdgetByTestId (최후 수단)
// ❌ 구현 세부 의존 (리팩토링에 취약)
const button = container.querySelector('.btn.btn-primary.submit-btn');

// ✅ 사용자 관점
const button = screen.getByRole('button', { name: /제출/i });

5-2. getBy / queryBy / findBy

접두사없을 때여러 개일 때비동기
getBythrowthrow
queryBynullthrow
findBy대기 후 throwthrow✅ (Promise)
  • 요소가 반드시 있어야 하는 검증: getBy
  • 요소가 없음을 확인: queryBy + toBeNull()
  • 비동기 렌더링 후 등장: findBy

5-3. user-event

fireEvent보다 실제 사용자 상호작용을 더 정확히 시뮬레이트.

import { userEvent } from '@testing-library/user-event';

const user = userEvent.setup();
await user.type(screen.getByLabelText('이메일'), 'a@b.com');
await user.click(screen.getByRole('button', { name: /로그인/ }));

6. Integration Test (컴포넌트 + API 모킹)

6-1. 전통적 실수 — 하드한 모킹

vi.mock('@/api/users', () => ({
  fetchUser: vi.fn(() => Promise.resolve({ name: 'Alice' })),
}));

문제:

  • 네트워크 계층 통째로 우회 → 실제 HTTP 경로, JSON 파싱, 헤더 처리 등 검증 누락
  • 여러 테스트·컴포넌트에 모킹 반복
  • 구현 의존(fetchUser 함수명 변경 시 테스트 깨짐)

6-2. MSW — Service Worker로 HTTP 가로채기

// mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'Alice' });
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 1, ...body }, { status: 201 });
  }),
];
// setup.ts
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';

export const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

6-3. 컴포넌트 + API 통합 테스트

it('shows user name after loading', async () => {
  render(<UserProfile id="1" />);

  // 로딩 상태 먼저
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // API 응답 후
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

6-4. 에러 케이스 오버라이드

it('shows error when API fails', async () => {
  server.use(
    http.get('/api/users/:id', () => new HttpResponse(null, { status: 500 }))
  );

  render(<UserProfile id="1" />);
  expect(await screen.findByText(/error/i)).toBeInTheDocument();
});

6-5. MSW의 장점

  • 브라우저와 Node에서 동일한 핸들러 (dev 모킹 + 테스트 공유)
  • 구현이 fetch든 axios든 상관없음
  • Contract-first 개발 가능 (백엔드 완성 전 프런트가 진행)

7. End-to-End (E2E)

7-1. 도구 선택

항목PlaywrightCypress
브라우저Chromium/Firefox/WebkitChromium, Firefox, WebKit (실험)
병렬 실행기본 지원유료
여러 탭/도메인지원제한적
API 테스트지원제한 (별도 플러그인)
속도빠름보통
문법page.getByRole(...)cy.findByRole(...)
현재 추세▲ 상승정체

7-2. Playwright 예

import { test, expect } from '@playwright/test';

test('사용자가 로그인 후 대시보드를 본다', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('이메일').fill('a@b.com');
  await page.getByLabel('비밀번호').fill('secret');
  await page.getByRole('button', { name: '로그인' }).click();

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: /환영합니다/ })).toBeVisible();
});

7-3. E2E 철학

  • 핵심 플로우만 커버 (로그인, 결제, 가입). 20개 넘어가면 느려지고 부서짐
  • 실제 백엔드 또는 스테이징 환경에 붙는 편이 이상적 (MSW는 유닛/통합용)
  • 데이터 시드테스트 격리(각 테스트가 독립적 사용자/데이터)
  • Flaky 테스트는 빨리 고치거나 삭제 — 신뢰를 잃으면 팀 전체가 무시하기 시작

7-4. Page Object Model

UI 구조를 객체로 래핑해 재사용성·가독성 확보.

class LoginPage {
  constructor(private page: Page) {}

  async goto() { await this.page.goto('/login'); }

  async login(email: string, password: string) {
    await this.page.getByLabel('이메일').fill(email);
    await this.page.getByLabel('비밀번호').fill(password);
    await this.page.getByRole('button', { name: '로그인' }).click();
  }
}

test('login', async ({ page }) => {
  const login = new LoginPage(page);
  await login.goto();
  await login.login('a@b.com', 'secret');
});

8. TDD · BDD · ATDD

8-1. TDD (Test-Driven Development)

Red → Green → Refactor 사이클.

  1. 실패하는 테스트 작성
  2. 통과시키는 최소 코드 작성
  3. 중복·냄새 제거 리팩토링

이점: 설계 피드백, 과잉 구현 방지, 안전망 기반 리팩토링.

FE에서의 현실:

  • 순수 로직(유틸, 훅)은 TDD 잘 맞음
  • 비주얼·인터랙션은 Storybook + 수동 확인 → 테스트 순이 실용적
  • TDD는 "항상 옳다"가 아니라 상황에 맞게

8-2. BDD (Behavior-Driven Development)

Given-When-Then 구조로 비즈니스 관점에서 테스트 서술.

describe('로그인', () => {
  it('유효한 자격이면 대시보드로 이동한다', async () => {
    // Given 로그인 페이지
    // When 올바른 이메일/비밀번호 입력 후 제출
    // Then /dashboard로 이동
  });
});

8-3. ATDD (Acceptance Test-Driven)

팀(개발+QA+기획)이 합의한 수용 조건을 자동화 테스트로. Cucumber(Gherkin)가 대표 도구.


9. 시각적 회귀 (Visual Regression)

CSS 수정으로 예상 못한 UI 변화를 잡는다.

9-1. 스크린샷 비교

// Playwright
await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixels: 100 });

9-2. Storybook + Chromatic

  • Storybook의 각 스토리에 대해 자동으로 스크린샷
  • PR마다 Before/After diff 리뷰 UI 제공
  • 의도한 변경은 승인, 의도치 않은 변경은 차단

9-3. 주의

  • 폰트 렌더링·플랫폼별 차이로 false positive 많음 → 렌더 환경 고정
  • 애니메이션 비활성화 (prefers-reduced-motion 또는 스크린샷 전 대기)
  • 픽셀 임계치 적절히 조정

10. 접근성 테스트 (a11y)

10-1. 자동화

import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

it('is accessible', async () => {
  const { container } = render(<Form />);
  expect(await axe(container)).toHaveNoViolations();
});

한계: 자동 검사로 잡히는 건 30-40%. 키보드 주행·스크린리더 수동 테스트 필수.

10-2. Playwright + axe

import AxeBuilder from '@axe-core/playwright';

test('홈 a11y', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

11. 성능 테스트 (Lighthouse CI)

# .github/workflows/lhci.yml
- uses: treosh/lighthouse-ci-action@v10
  with:
    urls: |
      https://staging.example.com/
    budgetPath: ./lighthouse-budget.json

Budget 초과 시 빌드 실패 → 성능 회귀 방지.


12. 커버리지 함정

12-1. 커버리지는 신뢰 지표가 아니다

function divide(a, b) {
  return a / b;
}

test('divide', () => {
  expect(divide(10, 2)).toBe(5); // 커버리지 100%
  // b=0 케이스는 안 본다
});

커버리지 100%가 버그 없음을 의미하지 않는다. 의미 있는 경계값·에러 케이스를 의도적으로 선별해야 한다.

12-2. 현실적 목표

  • 변화가 잦은 로직: 80%+
  • 안정된 유틸: 90%+
  • 비즈니스 룰: 가능한 한 100%
  • UI 리프 컴포넌트: 낮아도 OK (시각 회귀로 커버)

12-3. Mutation Testing

"테스트가 실제로 버그를 잡나?"를 검증. 소스 코드를 일부러 변경(mutate)해 테스트가 실패하는지 확인. Stryker 같은 도구.

  • ===!==로 바꿨는데 테스트가 통과? → 그 부분은 사실상 테스트 안 된 것.

13. 실전 테스트 작성 팁

13-1. 좋은 테스트 이름

// ❌
it('works');
it('test 1');

// ✅
it('disables the submit button when the form is invalid');
it('shows an error when the email is already taken');

13-2. 한 테스트에 한 주장

// ❌ 여러 개를 한꺼번에
it('form', () => {
  expect(...).toBe(...);
  expect(...).toBe(...);
  expect(...).toBe(...);
});

// ✅ 각 시나리오 분리
it('shows loading state', ...)
it('shows result after loading', ...)
it('shows error on failure', ...)

13-3. 실제 구현 테스트 금지

// ❌ 내부 함수 이름에 의존
expect(componentInstance._calculateTotal).toHaveBeenCalled();

// ✅ 관찰 가능한 결과
expect(screen.getByText('합계: ₩3,000')).toBeInTheDocument();

13-4. Flaky 테스트 제거

  • 고정 sleep(setTimeout(..., 2000)) 대신 findBy + 대기
  • 타임존·날짜는 고정 타임존·mock 시간 사용
  • 공유 상태(localStorage, module-level 변수) 각 테스트마다 초기화

14. 실무 체크리스트

  • 정적 분석(TS, ESLint)이 기본 안전망으로 동작하는가
  • 순수 함수/훅은 유닛으로, 컴포넌트는 Testing Library + MSW 통합으로 검증하는가
  • testid 남발 대신 role/label 쿼리를 우선 쓰는가
  • MSW 핸들러가 dev 모킹과 테스트 양쪽에서 재사용되는가
  • E2E는 핵심 플로우만(로그인, 결제, 가입) 커버하는가
  • Flaky 테스트 발생 시 즉시 고치거나 삭제하는가
  • 시각 회귀 / a11y / 성능을 CI에 통합했는가
  • 커버리지 수치에 현혹되지 않고 의미 있는 케이스를 보장하는가

15. 연습 문제

Q1. Testing Trophy가 전통 Pyramid와 다른 점을 설명하고 FE에서 Trophy가 선호되는 이유를 들어라.

정답

Pyramid는 Unit이 바닥·제일 많고 E2E가 꼭대기, Trophy는 Integration이 중앙 무게 중심이다. Trophy가 FE에서 선호되는 이유:

  1. UI 버그는 부품 간 상호작용에서 가장 많이 난다 (Unit만으론 안 잡힘).
  2. 과도한 모킹 Unit 테스트는 구현 세부에 결합해 리팩토링을 방해.
  3. TS/린트 같은 정적 분석이 Unit 수준 검증 상당 부분을 공짜로 해 준다.
  4. MSW 같은 도구로 통합 테스트가 이전만큼 느리거나 복잡하지 않다.

그래서 "타입+린트 → 통합 위주 → 핵심만 E2E" 구성이 실무 표준이 됐다.

Q2. Testing Library의 쿼리 우선순위(role > label > text > testid)를 지켜야 하는 이유를 설명하라.

정답

이 순서는 사용자·스크린리더가 요소를 인식하는 순서와 같다. getByRole('button', { name: '제출' })로 작성하면:

  1. 접근성 트리 기반이므로 a11y 구현이 깨지면 테스트도 깨진다 → 회귀 방지
  2. 스크린리더 사용자와 동일한 관점으로 UI 탐색 → 구현 세부(className, id) 변경에 강건
  3. testid는 프로덕션 코드에 테스트 전용 속성을 남기는 오염이고, 구현 디테일에 결합함

testid는 고유 식별이 정말 필요할 때(예: 목록 아이템 구분)만 최후의 수단으로.

Q3. MSW를 쓰는 것이 vi.mock('@/api/...') 방식보다 유리한 점을 3가지 들어라.

정답
  1. 네트워크 계층 포함: fetch/axios의 실제 호출, 직렬화, 헤더, 상태 코드 처리까지 검증. 잘못된 URL·메서드·바디도 잡힘.
  2. 구현 독립: 컴포넌트가 fetch 쓰든 axios 쓰든 React Query 쓰든 같은 핸들러가 동작. API 호출 위치를 리팩토링해도 테스트 영향 없음.
  3. 개발 환경과 공유: 같은 핸들러로 npm run dev에서 백엔드 없이 개발 가능. 테스트·로컬 개발의 모킹 소스가 일원화됨.

추가: msw 2.x는 Node + 브라우저 공용 API, TS 타입 자동 추론 등 개선.

Q4. 다음 테스트의 문제를 지적하라.

it('increments counter', () => {
  const { container } = render(<Counter />);
  const btn = container.querySelector('.counter-btn');
  fireEvent.click(btn!);
  const display = container.querySelector('.counter-display');
  expect(display?.textContent).toBe('1');
});
정답

문제:

  1. 클래스명(구현 세부)에 의존 — CSS 리팩토링으로 클래스명이 바뀌면 테스트가 깨짐. getByRole('button', { name: /증가/ }), getByText('1') 등 사용.
  2. fireEvent 대신 userEvent 선호 — 실제 사용자 상호작용(focus, keydown, click 순서) 시뮬레이션에 더 가까움.
  3. ! non-null 단언 — 요소가 없으면 런타임에 애매하게 실패. getByRole은 없으면 자동으로 명확한 에러.
  4. textContent 비교 — 부분 일치·정규식 쓰는 toHaveTextContent('1')가 더 안전.

개선:

const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /증가/ }));
expect(screen.getByRole('status')).toHaveTextContent('1');

Q5. E2E 테스트에서 "핵심 플로우만 커버하라"는 지침이 있는 이유를 설명하라.

정답
  1. 속도: E2E는 실제 브라우저·네트워크·DB까지 구동해 유닛/통합 대비 10-100배 느림. 100개 넘는 E2E는 CI 시간 폭증.
  2. Flakiness: 네트워크 지연, 타이밍, 외부 서비스 상태 등으로 간헐적 실패가 많음. 많을수록 신뢰 붕괴 → 팀이 결과를 무시.
  3. 유지보수 비용: UI 한 번 바뀌면 여러 E2E가 한꺼번에 깨짐. 커버 범위가 넓을수록 수선 비용 기하급수.
  4. ROI: 로그인·결제 같은 핵심 경로가 깨지면 서비스 전체 불능. 자잘한 상호작용은 통합 테스트로 잡으면 충분.

원칙: 핵심 유저 저니(user journey) 5-10개로 시작, 결코 100개를 목표로 두지 않는다.

Q6. 테스트 커버리지가 100%인데도 버그가 나는 시나리오를 예로 들어라.

정답
function divide(a, b) { return a / b; }
test('divides', () => { expect(divide(10, 2)).toBe(5); }); // 100% 라인/브랜치
  • b = 0Infinity 반환, 호출자는 NaN 추적에 시간 소비
  • a = 0.1, b = 0.2 → 부동소수 오차
  • a = Number.MAX_SAFE_INTEGER, b = 1 → 안전 범위 초과

커버리지는 **"코드가 한 번이라도 실행됐나"**만 잰다. 경계값·에러 경로·입력 다양성은 보지 않는다. Mutation Testing으로 "테스트를 통과시키는 최소 변경이 가능한가"를 측정하면 더 현실적이다.

Q7. Flaky(간헐적으로 실패하는) 테스트를 고치는 일반적 전략을 3가지 들어라.

정답
  1. 고정 sleep 제거, 조건 대기로 대체: setTimeout(done, 3000)findBy* 또는 waitFor(() => ...). 실제 조건이 만족될 때까지 대기하므로 타이밍 경쟁 회피.
  2. 환경 결정화: 시스템 시간·타임존·랜덤을 고정 (vi.useFakeTimers, Intl.DateTimeFormat locale 고정, vi.spyOn(Math, 'random')).
  3. 테스트 간 상태 격리: 각 테스트마다 localStorage.clear(), MSW resetHandlers, DB 시드 초기화. 테스트 실행 순서 의존성 제거.

추가:

  • 애니메이션 끄기 (prefers-reduced-motion 또는 CSS 주입)
  • 외부 서비스 모킹 (E2E에선 외부 3rd party 의존을 최대한 stub)
  • retries는 최후 수단 — 근본 원인 숨기고 신뢰 갉아먹음.

16. 정리

  • 테스트는 회귀 방지·리팩토링 안전망·설계 피드백·문서의 역할을 한다.
  • FE는 Testing Trophy — 정적 분석 + 통합 위주 + 핵심만 E2E가 실무 표준.
  • Testing Library + MSW 조합이 컴포넌트 통합 테스트의 표준이 됐다.
  • user-event, findBy, role 쿼리로 사용자 관점을 지킨다.
  • Playwright가 E2E의 현실적 선택, 핵심 플로우만 커버한다.
  • 시각 회귀·a11y·성능도 자동화해 CI에서 차단한다.
  • 커버리지 수치에 속지 말고 의미 있는 경계·에러 케이스를 설계한다.
  • Flaky 테스트는 적이다 — 즉시 고치거나 삭제.

← 6-4. 함수형 프로그래밍 | 6-6. 버전 관리 (Git) →

진도 체크시작 전
NEXT · 6-6

버전 관리 Git

Git 내부 구조, 브랜치 전략, rebase vs merge.

이어서 학습하기 →