본문 바로가기
DEVEL/PYTHON

Playwright로 이벤트 랜딩페이지 테스트 자동화하기: 매번 다른 페이지를 어떻게 검증할까?

by codebyai 2026. 6. 23.
반응형

1. 도입부: 이벤트 페이지는 왜 자동화 테스트가 어려울까?

이벤트 페이지, 프로모션 페이지, 캠페인 랜딩페이지는 일반적인 서비스 페이지와 다릅니다. 메인 서비스의 로그인, 회원가입, 결제 페이지는 UI와 플로우가 비교적 오래 유지되지만, 이벤트 페이지는 캠페인마다 문구, 이미지, 히어로 배너, 혜택 섹션, 신청 조건, CTA 버튼 위치가 계속 바뀝니다.

그래서 많은 팀이 이런 말을 합니다.

“이벤트 페이지는 매번 달라서 자동화 테스트가 어렵다.”
“어차피 마케터가 문구를 바꾸면 테스트가 깨질 텐데 의미가 있나?”
“랜딩페이지는 눈으로 보는 QA가 더 빠르지 않나?”

절반은 맞고 절반은 틀립니다. 디자인 완성도, 카피의 설득력, 브랜드 톤, 마케팅 메시지의 적절성은 사람이 보는 수동 QA가 더 적합할 수 있습니다. 하지만 CTA 버튼이 작동하는지, 신청 폼이 열리는지, 필수값 검증이 되는지, 개인정보 동의 없이 제출되지 않는지, 제출 완료 메시지가 보이는지는 사람이 매번 반복해서 확인하지 않아도 되는 영역입니다.

즉, 이벤트 페이지 테스트 자동화의 핵심은 “페이지 전체가 같은지”가 아니라 “전환에 필요한 기능 계약이 지켜지는지”를 검증하는 것입니다. Playwright는 현대 웹 앱용 E2E 테스트 프레임워크로, test runner, assertion, 격리 실행, 병렬 실행, 브라우저 실행 도구를 함께 제공하며 Chromium, WebKit, Firefox 및 모바일 에뮬레이션도 지원합니다.

2. 5줄 핵심 요약

이벤트 페이지가 매번 달라도 변하지 않는 기능 기준을 잡으면 Playwright 테스트 자동화가 가능하다.
화면 전체를 픽셀 단위로 고정 비교하면 문구·이미지·날짜 변경 때문에 테스트가 쉽게 깨진다.
CTA, 신청 폼, 필수 입력값, 개인정보 동의, 제출 완료 흐름은 고정 계약으로 검증해야 한다.
문구와 디자인은 유연하게, 전환 흐름과 접근 가능한 locator는 엄격하게 테스트해야 한다.
Playwright data-testid, Page Object Model, API mock, visual regression, CI 전략을 함께 설계해야 유지보수가 쉽다.

3. 결론부터: 페이지가 매번 달라도 테스트 자동화는 가능하다

결론부터 말하면, 매번 다른 페이지 테스트는 가능하다. 다만 모든 것을 자동화하면 안 된다.

이벤트 페이지 테스트 자동화에서 가장 중요한 질문은 “이 페이지가 어제와 완전히 같은가?”가 아닙니다. 더 좋은 질문은 다음과 같습니다.

  • 사용자가 이벤트 내용을 인지할 수 있는가?
  • CTA 버튼을 찾고 클릭할 수 있는가?
  • 신청 폼이 정상적으로 보이는가?
  • 필수값 누락 시 오류가 표시되는가?
  • 개인정보 동의 없이 제출되지 않는가?
  • 제출 성공 후 완료 메시지가 보이는가?
  • 모바일에서도 CTA와 신청 흐름이 작동하는가?
  • API 데이터가 바뀌어도 테스트가 예측 가능하게 동작하는가?

이 기준으로 보면 이벤트 페이지 테스트 자동화는 “랜딩페이지 QA 전체를 대체하는 도구”가 아니라, 전환 흐름의 회귀 버그를 빠르게 잡는 안전망입니다. Playwright E2E 테스트는 이런 흐름 검증에 잘 맞습니다. Playwright의 locator는 사용할 때마다 최신 DOM 요소를 다시 찾고, role·label·text·test id 기반 locator를 제공해 UI 리렌더링이 많은 React, Next.js, Vue 페이지에서도 더 안정적인 테스트를 작성할 수 있습니다.

4. 잘못된 접근: 화면 전체를 고정값으로 비교하기

이벤트 페이지 테스트 자동화를 처음 도입할 때 흔한 실수는 페이지 전체를 하나의 정답 이미지처럼 비교하는 것입니다.

예를 들어 다음과 같은 방식입니다.

 
// ❌ 나쁜 예시: 이벤트 페이지 전체를 무조건 동일한 스크린샷으로 비교합니다.
// 문구, 날짜, 배너, 이미지, 추천 상품 순서가 조금만 바뀌어도 실패할 수 있습니다.
await expect(page).toHaveScreenshot('event-page.png', {
  fullPage: true,
});
 

Playwright의 screenshot 비교 기능 자체가 나쁜 것은 아닙니다. Playwright Test는 expect(page).toHaveScreenshot()으로 기준 스크린샷을 만들고 이후 실행에서 시각적 차이를 비교할 수 있습니다. 다만 공식 문서에서도 브라우저 렌더링은 OS, 버전, 하드웨어, headless 여부 등 실행 환경의 영향을 받을 수 있으므로 기준 스크린샷과 같은 환경에서 실행하는 것이 중요하다고 설명합니다.

이벤트 페이지는 특히 다음 요소가 자주 바뀝니다.

  • 히어로 이미지
  • 이벤트 마감 날짜
  • 카운트다운
  • 랜덤 배너
  • 추천 상품
  • 후기 목록
  • 마케팅 문구
  • 섹션 순서
  • A/B 테스트 영역
  • 쿠폰 금액 또는 혜택 문구

이런 영역까지 전부 고정값으로 검증하면 실제 버그가 없어도 테스트가 계속 실패합니다. 그러면 팀은 결국 테스트를 믿지 않게 되고, 실패 알림은 무시됩니다. 자동화 테스트에서 가장 위험한 상태는 “가끔 실패하지만 대충 다시 돌리면 통과하는 테스트”입니다.

5. 올바른 접근: 변하지 않는 기능과 계약을 테스트하기

이벤트 페이지 자동화의 기준은 디자인이 아니라 계약입니다.

여기서 계약이란 개발자, 디자이너, 마케터, QA가 합의한 “이벤트 페이지라면 반드시 지켜야 하는 동작”을 뜻합니다.

예를 들어 다음과 같습니다.

 
<!-- ✅ 좋은 예시: 디자인은 바뀌어도 테스트 계약은 유지합니다. -->
<section data-testid="event-hero">
  <!-- 캠페인마다 제목과 이미지는 달라질 수 있습니다. -->
</section>

<a data-testid="event-primary-cta" href="#event-application-form">
  신청하기
</a>

<form data-testid="event-application-form">
  <label>
    이름
    <input name="name" />
  </label>

  <label>
    이메일
    <input name="email" type="email" />
  </label>

  <label>
    <input type="checkbox" name="privacyAgreement" />
    개인정보 수집 및 이용에 동의합니다
  </label>

  <button type="submit" data-testid="event-form-submit">
    신청 완료
  </button>
</form>
 

위 구조에서 마케팅 문구와 이미지는 바뀔 수 있습니다. 하지만 event-primary-cta, event-application-form, event-form-submit 같은 테스트 계약은 함부로 바뀌면 안 됩니다.

Playwright의 getByTestId()는 기본적으로 data-testid 속성을 기준으로 요소를 찾으며, 공식 문서에서도 test id는 텍스트나 role이 바뀌어도 통과할 수 있는 탄력적인 테스트 방법이라고 설명합니다. 다만 role이나 text 자체가 중요한 경우에는 사용자 관점의 locator를 고려해야 합니다.

6. 변해도 되는 것과 변하면 안 되는 것

이벤트 페이지 테스트 자동화의 첫 단계는 “무엇이 바뀌어도 되는가”와 “무엇이 바뀌면 안 되는가”를 나누는 것입니다.

표 1. 변해도 되는 것과 변하면 안 되는 것

항목변해도 되는가테스트 방식예시
히어로 이미지 존재 여부 또는 alt 정책만 확인 이미지 파일명이 바뀌어도 hero 영역은 보여야 함
이벤트 제목 문구 부분적으로 예 정규식 또는 핵심 키워드 포함 검증 “사전 신청”, “무료 웨비나” 등 핵심 표현만 확인
섹션 순서 순서보다 필수 섹션 존재 여부 검증 혜택 섹션이 CTA 위/아래로 이동 가능
CTA 버튼 문구 부분적으로 예 role + 정규식 또는 test id 검증 “신청하기”, “참여하기”, “등록하기” 허용
CTA 클릭 동작 아니오 클릭 후 폼 이동/모달 표시 검증 클릭해도 아무 반응 없으면 실패
신청 폼 필드 보통 아니오 label 기반 입력 검증 이름, 이메일, 연락처 등 필수 필드
개인정보 동의 아니오 체크 전 제출 불가, 체크 후 제출 가능 동의 없이 신청되면 실패
제출 API 아니오 API mock 또는 테스트 서버 검증 실제 고객 정보/이메일 발송 방지
완료 메시지 아니오 성공 상태 문구 또는 test id 검증 “신청이 완료되었습니다”
카운트다운/날짜 visual test에서는 mask 처리 매일 바뀌는 영역 제외
분석 이벤트 아니오 request 발생 여부 또는 dataLayer 검증 CTA 클릭 이벤트 누락 방지

7. Playwright가 이벤트 페이지 테스트에 적합한 이유

Playwright를 쓰는 이유는 “요즘 많이 쓰는 도구라서”가 아닙니다. 이벤트 페이지와 랜딩페이지 E2E 테스트에서 반복적으로 마주치는 문제를 해결하는 기능이 한곳에 모여 있기 때문입니다.

Playwright 테스트 자동화에 유리한 점

첫째, locator 전략이 좋습니다. getByRole, getByLabel, getByText, getByTestId를 통해 사용자가 인식하는 방식 또는 테스트 계약 기반으로 요소를 찾을 수 있습니다. Playwright 공식 문서는 role locator를 사용자가 페이지를 인식하는 방식에 가깝기 때문에 우선 고려하라고 설명합니다.

둘째, auto-waiting과 auto-retrying assertion이 있습니다. Playwright는 클릭 전 요소가 보이는지, 안정적인지, 이벤트를 받을 수 있는지, 활성화되어 있는지 같은 actionability check를 자동으로 수행합니다. 또한 expect(locator).toHaveText() 같은 웹 전용 assertion은 조건이 충족될 때까지 반복 확인합니다.

셋째, API mocking이 가능합니다. 이벤트 데이터, 신청 가능 여부, 마감 상태, 제출 응답처럼 운영 데이터에 따라 달라지는 값을 테스트에서 고정할 수 있습니다. Playwright는 HTTP/HTTPS 네트워크 요청을 추적·수정·mock할 수 있고, page.route()를 사용해 특정 API 응답을 테스트용으로 대체할 수 있습니다.

넷째, visual regression test를 선택적으로 사용할 수 있습니다. 다만 이벤트 페이지 전체를 무조건 비교하는 것이 아니라, 동적 영역을 mask 처리하고 핵심 레이아웃 붕괴를 잡는 용도로 사용해야 합니다. Playwright의 screenshot assertion은 mask, maxDiffPixels, stylePath 같은 옵션을 제공합니다.

다섯째, CI/CD에 붙이기 쉽습니다. 공식 문서 기준으로 Playwright 테스트는 CI 환경에서 실행할 수 있고, GitHub Actions 예제도 제공됩니다. CI에서는 브라우저와 OS 의존성을 설치한 뒤 npx playwright test로 실행하는 흐름이 기본입니다.

8. 안정적인 locator 전략

랜딩페이지 테스트 자동화에서 locator는 테스트 안정성의 절반입니다. 버튼 하나를 찾더라도 어떤 locator를 쓰느냐에 따라 테스트의 수명이 달라집니다.

getByRole로 CTA 버튼 찾기

 
import { test, expect } from '@playwright/test';

test('CTA 버튼을 사용자 관점의 role로 찾는다', async ({ page }) => {
  // 이벤트 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // 사용자가 버튼 또는 링크로 인식할 수 있는 CTA를 찾습니다.
  // 문구가 "신청하기", "참여하기", "등록하기" 중 하나여도 허용합니다.
  const cta = page
    .getByRole('link', { name: /신청하기|참여하기|등록하기/i })
    .or(page.getByRole('button', { name: /신청하기|참여하기|등록하기/i }))
    .first();

  // CTA가 화면에 보이는지 확인합니다.
  await expect(cta).toBeVisible();

  // CTA를 클릭합니다.
  await cta.click();

  // 클릭 후 신청 폼이 보이는지 확인합니다.
  await expect(page.getByTestId('event-application-form')).toBeVisible();
});
 

getByRole()은 버튼, 체크박스, 링크, heading 같은 접근성 role과 accessible name을 기준으로 요소를 찾습니다. CTA처럼 실제 사용자가 클릭하는 인터랙티브 요소에는 CSS selector보다 role locator를 우선 고려하는 편이 좋습니다.

getByLabel로 신청 폼 입력 찾기

 
import { test, expect } from '@playwright/test';

test('신청 폼 입력값을 label 기준으로 입력한다', async ({ page }) => {
  // 이벤트 신청 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // label 텍스트를 기준으로 이름 입력 필드를 찾습니다.
  await page.getByLabel(/이름|성함/).fill('홍길동');

  // label 텍스트를 기준으로 이메일 입력 필드를 찾습니다.
  await page.getByLabel(/이메일|email/i).fill('hong@example.com');

  // 입력값이 실제 필드에 반영되었는지 확인합니다.
  await expect(page.getByLabel(/이름|성함/)).toHaveValue('홍길동');
  await expect(page.getByLabel(/이메일|email/i)).toHaveValue('hong@example.com');
});
 

Playwright 공식 문서는 form control을 찾을 때 getByLabel()을 사용할 수 있다고 설명합니다. 이벤트 신청 폼 테스트에서는 label이 곧 사용자 경험과 접근성 품질을 함께 검증하는 기준이 됩니다.

getByText는 비상호작용 텍스트에 제한적으로 사용하기

 
import { test, expect } from '@playwright/test';

test('완료 메시지는 핵심 의미만 정규식으로 검증한다', async ({ page }) => {
  // 제출 완료 화면으로 이동했다고 가정합니다.
  await page.goto('/events/summer-promo/complete');

  // 문구 전체 exact match 대신 핵심 의미만 확인합니다.
  await expect(page.getByText(/신청.*완료|접수.*완료/)).toBeVisible();
});
 

getByText()는 문구를 기준으로 요소를 찾기 때문에 카피가 자주 바뀌는 이벤트 페이지에서는 너무 엄격하게 쓰면 쉽게 깨집니다. 공식 문서도 interactive element에는 role locator를 사용하고, text locator는 비상호작용 요소에 쓰는 것을 권장합니다.

getByTestId로 핵심 섹션 찾기

 
import { test, expect } from '@playwright/test';

test('핵심 이벤트 섹션은 data-testid 계약으로 찾는다', async ({ page }) => {
  // 이벤트 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // hero 영역은 디자인이 바뀌어도 존재해야 하는 핵심 섹션입니다.
  await expect(page.getByTestId('event-hero')).toBeVisible();

  // 신청 폼은 전환에 직접 연결되는 핵심 섹션입니다.
  await expect(page.getByTestId('event-application-form')).toBeVisible();

  // 제출 버튼은 문구가 바뀌어도 테스트 계약으로 찾습니다.
  await expect(page.getByTestId('event-form-submit')).toBeEnabled();
});
 

표 2. 이벤트 페이지 테스트 항목

테스트 대상왜 중요한가추천 locator실패 시 영향도
hero 영역 사용자가 이벤트의 맥락을 처음 인지하는 영역 getByTestId('event-hero'), getByRole('heading') 중간
CTA 버튼 신청/구매/참여 전환의 시작점 getByRole('link'/'button'), getByTestId 높음
신청 폼 전환 데이터를 받는 핵심 영역 getByTestId('event-application-form') 높음
이름/이메일 입력 리드 수집에 필요한 필수 데이터 getByLabel() 높음
개인정보 동의 법적·정책적 필수 절차 getByLabel(), getByRole('checkbox') 매우 높음
제출 버튼 전환 완료 액션 getByRole('button'), getByTestId 높음
완료 메시지 사용자에게 신청 결과를 알려줌 getByTestId, getByText(/완료/) 높음
모바일 CTA 모바일 유입이 많은 랜딩페이지에서 중요 getByTestId('event-mobile-sticky-cta') 높음
약관 링크 사용자가 정책 내용을 확인해야 함 getByRole('link') 중간~높음
분석 이벤트 마케팅 성과 측정에 필요 request 검증, dataLayer 검증 중간~높음

표 3. locator 전략 비교

방식장점단점추천 상황
getByRole 사용자와 보조기술이 인식하는 방식에 가까움 accessible name이 부정확하면 찾기 어려움 CTA, 버튼, 링크, heading, checkbox
getByLabel 폼 접근성까지 함께 검증 가능 label이 없거나 잘못 연결되면 실패 이름, 이메일, 전화번호, 동의 체크박스
getByText 안내 문구, 완료 메시지 확인에 직관적 카피 변경에 취약 완료 메시지, 비상호작용 안내 문구
getByTestId 문구·디자인 변경에 강함 사용자 관점 검증은 약함 핵심 섹션, 동적 컴포넌트, 테스트 계약
CSS selector 빠르게 작성 가능 DOM 구조 변경에 취약 아주 단순한 내부 요소 또는 대안이 없을 때
XPath 복잡한 DOM 탐색 가능 유지보수성이 낮고 구조 변경에 약함 레거시 페이지에서 최후의 수단

CSS selector와 XPath는 반드시 금지할 필요는 없지만, 이벤트 페이지처럼 DOM 구조가 자주 바뀌는 화면에서는 위험합니다. Playwright 공식 문서는 CSS/XPath가 DOM 구조나 구현에 묶일 수 있고, 구조 변경 시 테스트가 깨질 수 있으므로 role locator나 test id 같은 테스트 계약을 고려하라고 설명합니다.

9. data-testid를 테스트 계약으로 설계하기

data-testid는 단순한 테스트 편의 속성이 아닙니다. 이벤트 페이지에서는 디자이너와 마케터가 무엇을 바꿔도 되는지, 개발자가 무엇을 유지해야 하는지 정하는 계약입니다.

권장 네이밍 예시

 
<!-- ✅ hero 영역: 페이지 상단 핵심 영역입니다. -->
<section data-testid="event-hero">
  <h1>2026 여름 프로모션</h1>
</section>

<!-- ✅ CTA: 전환을 시작하는 핵심 액션입니다. -->
<a data-testid="event-primary-cta" href="#event-application-form">
  신청하기
</a>

<!-- ✅ 폼: 이벤트 신청 플로우의 핵심입니다. -->
<form data-testid="event-application-form">
  <input data-testid="event-input-name" name="name" />

  <input data-testid="event-input-email" name="email" />

  <button data-testid="event-form-submit" type="submit">
    신청 완료
  </button>
</form>

<!-- ✅ 완료 메시지: 제출 성공을 검증하는 기준입니다. -->
<div data-testid="event-submit-success">
  신청이 완료되었습니다.
</div>
 

data-testid 설계 원칙

  • 화면 위치가 아니라 의미를 이름으로 사용합니다.
  • top-button, blue-box, section-1 같은 이름은 피합니다.
  • event-primary-cta, event-application-form처럼 비즈니스 의미를 담습니다.
  • 마케터가 문구를 바꿔도 유지되어야 하는 요소에만 붙입니다.
  • 모든 div에 붙이지 말고, 테스트 계약이 필요한 요소에만 붙입니다.

Playwright의 기본 test id 속성은 data-testid이며, 설정에서 testIdAttribute를 바꾸면 data-pw 같은 커스텀 속성도 사용할 수 있습니다.

10. 이벤트 페이지 설정값을 분리해서 여러 페이지 테스트하기

이벤트 페이지가 여러 개라면 테스트 파일을 페이지마다 복사하면 안 됩니다. 대신 이벤트 페이지별 설정값을 JSON 또는 TypeScript 배열로 분리하고, 같은 테스트를 반복 실행하는 구조가 좋습니다.

Playwright 설치 명령어

 
# 새 프로젝트 또는 기존 프로젝트에 Playwright Test를 초기화합니다.
npm init playwright@latest

# CI 또는 Linux 환경에서 브라우저와 OS 의존성을 함께 설치할 때 사용합니다.
npx playwright install --with-deps

# 전체 Playwright 테스트를 실행합니다.
npx playwright test

# 실패 후 HTML 리포트를 확인합니다.
npx playwright show-report
 

Playwright 공식 설치 문서는 npm init playwright@latest, yarn create playwright, pnpm create playwright를 안내하며, 기본 선택값으로 TypeScript 사용, 테스트 폴더 생성, GitHub Actions workflow 추가, 브라우저 설치 등을 설정할 수 있다고 설명합니다.

기본 테스트 파일 구조

# 이벤트 랜딩페이지 테스트 예시 구조입니다.
e2e/
  fixtures/
    eventPages.ts
    # 이벤트 페이지별 path, 필수 섹션, 선택 섹션 설정을 둡니다.

  pages/
    EventLandingPage.ts
    # Page Object Model 클래스를 둡니다.

  specs/
    event-contract.spec.ts
    # CTA, hero, 필수 섹션 같은 공통 계약 테스트를 둡니다.

    event-form.spec.ts
    # 신청 폼, 필수값, 개인정보 동의, 제출 완료 테스트를 둡니다.

    event-visual.spec.ts
    # visual regression 및 mask 처리 테스트를 둡니다.

playwright.config.ts
# baseURL, browser project, retry, trace, reporter, CI 설정을 둡니다.
 

이벤트 페이지 설정 배열 예제

 
// e2e/fixtures/eventPages.ts

// 이벤트 페이지마다 달라지는 설정을 타입으로 정의합니다.
export type EventPageConfig = {
  // 테스트 리포트에 표시할 이벤트 이름입니다.
  name: string;

  // baseURL 기준 상대 경로입니다.
  path: string;

  // 제목 문구가 자주 바뀌므로 exact match 대신 정규식을 사용합니다.
  titlePattern: RegExp;

  // 반드시 존재해야 하는 섹션의 test id 목록입니다.
  requiredTestIds: string[];

  // 선택적으로 노출될 수 있는 섹션입니다.
  optionalSections: {
    benefit: boolean;
    speaker: boolean;
    faq: boolean;
  };
};

// 여러 이벤트 페이지를 배열로 관리합니다.
// 새 이벤트가 추가되면 테스트 파일을 복사하지 않고 이 배열에만 추가합니다.
export const eventPages: EventPageConfig[] = [
  {
    name: '2026-summer-promo',
    path: '/events/summer-promo',
    titlePattern: /여름|summer|프로모션/i,
    requiredTestIds: [
      'event-hero',
      'event-primary-cta',
      'event-application-form',
      'event-form-submit',
    ],
    optionalSections: {
      benefit: true,
      speaker: false,
      faq: true,
    },
  },
  {
    name: '2026-webinar',
    path: '/events/webinar',
    titlePattern: /웨비나|webinar|세미나/i,
    requiredTestIds: [
      'event-hero',
      'event-primary-cta',
      'event-application-form',
      'event-form-submit',
    ],
    optionalSections: {
      benefit: true,
      speaker: true,
      faq: true,
    },
  },
];
 

설정 배열을 이용한 공통 테스트

 
// e2e/specs/event-contract.spec.ts

import { test, expect } from '@playwright/test';
import { eventPages } from '../fixtures/eventPages';

for (const eventPage of eventPages) {
  test.describe(`${eventPage.name} 이벤트 페이지 계약 테스트`, () => {
    test('필수 섹션과 CTA가 존재한다', async ({ page }) => {
      // 설정에 있는 이벤트 페이지로 이동합니다.
      await page.goto(eventPage.path);

      // 제목은 exact match 대신 정규식으로 핵심 의미만 검증합니다.
      await expect(page.getByRole('heading', { name: eventPage.titlePattern })).toBeVisible();

      // 필수 test id가 모두 화면에 존재하는지 확인합니다.
      for (const testId of eventPage.requiredTestIds) {
        await expect(page.getByTestId(testId)).toBeVisible();
      }

      // CTA 버튼 또는 링크가 클릭 가능한 상태인지 확인합니다.
      await expect(page.getByTestId('event-primary-cta')).toBeVisible();
    });
  });
}
 

11. Page Object Model로 테스트 코드 구조화하기

테스트가 3~5개일 때는 spec 파일에 locator를 직접 써도 괜찮습니다. 하지만 이벤트 페이지가 10개, 20개로 늘어나고 CTA, 폼, 약관, 모바일 테스트가 추가되면 locator가 여기저기 흩어집니다.

이때 Playwright Page Object Model을 사용하면 테스트 코드를 유지보수하기 쉬워집니다. 공식 문서에서도 큰 테스트 suite를 구조화하는 방법 중 하나로 Page Object Model을 소개하며, page object가 selector를 한곳에 모으고 반복 코드를 줄여 유지보수를 쉽게 한다고 설명합니다.

Page Object Model 예제

 
// e2e/pages/EventLandingPage.ts

import { expect, type Locator, type Page } from '@playwright/test';

export class EventLandingPage {
  // Playwright의 Page 객체입니다.
  readonly page: Page;

  // 이벤트 페이지의 핵심 영역 locator입니다.
  readonly hero: Locator;
  readonly primaryCta: Locator;
  readonly applicationForm: Locator;
  readonly nameInput: Locator;
  readonly emailInput: Locator;
  readonly privacyCheckbox: Locator;
  readonly submitButton: Locator;
  readonly successMessage: Locator;

  constructor(page: Page) {
    this.page = page;

    // 디자인 변경에 흔들리지 않도록 핵심 섹션은 test id로 찾습니다.
    this.hero = page.getByTestId('event-hero');
    this.primaryCta = page.getByTestId('event-primary-cta');
    this.applicationForm = page.getByTestId('event-application-form');

    // 폼 입력은 사용자에게 보이는 label 기준으로 찾습니다.
    this.nameInput = page.getByLabel(/이름|성함/);
    this.emailInput = page.getByLabel(/이메일|email/i);

    // 개인정보 동의는 checkbox role 또는 label로 찾을 수 있습니다.
    this.privacyCheckbox = page.getByLabel(/개인정보.*동의|privacy.*agree/i);

    // 제출 버튼은 테스트 계약으로 찾습니다.
    this.submitButton = page.getByTestId('event-form-submit');

    // 완료 메시지는 test id로 찾되, 내용은 유연하게 검증합니다.
    this.successMessage = page.getByTestId('event-submit-success');
  }

  async goto(path: string) {
    // 이벤트 페이지로 이동합니다.
    await this.page.goto(path);

    // 페이지 진입 후 hero 영역이 보이는지 확인합니다.
    await expect(this.hero).toBeVisible();
  }

  async clickPrimaryCta() {
    // CTA를 클릭합니다.
    await this.primaryCta.click();

    // CTA 클릭 후 신청 폼이 보이는지 확인합니다.
    await expect(this.applicationForm).toBeVisible();
  }

  async fillRequiredFields(data: { name: string; email: string }) {
    // 이름 필드를 입력합니다.
    await this.nameInput.fill(data.name);

    // 이메일 필드를 입력합니다.
    await this.emailInput.fill(data.email);
  }

  async agreePrivacyPolicy() {
    // 개인정보 동의 체크박스를 체크합니다.
    await this.privacyCheckbox.check();

    // 실제 체크 상태가 되었는지 확인합니다.
    await expect(this.privacyCheckbox).toBeChecked();
  }

  async submit() {
    // 제출 버튼을 클릭합니다.
    await this.submitButton.click();
  }

  async expectSubmitSuccess() {
    // 성공 메시지가 보이는지 확인합니다.
    await expect(this.successMessage).toBeVisible();

    // 문구 전체가 아니라 핵심 의미만 확인합니다.
    await expect(this.successMessage).toContainText(/완료|접수|신청/);
  }
}
 

12. 이벤트 신청 플로우 테스트 예제

이벤트 신청 폼 테스트는 랜딩페이지 E2E 테스트에서 가장 중요한 영역입니다. 실제 고객 정보 입력, 실제 이메일 발송, 실제 결제, 실제 CRM 등록이 발생하지 않도록 테스트 환경 또는 API mock을 사용해야 합니다.

신청 폼 입력 테스트

 
// e2e/specs/event-form.spec.ts

import { test, expect } from '@playwright/test';
import { EventLandingPage } from '../pages/EventLandingPage';

test('이벤트 신청 폼에 필수 정보를 입력할 수 있다', async ({ page }) => {
  // Page Object를 생성합니다.
  const eventPage = new EventLandingPage(page);

  // 이벤트 페이지로 이동합니다.
  await eventPage.goto('/events/summer-promo');

  // CTA를 클릭해 신청 폼으로 이동합니다.
  await eventPage.clickPrimaryCta();

  // 필수 입력값을 입력합니다.
  await eventPage.fillRequiredFields({
    name: '홍길동',
    email: 'hong@example.com',
  });

  // 입력값이 정상적으로 반영되었는지 확인합니다.
  await expect(eventPage.nameInput).toHaveValue('홍길동');
  await expect(eventPage.emailInput).toHaveValue('hong@example.com');
});
 

Playwright의 locator.fill()은 input, textarea, contenteditable 요소를 입력하는 데 사용할 수 있으며, 입력 이벤트도 발생시킵니다. 체크박스와 라디오 버튼은 check() 또는 setChecked()를 사용할 수 있습니다.

필수 입력값 검증 테스트

 
import { test, expect } from '@playwright/test';

test('필수값을 입력하지 않으면 오류 메시지를 보여준다', async ({ page }) => {
  // 이벤트 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // 신청 폼으로 이동합니다.
  await page.getByTestId('event-primary-cta').click();

  // 아무 값도 입력하지 않고 제출합니다.
  await page.getByTestId('event-form-submit').click();

  // 이름 필수 오류가 보이는지 확인합니다.
  await expect(page.getByText(/이름.*필수|이름을 입력/)).toBeVisible();

  // 이메일 필수 오류가 보이는지 확인합니다.
  await expect(page.getByText(/이메일.*필수|이메일을 입력/)).toBeVisible();

  // 개인정보 동의 오류가 보이는지 확인합니다.
  await expect(page.getByText(/개인정보.*동의|동의.*필수/)).toBeVisible();
});
 

개인정보 동의 체크박스 테스트

 
import { test, expect } from '@playwright/test';

test('개인정보 동의 전에는 제출할 수 없고, 동의 후 제출할 수 있다', async ({ page }) => {
  // 이벤트 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // 신청 폼을 엽니다.
  await page.getByTestId('event-primary-cta').click();

  // 필수 정보를 입력합니다.
  await page.getByLabel(/이름|성함/).fill('홍길동');
  await page.getByLabel(/이메일|email/i).fill('hong@example.com');

  // 동의하지 않은 상태에서 제출합니다.
  await page.getByTestId('event-form-submit').click();

  // 개인정보 동의 오류가 표시되어야 합니다.
  await expect(page.getByText(/개인정보.*동의|동의.*필수/)).toBeVisible();

  // 개인정보 동의 체크박스를 선택합니다.
  await page.getByLabel(/개인정보.*동의|privacy.*agree/i).check();

  // 체크 상태를 확인합니다.
  await expect(page.getByLabel(/개인정보.*동의|privacy.*agree/i)).toBeChecked();
});
 

제출 완료 메시지 검증

 
import { test, expect } from '@playwright/test';
import { EventLandingPage } from '../pages/EventLandingPage';

test('이벤트 신청 완료 후 완료 메시지를 보여준다', async ({ page }) => {
  // 실제 운영 API 호출을 막기 위해 제출 API를 mock 처리합니다.
  await page.route('**/api/event-applications', async (route) => {
    await route.fulfill({
      status: 201,
      contentType: 'application/json',
      body: JSON.stringify({
        applicationId: 'test-application-001',
        status: 'COMPLETED',
      }),
    });
  });

  // Page Object를 생성합니다.
  const eventPage = new EventLandingPage(page);

  // 이벤트 페이지로 이동합니다.
  await eventPage.goto('/events/summer-promo');

  // CTA 클릭 후 폼을 입력합니다.
  await eventPage.clickPrimaryCta();
  await eventPage.fillRequiredFields({
    name: '홍길동',
    email: 'hong@example.com',
  });

  // 개인정보 수집 및 이용에 동의합니다.
  await eventPage.agreePrivacyPolicy();

  // 제출합니다.
  await eventPage.submit();

  // 완료 메시지를 검증합니다.
  await eventPage.expectSubmitSuccess();
});
 

13. API mock으로 이벤트 데이터를 고정하기

이벤트 페이지가 CMS, 백오피스, Laravel API, Next.js API Route, 외부 마케팅 플랫폼에서 데이터를 받아온다면 운영 데이터에 따라 테스트 결과가 달라질 수 있습니다.

예를 들어 오늘은 이벤트가 “신청 가능”이지만 내일은 “마감”일 수 있습니다. 배너가 매일 바뀔 수도 있고, 추천 상품이 랜덤으로 내려올 수도 있습니다. 이 상태에서 E2E 테스트를 운영 데이터에 의존하면 안정적인 QA 자동화가 어렵습니다.

이럴 때 Playwright API mock을 사용해 이벤트 데이터를 고정합니다.

Playwright API mock 예제

 
// e2e/specs/event-api-mock.spec.ts

import { test, expect } from '@playwright/test';

test('API mock으로 이벤트 데이터를 고정해 테스트한다', async ({ page }) => {
  // 페이지가 호출하는 이벤트 상세 API를 mock 처리합니다.
  // 이 mock을 page.goto()보다 먼저 등록해야 합니다.
  await page.route('**/api/events/*', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        id: 'summer-promo-2026',
        title: '2026 여름 프로모션',
        status: 'OPEN',
        ctaLabel: '신청하기',
        hasBenefitSection: true,
        hasSpeakerSection: false,
      }),
    });
  });

  // 이벤트 신청 제출 API도 mock 처리합니다.
  await page.route('**/api/event-applications', async (route) => {
    // 요청 본문을 확인해 테스트가 올바른 값을 보냈는지 검증할 수 있습니다.
    const requestBody = route.request().postDataJSON() as {
      name?: string;
      email?: string;
      privacyAgreement?: boolean;
    };

    // 테스트 데이터가 API로 전달되었는지 확인합니다.
    expect(requestBody.name).toBe('홍길동');
    expect(requestBody.email).toBe('hong@example.com');
    expect(requestBody.privacyAgreement).toBe(true);

    // 실제 서버에 요청하지 않고 성공 응답을 반환합니다.
    await route.fulfill({
      status: 201,
      contentType: 'application/json',
      body: JSON.stringify({
        applicationId: 'mock-application-001',
        status: 'COMPLETED',
      }),
    });
  });

  // mock이 등록된 뒤 페이지에 진입합니다.
  await page.goto('/events/summer-promo');

  // mock 데이터의 제목이 화면에 반영되었는지 확인합니다.
  await expect(page.getByRole('heading', { name: /여름 프로모션/ })).toBeVisible();

  // 폼 입력 후 제출합니다.
  await page.getByTestId('event-primary-cta').click();
  await page.getByLabel(/이름|성함/).fill('홍길동');
  await page.getByLabel(/이메일|email/i).fill('hong@example.com');
  await page.getByLabel(/개인정보.*동의/).check();
  await page.getByTestId('event-form-submit').click();

  // 완료 메시지를 확인합니다.
  await expect(page.getByTestId('event-submit-success')).toContainText(/완료|접수/);
});
 

Playwright의 mock API 기능은 페이지의 XHR/fetch 요청을 포함한 네트워크 요청을 추적, 수정, mock할 수 있습니다. 공식 예제에서도 page.route()를 page.goto() 전에 등록해 실제 API 호출 없이 custom response를 반환하는 흐름을 보여줍니다.

14. 선택 섹션과 조건부 UI 테스트하기

이벤트 페이지에는 선택 섹션이 많습니다.

  • 연사 소개가 있는 웨비나 페이지
  • 혜택 카드가 있는 프로모션 페이지
  • FAQ가 있는 신청 페이지
  • 후기 섹션이 있는 캠페인 페이지
  • 쿠폰 영역이 있는 세일 페이지

모든 이벤트 페이지에 모든 섹션이 있어야 한다고 테스트하면 안 됩니다. 대신 설정값에 따라 조건부로 검증해야 합니다.

선택 섹션이 있을 때 조건부 검증

 
// e2e/specs/event-optional-section.spec.ts

import { test, expect } from '@playwright/test';
import { eventPages } from '../fixtures/eventPages';

for (const eventPage of eventPages) {
  test(`${eventPage.name} 선택 섹션을 설정값 기준으로 검증한다`, async ({ page }) => {
    // 이벤트 페이지로 이동합니다.
    await page.goto(eventPage.path);

    // 혜택 섹션이 설정상 필요한 경우에만 보이는지 확인합니다.
    if (eventPage.optionalSections.benefit) {
      await expect(page.getByTestId('event-benefit-section')).toBeVisible();
    } else {
      await expect(page.getByTestId('event-benefit-section')).toHaveCount(0);
    }

    // 연사 섹션이 설정상 필요한 경우에만 보이는지 확인합니다.
    if (eventPage.optionalSections.speaker) {
      await expect(page.getByTestId('event-speaker-section')).toBeVisible();
    } else {
      await expect(page.getByTestId('event-speaker-section')).toHaveCount(0);
    }

    // FAQ 섹션이 설정상 필요한 경우에만 보이는지 확인합니다.
    if (eventPage.optionalSections.faq) {
      await expect(page.getByTestId('event-faq-section')).toBeVisible();
    } else {
      await expect(page.getByTestId('event-faq-section')).toHaveCount(0);
    }
  });
}
 

중요한 점은 “선택 섹션을 느슨하게 무시하는 것”과 “설정값에 따라 검증하는 것”은 다르다는 점입니다. 테스트를 너무 느슨하게 만들면 섹션이 실수로 빠져도 통과합니다. 반대로 모든 섹션을 항상 요구하면 정상적인 캠페인 차이를 버그로 오판합니다.

15. 모바일 화면과 반응형 테스트

이벤트 랜딩페이지는 모바일 유입 비중이 높은 경우가 많습니다. 데스크톱에서 CTA가 잘 보여도 모바일에서 sticky CTA가 가려지거나, 신청 폼 input이 화면 밖으로 밀리거나, 개인정보 동의 체크박스가 클릭되지 않는 문제가 생길 수 있습니다.

Playwright는 viewport, device emulation, locale, timezone 같은 emulation option을 설정할 수 있으며, 프로젝트 설정을 통해 데스크톱과 모바일 실행을 나눌 수 있습니다.

모바일 viewport 테스트 예제

 
// e2e/specs/event-mobile.spec.ts

import { test, expect } from '@playwright/test';

test('모바일 화면에서 고정 CTA를 통해 신청 폼으로 이동한다', async ({ page }) => {
  // 모바일 화면 크기를 명시적으로 지정합니다.
  await page.setViewportSize({
    width: 390,
    height: 844,
  });

  // 이벤트 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // 모바일 전용 sticky CTA를 찾습니다.
  const mobileCta = page.getByTestId('event-mobile-sticky-cta');

  // 모바일 CTA가 보이는지 확인합니다.
  await expect(mobileCta).toBeVisible();

  // 모바일 CTA를 클릭합니다.
  await mobileCta.click();

  // 신청 폼이 viewport 안에 들어왔는지 확인합니다.
  await expect(page.getByTestId('event-application-form')).toBeInViewport();
});
 

playwright.config.ts에서 모바일 프로젝트 분리하기

 
// playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // E2E 테스트가 위치한 폴더를 지정합니다.
  testDir: './e2e/specs',

  // CI에서는 flaky 테스트를 줄이기 위해 재시도를 1회만 허용합니다.
  retries: process.env.CI ? 1 : 0,

  // CI에서는 안정성을 위해 worker 수를 줄입니다.
  workers: process.env.CI ? 1 : undefined,

  use: {
    // page.goto('/events/...')처럼 상대 경로를 사용할 수 있게 합니다.
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',

    // 실패 재시도 시 trace를 남겨 원인 분석에 사용합니다.
    trace: 'on-first-retry',
  },

  projects: [
    {
      // 데스크톱 Chrome 기준 테스트입니다.
      name: 'desktop-chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      // 모바일 Chrome 기준 테스트입니다.
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
});
 

CI에서 worker를 1로 설정하는 것은 안정성과 재현성을 우선할 때 도움이 됩니다. Playwright 공식 CI 문서도 CI 환경에서 안정성과 재현성을 위해 worker를 1로 설정하는 예를 안내합니다.

16. visual regression test와 mask 처리

Playwright visual regression은 “랜딩페이지가 완전히 같은지”를 보기 위한 도구가 아닙니다. 이벤트 페이지에서는 다음처럼 제한적으로 쓰는 편이 좋습니다.

  • hero 영역이 깨졌는지
  • CTA가 화면 밖으로 밀렸는지
  • 신청 폼 레이아웃이 무너졌는지
  • 모바일 sticky CTA가 가려졌는지
  • 중요한 섹션이 아예 렌더링되지 않았는지

반면 다음 영역은 시각적 비교에서 제외하거나 mask 처리해야 합니다.

  • 카운트다운
  • 오늘 날짜
  • 랜덤 배너
  • 랜덤 추천 상품
  • 외부 광고 iframe
  • 실시간 재고/신청자 수
  • A/B 테스트 영역
  • 움직이는 애니메이션

visual screenshot test에서 동적 영역 mask 처리 예제

 
// e2e/specs/event-visual.spec.ts

import { test, expect } from '@playwright/test';

test('동적 영역을 mask 처리하고 핵심 레이아웃을 비교한다', async ({ page }) => {
  // 이벤트 페이지로 이동합니다.
  await page.goto('/events/summer-promo');

  // 폰트 로딩이나 초기 렌더링이 끝나 핵심 영역이 보이는지 먼저 확인합니다.
  await expect(page.getByTestId('event-hero')).toBeVisible();
  await expect(page.getByTestId('event-application-form')).toBeVisible();

  // 전체 페이지 스크린샷을 비교하되, 매번 달라지는 영역은 mask 처리합니다.
  await expect(page).toHaveScreenshot('summer-promo-layout.png', {
    // 스크롤 가능한 전체 페이지를 캡처합니다.
    fullPage: true,

    // CSS animation과 transition 차이로 인한 흔들림을 줄입니다.
    animations: 'disabled',

    // 매번 값이 달라지는 영역을 비교에서 제외합니다.
    mask: [
      page.getByTestId('event-countdown'),
      page.getByTestId('event-random-banner'),
      page.getByTestId('event-today-date'),
      page.getByTestId('event-live-apply-count'),
    ],

    // 아주 작은 렌더링 차이를 허용합니다.
    maxDiffPixels: 100,
  });
});
 

Playwright의 toHaveScreenshot()은 기준 스크린샷을 만들고 이후 실행에서 비교합니다. mask 옵션은 지정한 locator 영역을 overlay로 가려 비교할 수 있게 하며, animations: 'disabled'는 스크린샷 시 애니메이션으로 인한 차이를 줄이는 데 사용됩니다.

17. CI/CD에 붙이는 운영 전략

랜딩페이지 테스트 자동화는 “언제 어떤 테스트를 돌릴지”가 중요합니다. 모든 테스트를 Pull Request마다 돌리면 느려지고, 너무 적게 돌리면 배포 후 버그를 놓칩니다.

표 4. 테스트 단계별 전략

단계실행 시점테스트 범위예시
로컬 개발 중 개발자가 UI 수정 중 빠른 단일 브라우저 테스트 CTA, 폼 필수값, 개인정보 동의
Pull Request 코드 리뷰 전 핵심 회귀 테스트 필수 섹션, CTA, 신청 폼, mock 제출
Staging 배포 QA 환경 배포 후 실제 빌드 기준 E2E API mock + 일부 실제 staging API
Production 배포 후 운영 배포 직후 읽기 전용 smoke test 페이지 접근, CTA 존재, 폼 열림, 약관 링크
정기 모니터링 매시간/매일 운영 상태 확인 랜딩페이지 200 응답, hero/CTA 표시, 분석 request 확인

GitHub Actions CI 실행 예제

 
# .github/workflows/playwright.yml

name: Playwright E2E Tests

on:
  # main 브랜치로 push될 때 실행합니다.
  push:
    branches: [main]

  # main 브랜치로 들어오는 Pull Request에서 실행합니다.
  pull_request:
    branches: [main]

jobs:
  test:
    # 너무 오래 멈춘 테스트를 방지하기 위한 제한 시간입니다.
    timeout-minutes: 60

    # Ubuntu 최신 runner를 사용합니다.
    runs-on: ubuntu-latest

    steps:
      # 저장소 코드를 체크아웃합니다.
      - uses: actions/checkout@v5

      # Node.js LTS 버전을 설치합니다.
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*

      # package-lock.json 기준으로 의존성을 설치합니다.
      - name: Install dependencies
        run: npm ci

      # TypeScript 타입 오류를 별도로 확인합니다.
      - name: Run type checks
        run: npx tsc -p tsconfig.json --noEmit

      # Playwright 브라우저와 OS 의존성을 설치합니다.
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      # PR 단계에서 안전한 테스트만 실행합니다.
      - name: Run Playwright tests
        run: npx playwright test --project=desktop-chromium

      # 실패 원인 분석을 위해 HTML report를 artifact로 업로드합니다.
      - name: Upload Playwright report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v5
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
 

Playwright 공식 CI 문서는 GitHub Actions에서 checkout, Node 설치, npm ci, npx playwright install --with-deps, npx playwright test, HTML report artifact 업로드 흐름을 예시로 제공합니다.

또한 Playwright는 TypeScript를 바로 실행할 수 있지만, 타입 검사를 수행하지 않는 비치명적 TypeScript 오류가 있어도 테스트가 실행될 수 있으므로 tsc --noEmit을 함께 실행하는 것을 권장합니다.

PR 단계와 배포 후 테스트 구분

PR 단계에서는 실제 고객 데이터, 실제 이메일 발송, 실제 결제, 실제 CRM 등록이 일어나면 안 됩니다. 이 단계에서는 API mock 또는 테스트 서버를 사용해 빠르고 안전하게 핵심 기능을 검증합니다.

운영 배포 후에는 실제 제출까지 자동화하지 않는 것이 일반적으로 안전합니다. 운영에서는 “페이지가 접근되는지”, “CTA가 보이는지”, “폼이 열리는지”, “약관 링크가 살아 있는지”, “분석 이벤트 요청이 발생하는지” 같은 smoke test 중심으로 구성하는 편이 좋습니다.

실제 제출 완료까지 확인해야 한다면 운영과 분리된 테스트 이벤트, 테스트 전용 이메일 도메인, 발송 차단 플래그, CRM 제외 규칙을 반드시 마련해야 합니다.

18. 초보자가 자주 하는 실수

표 5. 초보자가 자주 하는 실수

실수왜 문제가 되는가해결 방법
모든 문구를 exact match로 검증 마케팅 카피 수정 때마다 테스트 실패 핵심 문구만 정규식 또는 test id로 검증
nth-child CSS selector 사용 섹션 순서 변경에 매우 취약 getByRole, getByLabel, getByTestId 사용
time.sleep 비슷한 고정 대기 사용 빠른 환경에서는 낭비, 느린 환경에서는 실패 auto-retrying assertion 사용
운영 데이터에 테스트를 의존 이벤트 마감, 랜덤 배너, CMS 수정으로 테스트 흔들림 API mock 또는 fixture 데이터 사용
시각적 비교에서 동적 영역을 제외하지 않음 날짜, 카운트다운, 랜덤 이미지 때문에 매번 실패 mask, stylePath, 안정된 CI 환경 사용
테스트를 너무 느슨하게 작성 실제 버그가 있어도 통과 전환 흐름은 엄격하게, 표현은 유연하게 검증
실패 원인을 추적할 trace/report를 남기지 않음 CI 실패 원인 파악이 어려움 HTML report, trace, screenshot artifact 저장

Playwright Trace Viewer는 CI에서 실패한 테스트를 디버깅하는 데 유용하며, 공식 문서는 CI에서는 실패한 테스트의 첫 retry에서 trace를 남기도록 trace: 'on-first-retry' 설정을 안내합니다.

19. 실무 체크리스트

  • 이벤트 페이지별로 “변해도 되는 것”과 “변하면 안 되는 것”을 문서화했다.
  • CTA, 신청 폼, 제출 버튼, 완료 메시지에 안정적인 data-testid를 부여했다.
  • CTA는 getByRole 또는 getByTestId로 찾고, CSS nth-child에 의존하지 않는다.
  • 폼 입력은 가능하면 getByLabel로 찾는다.
  • 필수값 누락, 이메일 형식 오류, 개인정보 동의 누락을 테스트한다.
  • 실제 고객 정보, 실제 이메일 발송, 실제 결제가 발생하지 않도록 mock 또는 테스트 환경을 사용한다.
  • 이벤트 데이터가 CMS/API에서 내려온다면 API mock으로 주요 상태를 고정한다.
  • 선택 섹션은 설정값 기반으로 조건부 검증한다.
  • 모바일 viewport에서 sticky CTA와 신청 폼 이동을 확인한다.
  • visual regression은 전체 디자인 승인 용도가 아니라 레이아웃 붕괴 감지용으로 사용한다.
  • 카운트다운, 날짜, 랜덤 배너, 외부 iframe은 visual test에서 mask 처리한다.
  • PR 단계 테스트와 배포 후 운영 smoke test를 분리한다.
  • CI에서 HTML report와 trace를 남긴다.
  • 테스트가 너무 느슨해져 실제 버그를 놓치지 않는지 정기적으로 리뷰한다.
  • 디자인 완성도, 카피 품질, 마케팅 메시지는 수동 QA 또는 리뷰 프로세스로 확인한다.

20. 최종 결론

이벤트 페이지가 매번 다르다는 이유로 테스트 자동화를 포기할 필요는 없습니다. 다만 페이지 전체를 똑같이 만들려고 하면 실패합니다.

이벤트 페이지 테스트 자동화의 핵심은 변하지 않는 전환 계약입니다.

  • hero 영역이 존재하는가?
  • CTA가 보이고 작동하는가?
  • 신청 폼이 열리는가?
  • 필수 입력값 검증이 되는가?
  • 개인정보 동의가 강제되는가?
  • 제출 후 완료 상태가 보이는가?
  • 모바일에서도 같은 흐름이 가능한가?
  • API 데이터가 바뀌어도 테스트가 안정적인가?

Playwright 테스트 자동화는 이런 회귀 버그 방지에 강합니다. 특히 Playwright data-testid, Playwright Page Object Model, Playwright API mock, Playwright visual regression, Playwright CI를 함께 설계하면 매번 다른 페이지 테스트도 유지보수 가능한 구조로 만들 수 있습니다.

반대로 자동화가 모든 것을 해결한다고 믿으면 안 됩니다. 카피 품질, 디자인 완성도, 브랜드 메시지, 이벤트 혜택의 설득력은 여전히 사람이 봐야 할 수 있습니다. 좋은 랜딩페이지 QA는 자동화와 수동 검토를 나누는 데서 시작합니다.

6. 비교표

위 본문에는 다음 5개 표를 포함했습니다.

  • 표 1. 변해도 되는 것과 변하면 안 되는 것
  • 표 2. 이벤트 페이지 테스트 항목
  • 표 3. locator 전략 비교
  • 표 4. 테스트 단계별 전략
  • 표 5. 초보자가 자주 하는 실수

7. 코드 예제

위 본문에는 다음 TypeScript 기반 Playwright Test 예제를 포함했습니다.

  1. Playwright 설치 명령어
  2. 기본 테스트 파일 구조
  3. 이벤트 페이지 설정 배열 예제
  4. getByRole로 CTA 버튼 찾기
  5. getByTestId로 핵심 섹션 찾기
  6. 신청 폼 입력 테스트
  7. 개인정보 동의 체크박스 테스트
  8. 제출 완료 메시지 검증
  9. 선택 섹션이 있을 때 조건부 검증
  10. Page Object Model 예제
  11. API mock 예제
  12. visual screenshot test에서 동적 영역 mask 처리 예제
  13. 모바일 viewport 테스트 예제
  14. GitHub Actions 또는 CI 실행 예제

8. 실무 체크리스트

랜딩페이지 QA에 바로 적용할 때는 아래 순서로 시작하는 것이 좋습니다.

  1. 이벤트 페이지 공통 계약을 먼저 정합니다: hero, CTA, form, submit, success.
  2. data-testid 네이밍을 정합니다: event-hero, event-primary-cta, event-application-form.
  3. CTA와 폼은 사용자 관점 locator와 test id를 함께 검토합니다.
  4. 이벤트별 설정값을 배열 또는 JSON으로 분리합니다.
  5. API mock으로 마감 상태, 신청 가능 상태, 제출 성공 응답을 고정합니다.
  6. visual regression은 동적 영역 mask를 먼저 정한 뒤 도입합니다.
  7. PR에서는 빠른 핵심 테스트만 돌리고, staging에서는 더 넓은 E2E를 돌립니다.
  8. production에서는 실제 제출 없이 smoke test 중심으로 운영합니다.
  9. 실패 시 trace와 HTML report를 확인할 수 있게 CI artifact를 남깁니다.
  10. 테스트가 너무 빡빡하거나 너무 느슨하지 않은지 캠페인 종료 후 회고합니다.

9. FAQ

Q1. 이벤트 페이지가 매번 다른데 자동화 테스트가 가능한가요?

가능합니다. 다만 페이지 전체의 디자인과 문구를 모두 고정값으로 검증하는 방식은 적합하지 않습니다. CTA, 신청 폼, 개인정보 동의, 제출 완료처럼 변하면 안 되는 기능 계약을 중심으로 테스트해야 합니다. 카피 품질과 디자인 완성도는 자동화보다 수동 QA가 더 적합할 수 있습니다.

Q2. Playwright로 랜딩페이지 테스트를 어떻게 하나요?

먼저 랜딩페이지에서 전환에 필요한 핵심 흐름을 정합니다. 예를 들어 hero 노출, CTA 클릭, 폼 입력, 필수값 검증, 개인정보 동의, 제출 완료 메시지를 테스트합니다. Playwright에서는 getByRole, getByLabel, getByTestId를 조합해 안정적인 locator를 만들고, API mock으로 데이터 변동성을 줄입니다. Playwright는 locator, assertion, mock, screenshot, CI 실행을 함께 지원합니다.

Q3. data-testid를 많이 넣어도 괜찮나요?

핵심 계약이 필요한 요소에는 넣어도 괜찮습니다. 하지만 모든 div와 span에 무분별하게 넣으면 테스트 코드가 구현 세부사항에 너무 묶일 수 있습니다. CTA, 신청 폼, 제출 버튼, 완료 메시지처럼 비즈니스 흐름상 중요한 요소에만 붙이는 것이 좋습니다. 사용자에게 보이는 접근성 기준이 중요한 곳은 getByRole이나 getByLabel도 함께 고려해야 합니다.

Q4. 문구가 자주 바뀌는 페이지는 어떻게 테스트하나요?

문구 전체를 exact match로 검증하지 말고 핵심 의미만 정규식으로 검증합니다. 예를 들어 “신청이 완료되었습니다”가 “접수가 완료되었습니다”로 바뀔 수 있다면 /완료|접수/처럼 의미 중심으로 확인합니다. CTA는 문구가 자주 바뀐다면 data-testid로 기능 계약을 잡고, role locator로 접근성도 보조 검증하는 방식이 좋습니다.

Q5. 이미지나 배너가 바뀌면 스크린샷 테스트가 계속 실패하지 않나요?

그럴 수 있습니다. 그래서 이벤트 페이지에서는 visual regression test를 전체 디자인 승인 용도로 쓰기보다 레이아웃 붕괴 감지용으로 제한하는 편이 좋습니다. 카운트다운, 날짜, 랜덤 이미지, 외부 iframe 같은 동적 영역은 mask 또는 stylePath로 제외해야 합니다. Playwright screenshot assertion은 mask와 차이 허용 옵션을 제공합니다.

Q6. 이벤트 신청 폼은 어떤 항목을 테스트해야 하나요?

최소한 이름, 이메일 같은 필수 입력값, 잘못된 이메일 형식, 개인정보 동의 체크박스, 제출 버튼 활성화, 제출 완료 메시지를 테스트해야 합니다. 운영 환경에서 실제 고객 정보 입력이나 이메일 발송이 발생하지 않도록 테스트 API 또는 mock을 사용하는 것이 안전합니다. 추가로 모바일 화면에서 input이 가려지지 않는지, 키보드 사용 흐름이 막히지 않는지도 확인하면 좋습니다.

Q7. API 데이터가 계속 바뀌면 E2E 테스트는 어떻게 안정화하나요?

테스트에서 필요한 이벤트 데이터를 mock으로 고정합니다. 예를 들어 이벤트 상태를 항상 OPEN으로 내려주고, 제출 API는 201 Created와 테스트용 신청 ID를 반환하게 만들 수 있습니다. 이렇게 하면 CMS 수정, 이벤트 마감, 랜덤 추천 상품 때문에 테스트가 흔들리는 문제를 줄일 수 있습니다. Playwright는 page.route()로 네트워크 요청을 mock할 수 있습니다.

Q8. Playwright와 Selenium 중 랜딩페이지 테스트에는 무엇이 좋나요?

둘 다 웹 자동화에 사용할 수 있습니다. 랜딩페이지 테스트에서는 locator, auto-waiting, test runner, assertion, trace, screenshot, CI 도구가 한 번에 제공되는지가 중요합니다. Playwright는 이러한 기능을 통합 제공하므로 현대적인 프론트엔드 테스트 자동화와 Playwright E2E 테스트를 빠르게 구성하기 좋습니다. 단, 이미 Selenium 기반 QA 인프라가 잘 갖춰져 있다면 전환 비용도 함께 고려해야 합니다.

Q9. 테스트를 너무 유연하게 만들면 버그를 놓치지 않나요?

맞습니다. 문구와 이미지에는 유연해야 하지만, 전환 흐름에는 엄격해야 합니다. CTA가 사라졌거나, 개인정보 동의 없이 제출되거나, 완료 메시지가 나오지 않는 문제는 반드시 실패해야 합니다. 좋은 이벤트 페이지 테스트 자동화는 “표현은 유연하게, 기능 계약은 엄격하게” 설계하는 것입니다.

Q10. 배포 후 실제 운영 페이지도 자동 테스트해야 하나요?

하는 것이 좋지만, 범위를 조심해야 합니다. 운영 페이지에서는 실제 신청, 결제, 이메일 발송, CRM 등록이 일어나지 않도록 읽기 전용 smoke test 중심으로 구성하는 것이 안전합니다. 페이지 접근, hero 표시, CTA 표시, 폼 열림, 약관 링크, 분석 이벤트 요청 정도를 확인할 수 있습니다. 실제 제출 완료까지 확인해야 한다면 테스트 전용 이벤트와 발송 차단 장치가 필요합니다.

반응형