다크 모드
Chapter 14. 실전 Vitest와 MSW(Mock Service Worker) 테스트
"1장의 초난감 jQuery 스파게티 코드에서 시작해 13장까지 진화해 온 우리의 단일 게시판 코드베이스. 이제 자동화된 테스트 방어막을 치고 프로덕션 배포의 깃발을 꽂자."
14.1 우리가 걸어온 단일 코드의 진화 여정
우리가 1장에서 마주했던 코드는 사내 인트라넷의 낡은 **legacy-board.js**였습니다:
- Ch 01: XSS 취약점과 DOM 수동 갱신의 고통을 겪고
BoardApp.vue로 1차 선언적 전환 - Ch 02: Proxy 반응형 엔진을 파헤치고
ref와reactive표준 정립 - Ch 03: 상세 보기 추가 중 겪은 반응성 유실을
toRefs와 Vue 3.5 표준으로 해결 - Ch 04~05: 800줄로 비대해진 Fat Component에서
usePagination,usePostFilterComposable 도려내기 - Ch 06: 무한 계층형 대댓글 트리의 Props Drilling을
Provide / Inject (DI)로 격리 - Ch 07: Spring Boot 백엔드 DTO와 1:1 매핑되는 TypeScript 엄격한 도메인 계약 체결
- Ch 08: 템플릿의
<table>마크업을 사내 공통 제네릭BoardTable<T>로 모듈화 - Ch 09~10: 상태를 Pinia로 승격하여 뒤로가기 캐싱과 '좋아요' 낙관적 업데이트 적용
- Ch 11: 라우터 네비게이션 가드(AOP)로 비로그인자 차단 및 리다이렉트 체인 구축
- Ch 12: 컴포넌트의 Axios 직접 호출을
postRepository와 토큰 자동 갱신 AOP로 격리 - Ch 13: 10,000개 댓글 폭주를 60줄
VirtualList가상 스크롤로 60fps 최적화
이제 이 거대한 엔터프라이즈 코드베이스를 운영 서버에 배포하기 전, Vitest와 MSW를 통한 자동화 테스트로 무결성을 증명할 차례입니다.
14.2 [1단계] MSW로 Spring Boot 백엔드 가상화
Spring Boot 서버를 로컬에 띄우지 않아도 프론트엔드 테스트가 100% 독립적으로 성공할 수 있도록 네트워크 모의 환경을 만듭니다.
[코드 14-1] tests/mocks/handlers.ts
typescript
import { http, HttpResponse } from 'msw';
const mockPosts = [
{ id: 1, title: '사내 기술 아키텍처 가이드', author: '최시니어', createdAt: '2026-10-27', viewCount: 15, likeCount: 5 },
{ id: 2, title: 'Vue 3 점진적 리팩터링 후기', author: '김개발', createdAt: '2026-10-27', viewCount: 8, likeCount: 2 }
];
export const handlers = [
// GET /api/posts 가로채기
http.get('/api/posts', () => {
return HttpResponse.json({
items: mockPosts,
total: mockPosts.length,
page: 1,
size: 10
});
}),
// POST /api/posts 등록 가로채기
http.post('/api/posts', async ({ request }) => {
const body = (await request.json()) as any;
const newPost = {
id: 3,
...body,
createdAt: '2026-10-27',
viewCount: 0,
likeCount: 0
};
return HttpResponse.json(newPost, { status: 201 });
})
];14.3 [2단계] 5장에서 분리했던 usePagination 단위 테스트
5장에서 BoardApp.vue로부터 도려냈던 usePagination.ts를 단독 검증합니다.
UI 없이 순수한 반응형 상태와 계산 로직만 존재하므로 0.01초 만에 초고속으로 검증됩니다.
[코드 14-2] tests/unit/usePagination.test.ts
typescript
import { describe, it, expect, vi } from 'vitest';
import { ref } from 'vue';
import { usePagination } from '@/composables/usePagination';
describe('usePagination Composable 단위 테스트', () => {
it('총 개수와 페이지 크기에 따라 올바른 전체 페이지 수를 계산해야 한다', () => {
const totalCount = ref(25);
const { totalPages, hasNextPage, hasPrevPage } = usePagination({
totalCount,
initialSize: 10
});
// 25개 / 10 = 3페이지
expect(totalPages.value).toBe(3);
expect(hasNextPage.value).toBe(true);
expect(hasPrevPage.value).toBe(false);
});
it('nextPage 호출 시 페이지가 1 증가하고 콜백이 실행되어야 한다', () => {
const totalCount = ref(25);
const onPageChangeMock = vi.fn();
const { currentPage, nextPage } = usePagination({
totalCount,
initialSize: 10,
onPageChange: onPageChangeMock
});
nextPage();
expect(currentPage.value).toBe(2);
expect(onPageChangeMock).toHaveBeenCalledWith(2);
});
});14.4 [3단계] 최종 게시판 컴포넌트 통합 시나리오 테스트
화면에서 사용자가 폼에 글을 쓰고 [등록] 버튼을 눌렀을 때, 컴포넌트가 올바르게 목록 상단에 새 글을 꽂아 넣는지 전체 통합 흐름을 검증합니다.
[코드 14-3] tests/integration/BoardApp.test.ts
typescript
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { setupServer } from 'msw/node';
import { handlers } from '../mocks/handlers';
import BoardApp from '../../src/BoardApp.vue';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('BoardApp 통합 시나리오 테스트', () => {
it('마운트 시 MSW 목 서버에서 게시글 2건을 가져와 테이블에 렌더링해야 한다', async () => {
const wrapper = mount(BoardApp);
await flushPromises();
const rows = wrapper.findAll('tbody tr');
expect(rows.length).toBeGreaterThanOrEqual(2);
expect(rows[0].text()).toContain('사내 기술 아키텍처 가이드');
});
it('새 글을 입력하고 등록하면 목록 상단에 즉시 새 글이 반영되어야 한다', async () => {
const wrapper = mount(BoardApp);
await flushPromises();
const titleInput = wrapper.find('input[placeholder*="제목"]');
const authorInput = wrapper.find('.author-input');
const form = wrapper.find('form');
await titleInput.setValue('Vitest 통합 테스트 완료 글');
await authorInput.setValue('이아키텍트');
await form.trigger('submit.prevent');
await flushPromises();
const rows = wrapper.findAll('tbody tr');
expect(rows[0].text()).toContain('Vitest 통합 테스트 완료 글');
});
});터미널에서 npm run test를 치면, 불과 1초도 안 되는 시간에 14개 챕터에 걸쳐 빌드업한 모든 기능이 초록색 불(PASS)을 뿜어냅니다!
14.5 에필로그: 진정한 프론트엔드 엔지니어로의 탄생
여러분은 1장의 그 냄새나던 jQuery 스파게티 코드 단 하나를 포기하지 않고 끝까지 붙잡았습니다.
새로운 문법이 나올 때마다 다른 장난감 예제로 갈아타지 않고, 동일한 하나의 게시판 코드베이스를 14번 뜯어고치며 엔터프라이즈 아키텍처의 필연성을 손끝으로 증명해 냈습니다.
이제 여러분은 단순히 주어진 화면을 찍어내는 코더(Coder)가 아닙니다.
**"기술의 원리를 꿰뚫고, 지속 가능한 아키텍처를 설계하며, 사내 동료들의 코드 품질을 상향 평준화하는 진정한 소프트웨어 엔지니어"**입니다.