Testing Collaboration
3줄 요약
- 프론트 테스트는 구현 세부를 고정하는 것이 아니라 사용자가 보는 행동과 API 계약을 지키기 위한 안전망이다.
- unit, component, e2e 테스트는 각각 순수 로직, UI 상호작용, 실제 사용자 흐름을 검증한다.
- 백엔드와의 협업은 OpenAPI 문서만이 아니라 mock server, error code, nullable, pagination, 날짜/타임존 계약까지 포함해야 한다.
핵심 정리
- unit test는 pure function, mapper, formatter, validation helper에 적합하다.
- component test는 React Testing Library로 사용자가 클릭하고 입력하는 흐름을 검증한다.
- e2e test는 Playwright로 로그인, 생성, 수정, 삭제 같은 핵심 smoke flow만 얇게 가져간다.
- MSW는 브라우저와 Node 테스트 환경에서 API를 네트워크 계층에 가깝게 mock해 프론트 테스트를 실제 API 서버에 덜 의존하게 한다.
- 협업 계약은 성공 응답보다 실패 응답, nullable, enum 확장, pagination에서 더 중요하다.
백엔드 개발자가 헷갈리는 지점
- Swagger/OpenAPI가 있으면 협업이 끝났다고 생각하기 쉽다.
- 판단 기준은 실제 예시 응답, 에러 응답, nullable, mock이 맞는지다.
- 문서와 실제 응답이 다르면 프론트 테스트가 신뢰를 잃는다.
- e2e 테스트를 많이 만들면 안정성이 높아진다고 생각하기 쉽다.
- 판단 기준은 e2e는 느리고 취약하므로 핵심 흐름을 얇게 가져가는 것이다.
- 많은 분기는 component test와 unit test로 내린다.
- mock이 실제 API와 다를 수 있다는 점을 놓치기 쉽다.
- 판단 기준은 mock을 OpenAPI나 실제 fixture에서 생성하거나 주기적으로 검증하는 것이다.
개인 프로젝트 기준
- formatter, mapper, schema는 unit test로 둔다.
- form submit, validation error, empty/error state는 component test로 둔다.
- 로그인 후 핵심 CRUD 하나는 Playwright smoke test로 둔다.
- 협업 관련 판단은 OpenAPI만 있으면 충분한가?를 본다.
확인 질문
- React Testing Library의 테스트 관점은 무엇인가?
- 사용자가 보는 화면과 상호작용 중심으로 테스트하는 것이다.
- MSW를 쓰는 이유는 무엇인가?
- API 요청을 네트워크 계층에서 mock해 컴포넌트 테스트를 실제 API 서버와 분리하기 위해서다.
- e2e 테스트는 어디에 쓰는 것이 좋은가?
- 로그인, 핵심 생성/수정/삭제 같은 사용자 smoke flow에 얇게 쓰는 것이 좋다.
01. 테스트 협업 디버깅 상세
이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- unit, component, e2e 테스트는 각각 무엇을 검증해야 하는가?
- MSW는 프론트-백엔드 협업에서 어떤 역할을 하는가?
- 프론트 개발자가 백엔드 API에서 답답해하는 지점은 무엇인가?
개요
프론트 테스트는 “컴포넌트가 특정 div를 렌더링한다”를 확인하는 일이 아니다. 사용자가 입력하고 클릭했을 때 화면이 어떤 상태를 보여주는지, API 실패를 어떻게 복구하는지, 백엔드 계약이 바뀌었을 때 어디가 깨지는지 알려주는 안전망이다. 테스트가 사용자의 언어와 API 계약의 언어를 함께 쓰면 리팩터링에도 오래 버틴다.
원리
테스트 피라미드 관점에서 순수 로직은 unit test, UI 상호작용은 component test, 실제 브라우저 통합 흐름은 e2e test가 맡는다. 프론트는 백엔드 API에 의존하므로 component test에서 실제 API 서버를 띄우기보다 MSW로 요청을 가로채고 응답을 제어하는 방식이 유용하다. 다만 mock이 실제 API와 달라지면 테스트는 안심이 아니라 착각이 되므로 계약과 fixture를 관리해야 한다.
코드로 보는 핵심 예시
React Testing Library와 MSW로 validation error를 검증한다.
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
const server = setupServer(
http.post("/api/signup", () =>
HttpResponse.json(
{
message: "Validation failed",
fieldErrors: {
email: ["이미 사용 중인 이메일입니다."],
},
},
{ status: 400 },
),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test("서버 field error를 이메일 input 아래에 표시한다", async () => {
const user = userEvent.setup();
render(<SignupFormView />);
await user.type(screen.getByLabelText("이메일"), "taken@example.com");
await user.type(screen.getByLabelText("비밀번호"), "password123");
await user.click(screen.getByRole("button", { name: "가입" }));
expect(await screen.findByText("이미 사용 중인 이메일입니다.")).toBeInTheDocument();
});Playwright smoke test는 핵심 흐름만 얇게 둔다.
import { test, expect } from "@playwright/test";
test("로그인 후 프로젝트를 생성한다", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("이메일").fill("demo@example.com");
await page.getByLabel("비밀번호").fill("password123");
await page.getByRole("button", { name: "로그인" }).click();
await page.getByRole("link", { name: "프로젝트" }).click();
await page.getByRole("button", { name: "새 프로젝트" }).click();
await page.getByLabel("프로젝트 이름").fill("프론트 학습");
await page.getByRole("button", { name: "생성" }).click();
await expect(page.getByText("프론트 학습")).toBeVisible();
});백엔드와 맞춰야 할 지점
- OpenAPI schema와 실제 응답이 일치해야 한다.
- error response에는 code, message, fieldErrors, traceId를 일관되게 둔다.
- enum은 확장 가능성을 고려하고, 프론트 fallback UI를 합의한다.
- nullable 필드와 omitted 필드의 의미를 구분한다.
- 날짜/시간/타임존 기준을 문서와 fixture에 반영한다.
- mock handler는 성공 응답뿐 아니라 validation error, 401/403, empty, pagination 끝 상태를 포함해야 한다.
개인 프로젝트 기준
- mapper와 formatter는 빠른 unit test로 보호한다.
- form, auth, empty/error state는 component test로 검증한다.
- Playwright는 전체 기능을 모두 테스트하기보다 핵심 happy path와 smoke flow만 둔다.
- MSW handler는 실제 API 문서와 같은 schema로 관리한다.
- 테스트 데이터는 “항상 예쁜 값”만 두지 말고 긴 문자열, null, 권한 부족, 빈 목록을 포함한다.
실전 팁
- React Testing Library는 role, label text처럼 접근성 기반 query를 우선 사용한다.
- mock 데이터는 성공 케이스만 만들지 말고 empty, error, nullable 케이스를 포함한다.
- PR에서 API 계약 변경이 있으면 프론트 fixture와 MSW handler 변경도 같이 요청한다.
- e2e에서 실패한 흐름은 screenshot, trace, network log를 같이 확인해야 원인을 프론트/백엔드/환경으로 나눌 수 있다.
- 관련 함정은 nullable API 응답 타입을 어떻게 표현해야 할까?와 JavaScript Date와 timezone은 왜 개인 프로젝트에서도 터질까?를 본다.
위험 신호!
- 테스트가 CSS class나 DOM depth 같은 구현 세부에 강하게 의존한다.
- mock response가 실제 API와 다르다.
- e2e 테스트가 너무 많아 느리고 자주 flaky하다.
- 백엔드 에러 코드가 매번 달라 프론트가 분기하기 어렵다.
확인 질문
확인 질문
- unit test에 적합한 프론트 코드는 무엇인가?
- formatter, mapper, schema helper처럼 순수 함수에 가까운 코드다.
- component test는 무엇을 중심으로 작성하는가?
- 사용자가 보는 문구, role, label, 클릭, 입력 같은 상호작용 중심으로 작성한다.
- 좋은 API 계약은 성공 응답만 포함하면 충분한가?
- 아니다. 실패 응답, nullable, pagination, enum, 날짜/시간 기준까지 포함해야 한다.