이번 주차는 E2E 테스트와 시각적 회귀 테스트에 대해 공부했다.
큰 흐름은 이렇게 볼 수 있다.
E2E 테스트:
사용자가 실제로 서비스를 쓰는 흐름이 끝까지 잘 동작하는지 확인
시각적 회귀 테스트:
화면 UI가 의도치 않게 깨지거나 바뀌지 않았는지 확인
관련 도구들은 역할이 조금씩 다르다.
Cypress / Playwright → E2E 테스트 도구
Storybook → 컴포넌트를 독립적으로 보여주는 도구
Chromatic → Storybook 화면을 캡처해서 이전 화면과 비교하는 도구
E2E는 End-to-End의 줄임말이다.
말 그대로 사용자가 서비스를 사용하는 흐름을 처음부터 끝까지 테스트하는 것이다.
예를 들어 로그인 기능이라면 이런 흐름을 테스트한다.
로그인 페이지 접속
이메일 입력
비밀번호 입력
로그인 버튼 클릭
홈 화면으로 이동했는지 확인
즉, E2E 테스트의 핵심은 이거다.
사용자가 이 기능을 실제로 끝까지 사용할 수 있는가?
Unit Test가 작은 함수나 컴포넌트 단위를 본다면, E2E 테스트는 실제 브라우저에서 사용자가 하는 행동을 기준으로 본다.
예를 들어 공동구매 서비스라면 이런 흐름이 E2E 테스트 대상이 될 수 있다.
로그인 → 상품 목록 → 상품 상세 → 참여하기 → 수량 선택 → 참여 완료 → 마이페이지 확인
이렇게 핵심 흐름을 자동화해두면 배포 전에 중요한 기능이 깨졌는지 확인할 수 있다.
장점은 실제 사용자 관점에 가장 가깝다는 것이다.
그래서 이런 문제를 잡을 수 있다.
버튼이 실제로 클릭되는지
로그인 후 페이지 이동이 되는지
토큰 저장 후 인증 상태가 유지되는지
폼 제출 후 결과 화면이 잘 뜨는지
하지만 단점도 있다.
브라우저를 실제로 띄우고 페이지 이동, 클릭, 입력을 수행하기 때문에 상대적으로 느리고 무겁다.
또 요소를 너무 구현 세부사항에 의존해서 찾으면 테스트가 쉽게 깨질 수 있다.
예를 들어 className으로 찾는 경우:
cy.get('.login-button').click();
디자인 수정으로 className이 바뀌면 기능은 멀쩡한데 테스트만 실패할 수 있다.
그래서 E2E 테스트는 모든 기능에 다 작성하기보다는, 서비스에서 중요한 핵심 흐름 위주로 작성하는 것이 좋다.
Cypress는 대표적인 E2E 테스트 도구다.
사용자가 브라우저에서 하는 행동을 코드로 작성할 수 있다.
describe('로그인', () => {
it('이메일과 비밀번호를 입력하면 로그인된다', () => {
cy.visit('/login');
cy.get('[data-cy=email-input]').type('test@example.com');
cy.get('[data-cy=password-input]').type('password123');
cy.get('[data-cy=login-button]').click();
cy.url().should('include', '/home');
cy.contains('환영합니다').should('be.visible');
});
});
처음에는 Cypress가 className이나 id로 요소를 찾는 도구처럼 느껴졌는데, 실제로는 className보다 테스트 전용 속성을 쓰는 것이 더 안정적이다.
<input data-cy="email-input" />
<button data-cy="login-button">로그인</button>
cy.get('[data-cy=email-input]').type('test@example.com');
cy.get('[data-cy=login-button]').click();
className은 스타일 변경 때문에 자주 바뀔 수 있지만, data-cy는 테스트를 위한 이름이라 상대적으로 안정적이다.
Playwright도 E2E 테스트 도구다.
Cypress처럼 실제 브라우저에서 페이지에 접속하고, 클릭하고, 입력하고, 결과를 확인할 수 있다.
import { test, expect } from '@playwright/test';
test('로그인할 수 있다', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('이메일').fill('test@example.com');
await page.getByLabel('비밀번호').fill('password123');
await page.getByRole('button', { name: '로그인' }).click();
await expect(page).toHaveURL(/home/);
await expect(page.getByText('환영합니다')).toBeVisible();
});
Playwright는 getByRole, getByLabel, getByText처럼 사용자가 실제로 인식하는 방식으로 요소를 찾는 API가 잘 제공된다.
그래서 React Testing Library와 문법 느낌이 비슷하게 느껴졌다.
처음에는 Playwright 코드가 React Testing Library와 비슷해서 둘이 관련 있는 도구인가 싶었다.
React Testing Library:
screen.getByRole('button', { name: '로그인' });
screen.getByLabelText('이메일');
screen.getByText('환영합니다');
Playwright:
await page.getByRole('button', { name: '로그인' }).click();
await page.getByLabel('이메일').fill('test@example.com');
await expect(page.getByText('환영합니다')).toBeVisible();
정리하면, 둘이 직접 연결된 도구라서 비슷한 것은 아니다.
둘 다 사용자 관점으로 테스트하자는 철학을 가지고 있어서 비슷한 API가 나온 것이다.
Vitest → 테스트 실행기
React Testing Library → React 컴포넌트 테스트 도구
Playwright → 실제 브라우저 기반 E2E 테스트 도구
즉, Playwright가 Vitest와 비슷하다기보다는, Playwright의 요소 찾는 방식이 React Testing Library와 비슷하다고 보는 게 맞다.
React Testing Library는 실제 브라우저를 띄우는 도구가 아니다.
보통 jsdom이라는 가짜 DOM 환경에서 컴포넌트를 테스트한다.
그래서 input 입력, 버튼 클릭, 텍스트 확인은 가능하다.
render(<LoginForm />);
await user.type(screen.getByLabelText('이메일'), 'test@example.com');
await user.click(screen.getByRole('button', { name: '로그인' }));
expect(screen.getByText('로그인 성공')).toBeInTheDocument();
하지만 실제 주소창 이동, 새로고침, 뒤로가기 같은 브라우저 동작을 테스트하는 도구는 아니다.
React Router의 MemoryRouter를 쓰면 라우팅을 흉내낼 수는 있다.
render(
<MemoryRouter initialEntries={['/login']}>
<App />
</MemoryRouter>
);
하지만 이것은 실제 주소창이 바뀌는 것이 아니라, 메모리상에서 라우팅 상태를 흉내내는 것이다.
실제 URL 이동을 확인하려면 Playwright나 Cypress를 써야 한다.
await expect(page).toHaveURL(/home/);
| 구분 | Cypress | Playwright |
|---|---|---|
| 역할 | E2E 테스트 도구 | E2E 테스트 도구 |
| 코드 느낌 | cy.visit, cy.get, cy.contains | page.goto, page.getByRole, expect |
| 요소 찾기 | data-cy 같은 selector를 많이 사용 | role, label, text 기반 locator를 많이 사용 |
| 장점 | 실행 과정을 보기 쉽고 디버깅이 직관적 | 사용자 관점 locator, 여러 브라우저 테스트에 강함 |
둘 중 뭐가 무조건 더 좋다기보다는 팀 상황과 테스트 목적에 따라 선택하면 될 것 같다.
처음 이해할 때는 이렇게 잡았다.
Cypress:
테스트용 이름(data-cy)으로 요소를 안정적으로 찾는 느낌
Playwright:
사용자가 보는 이름(role, label, text)으로 요소를 찾는 느낌
물론 Cypress도 Testing Library를 추가하면 사용자 관점 query를 사용할 수 있다.
시각적 회귀 테스트는 UI의 생김새가 의도치 않게 바뀌지 않았는지 확인하는 테스트다.
여기서 회귀는 기존에 잘 되던 것이 다시 망가지는 것을 의미한다.
예를 들어 버튼 스타일을 조금 수정했는데, 그 버튼이 여러 페이지에서 쓰이고 있었다면 예상 못한 화면 깨짐이 생길 수 있다.
버튼 크기가 커짐
카드 레이아웃이 밀림
텍스트가 두 줄로 밀림
모바일 화면에서 요소가 잘림
모달 위치가 어긋남
이런 문제는 일반 테스트 코드로 하나하나 확인하기 어렵다.
그래서 시각적 회귀 테스트는 화면을 스크린샷으로 비교한다.
이전 정상 화면 캡처
현재 화면 캡처
두 화면 비교
차이가 있으면 알려줌
의도한 변경이면 승인하고, 의도하지 않은 변경이면 수정하면 된다.
Storybook은 컴포넌트를 앱에서 분리해서 독립적으로 볼 수 있게 해주는 도구다.
예를 들어 버튼 컴포넌트를 앱의 특정 페이지까지 이동하지 않고도 따로 확인할 수 있다.
<Button variant="primary">확인</Button>
<Button variant="secondary">취소</Button>
<Button disabled>비활성화</Button>
Storybook에서는 컴포넌트의 상태 하나하나를 story로 만든다.
export const Default = {
args: {
children: '확인',
},
};
export const Disabled = {
args: {
children: '비활성화',
disabled: true,
},
};
이렇게 해두면 버튼의 기본 상태, 비활성화 상태 등을 독립적으로 확인할 수 있다.
Storybook 자체는 테스트 도구라기보다는 UI 컴포넌트를 개발하고 문서화하는 도구에 가깝다.
하지만 컴포넌트 상태를 story로 정리해두면, 이후 시각적 회귀 테스트의 기준으로 활용할 수 있다.
Chromatic은 Storybook과 함께 사용하는 시각적 회귀 테스트 도구다.
Storybook에 등록된 story들을 캡처하고, 이전 화면과 현재 화면을 비교한다.
Storybook → 컴포넌트 상태를 story로 정리
Chromatic → story 화면을 캡처해서 이전 화면과 비교
예를 들어 Button story가 있다고 하면:
Button / Default
Button / Disabled
Button / Loading
Chromatic은 각각의 화면을 캡처해두고, PR에서 UI가 바뀌면 이전 화면과 비교해준다.
의도한 변경이면 승인하고, 의도하지 않은 변경이면 수정하면 된다.
비유하면 이렇게 이해했다.
Storybook = 컴포넌트 전시장
Chromatic = 전시장 사진을 찍고 이전 사진과 비교하는 도구
| 구분 | E2E 테스트 | 시각적 회귀 테스트 |
|---|---|---|
| 관심사 | 기능 흐름 | 화면 생김새 |
| 질문 | 사용자가 기능을 끝까지 사용할 수 있는가? | UI가 의도치 않게 바뀌지 않았는가? |
| 대표 도구 | Cypress, Playwright | Storybook, Chromatic |
| 검증 방식 | 클릭, 입력, 이동, 결과 확인 | 스크린샷 비교 |
예를 들어 로그인 버튼을 기준으로 보면:
E2E 테스트:
로그인 버튼을 누르면 실제로 로그인이 되는가?
시각적 회귀 테스트:
로그인 버튼의 크기, 색상, 위치가 의도치 않게 바뀌지 않았는가?
둘은 대체 관계가 아니라 보완 관계다.
이번 주차 내용을 정리하면 이렇게 볼 수 있다.
Vitest + React Testing Library:
컴포넌트가 사용자 행동에 잘 반응하는지 테스트
Cypress / Playwright:
실제 브라우저에서 사용자의 핵심 흐름을 테스트
Storybook:
컴포넌트를 독립적으로 보여주고 상태별로 정리
Chromatic:
Storybook 화면을 이전 화면과 비교해서 UI 변경 감지
테스트마다 보는 관점이 다르다.
Unit Test:
작은 코드가 맞는지
Integration Test:
여러 조각이 함께 잘 동작하는지
E2E Test:
사용자가 실제로 기능을 끝까지 쓸 수 있는지
Visual Regression Test:
사용자가 보는 화면이 깨지지 않았는지
결국 이번 주차의 핵심은 이거다.
E2E 테스트는 기능 흐름을 확인하고,
시각적 회귀 테스트는 화면의 생김새를 확인한다.
둘 다 사용자가 실제로 마주하는 경험을 안정적으로 만들기 위한 테스트라고 이해하면 될 것 같다.