co-cherry
테스트 전략 본문
"You should write tests if you value your time.
Much better to catch a bug locally from the tests
than getting a call at 2:00 in the morning and fix it then."
— Kent C. Dodds
버그는 발견이 늦을수록 비용이 기하급수적으로 증가한다.
설계 단계 대비 프로덕션에서는 최대 100배, CrowdStrike처럼 한 줄이 80억 달러로 이어질 수도 있다.
테스트는 "만일의 보험"이 아니라 개발 속도를 높이는 투자다.
Viest + React Testing Library + MSW로 프론트엔드 테스트 전략을 완성하는 법을 알아보자.
테스트가 왜 필요할까?
테스트가 없는 코드는 단순히 "버그가 있을 수 있는 코드"가 아니라 변경할 수 없는 코드다.
기도하는 배포
우아한 형제들 기술 블로그에 나온 표현이다.
테스트 코드 없이 레거시 코드를 고치면 다음과 같은 과정을 거친다.
- 소스 분석하고
- 변경하고
- 수동으로 기능 확인하고
- 배포하고
- 기도한다 🙏
왜? 내가 건드린 게 어디를 망가뜨렸을지 모르니까...
| 상황 | 테스트가 없을 때 | 테스트가 있을 때 |
| 새 기능 추가 | "이거 건드리면 뭐가 깨질까?" 전전긍긍 | CI가 깨지면 바로 알림 |
| 리팩토링 | 무서워서 못 함 → 기술부채 누적 | 자유롭게 구조 개선 |
| 라이브러리 업데이트 | 수동 회귀 테스트 → 결국 안 함 | npm update, npm test 면 끝 |
| 신규 팀원 온보딩 | 코드 동작을 추측만 가능 | 테스트 = 실행 가능한 문서 |
단위 테스트 VS 통합 테스트
테스트란?
프로그램이 요구사항에 맞춰 제대로 동작하는지 검증하는 행위
테스트는 범위에 따라 단위 테스트 → 통합 테스트 → E2E 테스트로 구분한다.
단일 테스트(Unit Test)
소프트웨어의 가장 작은 단위(함수, 메소드, 컴포넌트)가 의도대로 작동하는지 검증
개별 모듈의 기능 요구사항을 만족하는지 확인한다.
- 작은 부분만 테스트하므로 빠르게 실행
- 개발 초기 단계에서 버그 발견 및 수정에 효과적
- 레이어 단위 또는 특정 클래스 단위로 테스트
보통 프론트엔드에서는 Jest, React Testing Library 를, 백엔드에서는 JUnit을 사용한다.
통합 테스트(Integration Test)
여러 단위/컴포넌트 간의 상호작용을 테스트
개별 단위가 시스템 전체에서도 올바르게 동작하는지 확인한다.
- 외부 라이브러리, 데이터베이스 등 개발자가 변경할 수 없는 부분까지 포함
- 독립된 2개 이상의 모듈이 함께 동작할 때 테스트
- 모듈 간 연결에서 발생하는 에러 검증
- 단위 테스트보다 실행 시간이 길고 복잡함
효과적인 테스트 작성 방법
AAA 패턴 (Arrange-Act-Assert)
모든 테스트를 준비, 실행, 검증 세 부분으로 나누는 구조
단순하고 균일한 구조로 일관성을 확보할 수 있다.
- 준비(Arrange) 테스트 대상의 초기 상태 설정
- 실행(Act) 사용자 상호작용 또는 함수 호출 실행
- 확인(Assert) 기대한 결과가 나왔는지 검증
Given-When-Then 패턴
- Given 준비 구절에 해당
- When 실행 구절에 해당
- Then 검증 구절에 해당
기능적으로는 AAA와 동일하나, 비개발자에게 더 읽기 쉬운 표현
BDD(Behavior-Driven Development) 스타일에서 주로 사용
언제 어떤 테스트를 사용할까?
테스팅 피라미드 (Mike Cohn)

- 단위 테스트 > 통합 테스트 > E2E 테스트 순으로 많이 작성
- 테스트 커버리지 최대화 + 시간/리소스 최소화
- 낮은 단계일수록 아이디어와 구현 간극이 짧아 안정화 필요
테스팅 트로피/다이아몬드 (Guillermo Rauch)

- 통합 테스트에 가장 큰 비중
- 통합 테스트 > 단위 테스트 > E2E 테스트 순
- 정적 테스트의 추가로 에러 사전 발견
vitest 와 React Testing Library 실습해보기
https://white-blank.tistory.com/186
vitest + React Testing Library 사용하여 테스트 케이스 만들기
해당 글에서는 vitest 와 React Testing Library 를 이용하여 테스트 케이스를 만드는 방법을 소개합니다.vitest 는 실행속도와 vite 와 통합이 간편하며, RTL 은 리액트 컴포넌트를 테스트 하기 위한 도구
white-blank.tistory.com
↑ 위 블로그 과정을 보고 따라 해보았다.
먼저 필요한 라이브러리를 설치한다.
pnpm add -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom
그리고 vite.config.ts 를 수정한다.
- globals: true 테스트 파일에서 전역 함수를 import 없이 사용할 수 있게 설정
- environment: 'jsdom' 테스트 환경을 JSDOM(Node.js 환경에서 브라우저와 유사한 DOM 환경을 시뮬레이션)으로 설정
- setupFiles: './src/setupTests.ts' 테스트 실행 전, 실행될 설정 파일의 경로 지정
/// <reference types="vitest" />
// https://vite.dev/config/
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
},
})
그 다음, 위에서 설정한 setup 파일을 해당 경로에 생성한다.
import '@testing-library/jest-dom'
tsconfig.app.json 에서 types 배열에 vitest/globals와 @testing-library/jest-dom을 추가해
TypeScript가 전역 API를 인식하게 한다.
{
"compilerOptions": {
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"]
}
}
마지막으로, package.json 에 스크립트를 추가하면 설정은 완료된다.
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
테스트 파일을 작성해야 하는데 나의 경우는 MovieCard 파일에 대한 테스트를 작성했다.
파일명은 컴포넌트명.test.tsx 의 규칙으로 생성하며 컴포넌트 파일 옆에 두는 것이 일반적이다.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import MovieCard from './MovieCard'
import type { Movie } from '../model/types'
const baseMovie: Movie = {
id: 1,
title: '인터스텔라',
overview: '우주 탐험 영화',
poster_path: '/poster.jpg',
release_date: '2014-11-06',
vote_average: 8.6,
popularity: 100,
adult: false,
original_language: 'en',
original_title: 'Interstellar',
}
describe('MovieCard', () => {
it('영화 제목이 렌더링된다', () => {
render(<MovieCard movie={baseMovie} onClick={() => {}} />)
expect(screen.getByText('인터스텔라')).toBeInTheDocument()
})
it('포스터 이미지가 렌더링된다', () => {
render(<MovieCard movie={baseMovie} onClick={() => {}} />)
const img = screen.getByAltText('인터스텔라')
expect(img).toBeInTheDocument()
})
it('poster_path가 없으면 No Image 텍스트가 보인다', () => {
render(<MovieCard movie={{ ...baseMovie, poster_path: null }} onClick={() => {}} />)
expect(screen.getByText('No Image')).toBeInTheDocument()
})
it('adult가 true이면 19 배지가 보인다', () => {
render(<MovieCard movie={{ ...baseMovie, adult: true }} onClick={() => {}} />)
expect(screen.getByText('19')).toBeInTheDocument()
})
it('adult가 false이면 19 배지가 없다', () => {
render(<MovieCard movie={baseMovie} onClick={() => {}} />)
expect(screen.queryByText('19')).not.toBeInTheDocument()
})
it('카드 클릭 시 movie.id를 인자로 onClick이 호출된다', async () => {
const handleClick = vi.fn()
render(<MovieCard movie={baseMovie} onClick={handleClick} />)
await userEvent.click(screen.getByText('인터스텔라'))
expect(handleClick).toHaveBeenCalledWith(1)
})
it('예매하기 버튼이 렌더링된다', () => {
render(<MovieCard movie={baseMovie} onClick={() => {}} />)
expect(screen.getByRole('button', { name: '예매하기' })).toBeInTheDocument()
})
})
pnpm test로 테스트를 실행하면 아래와 같이 테스트 결과가 나온다.

MSW(Mock Service Worker)를 활용한 API Mocking
https://mswjs.io/docs/quick-start
Quick start
Get MSW up and running in under five minutes.
mswjs.io
API Mocking
실제 백엔드 서버 없이 가짜 API 응답을 만들어내는 것
요청이 가기 전에 중간에서 가로채서 미리 만든 가짜 데이터를 응답으로 돌려준다.
- 백엔드가 아직 없을 때 프론트 먼저 개발 가능
- 테스트 할 때 실제 서버에 의존하지 않아도 됨
- 에러 상황, 로딩 상황 등을 마음대로 재현 가능
MSW(Mock Service Worker)
서비스 워커 기술을 활용하여 네트워크 레벨에서 API 요청을 가로채고 모킹할 수 있는 라이브러리
쉽게 말해, 실제 네트워크가 이루어지는 것처럼 프론트엔드의 응답값을 주는 라이브러리이다. (가짜 API 생성)
- 별도 Mocking 서버를 구축할 필요 없음
- 프레임워크과 라이브러리에 종속되지 않음
- 브라우저, Node.js 등 다양한 환경 지원
- 기존 코드 수정 없이 사용 가능
세팅해보기
1. MSW 설치
pnpm add -D msw
2. Service Worker 파일 생성하기
public/mockServiceWorker.js가 생성되고 package.json에 msw.workerDirectory 항목이 자동으로 추가된다.
npx msw init public/ --save
3. 가짜 데이터 파일 작성하기
src/mocks/data/ 위치에 가짜 데이터 파일을 생성한다.
실제 API가 반환값과 같은 형태의 데이터를 타입에 맞게 작성해야 한다.
나의 경우, 영화 데이터를 넣었다.
import type { Movie, MovieListResponse, MovieDetail } from '@/entities/movie/model/types'
export const mockMovies: Movie[] = [
{
id: 1,
title: '인터스텔라',
overview: '우주를 배경으로 한 SF 영화',
poster_path: '/poster1.jpg',
release_date: '2014-11-06',
vote_average: 8.6,
popularity: 100,
adult: false,
original_language: 'en',
original_title: 'Interstellar',
},
{
id: 2,
title: '기생충',
overview: '계층 간의 갈등을 다룬 한국 영화',
poster_path: '/poster2.jpg',
release_date: '2019-05-30',
vote_average: 8.5,
popularity: 90,
adult: false,
original_language: 'ko',
original_title: '기생충',
},
]
export const mockMovieListResponse: MovieListResponse = {
page: 1,
results: mockMovies,
total_pages: 1,
total_results: 2,
}
export const mockMovieDetail: MovieDetail = {
...mockMovies[0],
genres: [{ id: 878, name: 'SF' }],
runtime: 169,
tagline: '우주의 끝에서 답을 찾다',
status: 'Released',
vote_count: 30000,
backdrop_path: '/backdrop1.jpg',
}
4. 핸들러 파일 작성하기
어떤 URL로 요청이 오면 어떤 데이터를 응답할지 규칙을 정의한다.
import { http, HttpResponse } from 'msw'
import { mockMovieListResponse, mockMovieDetail } from './data/movies'
const BASE = 'https://api.themoviedb.org/3'
export const handlers = [
http.get(`${BASE}/movie/popular`, () => {
return HttpResponse.json(mockMovieListResponse)
}),
http.get(`${BASE}/movie/now_playing`, () => {
return HttpResponse.json(mockMovieListResponse)
}),
http.get(`${BASE}/movie/top_rated`, () => {
return HttpResponse.json(mockMovieListResponse)
}),
http.get(`${BASE}/movie/upcoming`, () => {
return HttpResponse.json(mockMovieListResponse)
}),
http.get(`${BASE}/movie/:movieId`, () => {
return HttpResponse.json(mockMovieDetail)
}),
]
5. 브라우저용 설정 파일 생성하기
브라우저는 테스트와 달리, Service Worker를 사용한다.
[React 앱] → fetch 요청 → [Service Worker가 가로챔] → 가짜 응답 반환 의 순서로 실행된다.
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
6. 테스트용 설정 파일 생성하기
테스트 환경(Vitest)은 브라우저가 아닌 Node.js에서 실행된다.
Node.js에는 Service Worker가 없으므로 MSW가 대신 http 모듈을 가로채는 방식으로 작동된다.
[테스트 코드] → fetch 요청 → [MSW Node 서버가 가로챔] → 가짜 응답 반환 의 순서로 실행된다
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
7. 테스트 환경에 MSW 연결하기
이전에 설정한 setup 파일에 추가적으로 설정한다.
- beforeAll(() => server.listen()) 테스트 시작 전 서버 켜기
- afterEach(() => server.resetHandlers()) 각 테스트 후 핸들러 초기화
- afterAll(() => server.close()) 테스트 끝나면 서버 끄기
import '@testing-library/jest-dom'
import { server } from './mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
8. 앱 진입점에 MSW 연결하기
main.tsx 에서 개발 환경에서만 MSW가 켜지도록 enableMocking()으로 감싼다.
- onUnhandledRequest: 'bypass' mock 등록 안 한 요청은 실제 서버로 그냥 통과시킨다.
async function enableMocking() {
if (import.meta.env.DEV) {
const { worker } = await import('./mocks/browser')
return worker.start({ onUnhandledRequest: 'bypass' })
}
}
enableMocking().then(() => {
createRoot(document.getElementById('root')!).render(...)
})
pnpm dev 를 통해 실행하면, 이렇게 설정한 가짜 데이터가 응답으로 나오는 것을 볼 수 있다.

https://techblog.woowahan.com/2613/
테스트 코드 없이 레거시 코드를 다 감수하시겠습니까? | 우아한형제들 기술블로그
부서 이동을 하다 2018년 말미, 결제/정산 파트에서 주문중계 파트로 부서 이동하게 되었습니다. 인사 발령을 받고 나서 팀 이동을 하게 되면 누구나 직면하게 되는 상황이 발생하는데요. 그것은
techblog.woowahan.com
https://velog.io/@leeseonseonje/%EB%8B%A8%EC%9C%84%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B5%AC%EC%A1%B0
단위 테스트 AAA 패턴
AAA 패턴 AAA 패턴은 각 테스트를 준비(arrange), 실행(act), 검증(assert)이라는 세부분으로 나눌 수 있다. 모든 테스트가 단순하고 균일한 구조를 갖는데 도움이 된다. (일관성) 테스트를 쉽게 읽고, 이
velog.io
단위 테스트와 통합 테스트
단위 테스트 단위 테스트는 소프트웨어 개발 과정에서 가장 작은 단위의 코드, 예를 들어 각 컴포넌트 혹은 함수가 기대한 대로 작동하며 그 기능 요구사하을 만족시키는지 검증하는 것이다. 프
velog.io
테스트란? (단위테스트, 통합테스트, E2E테스트)
테스트란 소프트웨어의 관점에서 정의하면프로그램을 실행하는 경우에 요구 사항에 맞춰 동작하는지 검증하는 행위 라고 할 수 있다.테스트에는 다양한 종류가 있는데, 보통 범위에 따라서 단
velog.io
MSW(Mock Service Worker)를 이용한 API mocking
프론트 개발을 하던 와중에 API 개발속도와 맞지 않아 동시에 개발이 진행되게 되어 /public경로에 실제 API response의 json key-value값을 맞춰서 mock data를 만들어 mocking하는 식으로 UI를 만들었다. 그러
velog.io
MSW로 프론트엔드 개발 프로세스 개선하기 : API Mocking
[kt cloud 플랫폼Innovation팀 송재희 님] MSW로 프론트엔드 개발 프로세스 개선하기 : API Mocking 프론트엔드 개발자라면, 종종 백엔드 API가 준비되기 전까지 대기해야 하는 상황을 경험해 보셨을 겁니
tech.ktcloud.com
'React' 카테고리의 다른 글
| 폴더 구조와 아키텍처: 프로젝트 구조 개선 (0) | 2026.05.03 |
|---|---|
| 에러 핸들링과 안전성 (0) | 2026.04.26 |
| useMemo, useCallback을 활용한 상세 페이지 구현하기 (w. TMDB) (0) | 2026.04.12 |
| Tanstack query 활용한 무한 스크롤 페이지 (w.TMDB) (0) | 2026.03.29 |
| TanStack Query (0) | 2026.03.27 |
