가비아 · 하이웍스개발팀
Playwright를 활용한 비즈니스 로직 검증
복잡한 반복 일정의 사용자 흐름을 검증하기 위해 Playwright 기반 E2E 테스트 환경을 구축했습니다.
2024.07 — 2025.04
- Playwright
- TypeScript
- E2E Testing
- WAI-ARIA
주요 기여
- 팀 내 최초로 Playwright 기반 E2E 테스트 환경을 구축하고, 복잡한 반복 일정의 주요 사용자 흐름을 검증했습니다.
- 테스트 과정에서 발견한 접근성 문제를 분석하고 개선을 제안해 디자인 시스템 33종 컴포넌트의 Role·ARIA 속성 개선으로 연결했습니다.
- 테스트 리포트를 기획자에게 공유하고 반복 QA를 자동화해 기획자의 확인 업무를 줄였습니다.
테스트 리포트 데모
상태를 선택해 보세요. 항목을 누르면 검증 단계와 결과를 확인할 수 있습니다.
recurrence-rule.spec.ts3
recurrence-update.spec.ts3
recurrence-delete.spec.ts2
복잡한 비즈니스 로직을 테스트하는 방법
인프런 판교 퇴근길 밋업에서 탐정토끼 님의 스토리북 & 자동화 테스트와 함께 하는 컴포넌트 주도 프론트엔드 개발 발표를 보고, 자동화 테스트를 실제 업무에 도입할 방법을 고민했습니다.
당시 하이웍스 일정관리를 개편하며 일정 생성·수정·삭제와 반복 일정 기능을 개발하고 있었습니다. 반복 일정은 주기와 종료 조건, 수정·삭제 범위에 따라 처리 결과가 달라졌습니다. 또한 일정 작성 모달 위에 반복 설정이나 수정·삭제 범위를 선택하는 모달이 열려, 개별 컴포넌트의 동작뿐 아니라 여러 단계를 거치는 사용자 흐름까지 검증할 필요가 있었습니다.

이를 계기로 Playwright 기반 E2E 테스트 환경을 구축했습니다. 공통 인증 환경을 구성하고 테스트 작성 및 실행 방법을 문서화해, 팀원들도 동일한 방식으로 테스트를 작성하고 실행할 수 있도록 했습니다.
테스트 작성을 막던 접근성 정보
Playwright로 테스트를 작성하면서 디자인 시스템 컴포넌트의 접근성 정보가 부족해 요소를 찾기 어려운 문제를 발견했습니다. 화면에서는 버튼이나 선택 목록으로 동작하지만, 적절한 Role과 Accessible Name이 정의되지 않아 getByRole로 요소를 찾기 어려웠습니다.
내부 값과 버튼 순서에 의존하던 테스트
일정 작성 모달에서 반복 일정의 상세 설정을 열면, 반복 주기와 종료 조건을 선택하는 모달이 추가로 열렸습니다.
당시 '입력한 횟수만큼 반복'을 선택하고 저장하는 테스트는 다음과 같이 작성했습니다.
// '입력한 횟수만큼 반복' 옵션에 해당하는 value="1"로 선택
await page.locator('input[type="radio"][value="1"]').click();
// 중첩된 모달 중 나중에 열린 모달의 '저장' 버튼 선택
await page.getByRole('button', { name: '저장' }).last().click();
라디오의 value="1"은 '입력한 횟수만큼 반복'을 의미하는 내부 값이었습니다. 코드만으로는 어떤 종료 조건을 선택하는지 알기 어려워 컴포넌트 구현을 확인해야 했습니다.
또한 중첩된 모달에 각각 '저장' 버튼이 있어, .last()로 마지막에 매칭되는 버튼을 선택했습니다. 이 방식은 모달의 DOM 구조나 버튼 순서가 변경되면 다른 요소를 선택할 위험이 있었습니다.
접근성 정보로 테스트 의도 표현하기
요소의 내부 값이나 순서에 의존하는 대신, Role과 Accessible Name을 기준으로 대상을 찾는 방식을 검토했습니다.
예를 들어 반복 설정 모달과 라디오에 적절한 접근성 정보를 제공하면 다음처럼 테스트를 작성할 수 있습니다.
const repeatDialog = page.getByRole('dialog', {
name: '반복 일정 설정',
});
await repeatDialog.getByRole('radio', { name: '입력한 횟수만큼 반복' }).check();
await repeatDialog.getByRole('button', { name: '저장' }).click();
개선된 코드에서는 반복 일정 설정에서 횟수를 지정하고 저장한다는 의도가 명확하게 드러납니다. 또한 탐색 범위를 모달 내부로 제한해 다른 모달의 동일한 이름을 가진 버튼과 구분할 수 있습니다.
테스트 작성 과정에서 발견한 문제를 공통 컴포넌트에서도 개선하고자, 디자인 시스템 전체 컴포넌트의 접근성 속성을 조사하고 사내 문서에 접근성 Role 문서를 작성했습니다.
작성한 문서를 팀 내부에서 발표하며 테스트 작성 과정에서 겪은 문제와 개선 방향을 공유했습니다. 이후 제안한 내용이 반영되어 디자인 시스템 33종 컴포넌트의 Role·ARIA 속성이 개선됐습니다.
복잡한 비즈니스 로직을 어디까지 테스트해야할까?
반복 일정은 매일·매주·매월·매년의 기본 주기 외에도 반복 간격, 종료 조건, 수정·삭제 범위에 따라 처리 결과가 달라졌습니다.
모든 조건을 조합해 테스트하면 케이스가 지나치게 많아지고, 실행 시간과 유지보수 부담도 커지게 됩니다. 따라서 모든 경우를 검증하기보다 기획 정책에 따라 동작이 달라지는 경계 조건을 중심으로 테스트를 작성했습니다.
정책이 달라지는 경계 조건 찾기
월간 반복은 시작일에 따라 선택할 수 있는 반복 조건이 달라졌습니다. 일반적인 날짜에는 '매월 특정 날짜'와 '매월 특정 번째 요일'을 선택할 수 있지만, 마지막 주에 속하거나 말일인 경우에는 추가적인 반복 옵션이 제공됐습니다.
예를 들어 2025년 2월 28일은 마지막 금요일이면서 말일이므로 '매월 마지막 금요일'과 '매월 말일'을 모두 선택할 수 있습니다. 반면 5월 30일은 마지막 금요일이지만 말일은 아니므로 '매월 말일' 옵션이 제공되지 않아야 합니다.


따라서 일반 날짜, 마지막 주에 속한 날짜, 말일을 구분해 각 조건에서 올바른 반복 옵션이 제공되는지 테스트했습니다.
단순히 옵션이 표시되는지만 확인하지 않고, 선택한 반복 조건이 저장 후에도 올바르게 반영되는지까지 검증했습니다.
변경되지 않아야 할 결과까지 검증하기
반복 일정은 수정·삭제할 때 선택한 범위에 따라 영향을 받는 일정이 달라졌습니다. 예를 들어 10월 5일부터 9일까지 매일 반복되는 일정에서 7일의 제목을 '현재 일정과 이후 반복 일정 모두 수정' 범위로 변경한다면, 5·6일은 기존 제목이 유지되고 7·8·9일에만 수정한 제목이 표시되어야 합니다.
다음은 변경 범위에 따른 결과와 기존 상태가 유지되는지 함께 검증한 테스트 코드입니다.
const titleInput = page.getByRole('textbox', { name: '제목' });
const originalTitle = await titleInput.inputValue();
const updatedTitle = `${originalTitle} (수정)`;
await titleInput.fill(updatedTitle);
await page.getByRole('button', { name: '저장', exact: true }).click();
// 변경 대상에는 수정된 제목이 반영되어야 합니다.
for (const date of ['2025-10-07', '2025-10-08', '2025-10-09']) {
await expect(getSchedule(date)).toHaveText(updatedTitle);
}
// 변경 대상이 아닌 경우 기존 제목을 유지해야 합니다.
for (const date of ['2025-10-05', '2025-10-06']) {
await expect(getSchedule(date)).toHaveText(originalTitle);
}
수정 결과를 검증할 때는 변경된 값뿐 아니라 변경 범위 밖의 데이터가 기존 상태를 유지하는지도 확인했습니다. 삭제 테스트에서도 같은 기준을 적용해, 삭제 대상이 아닌 데이터가 유지되는지 검증했습니다.
테스트 결과 활용 사례
Playwright 테스트 결과를 기획자의 QA 업무에도 활용할 수 있도록 리포트로 공유했습니다.
기획자가 코드를 읽지 않아도 검증 내용을 이해할 수 있도록 각 테스트 항목에 검증하는 동작과 결과를 구체적으로 표현했습니다.
또한 기존 정책 문서의 각 항목에 테스트 리포트 링크를 연결해, 정책별 검증 결과를 바로 확인하고 반복적인 QA 업무에 활용할 수 있도록 했습니다.