E2E as a Deploy Gate
2024년 봄 운영 중인 고객사 서비스에 Playwright E2E를 붙이던 중 특정 케이스가 실패했는데 고객사는 그 실패와 상관없이 배포하기를 원했습니다. 실패 신호는 버리지 않으면서 배포는 막지 않으려고 E2E를 빌드에서 떼어 사내 서버 배포 뒤에 돌리고 고객사 배포는 사람이 하는 흐름으로 정리했습니다. 2026년 SK AX GAS Position Play에서는 E2E가 맡을 범위 자체를 화면 사이 연결로 줄였습니다.
실패하는 케이스를 빼면 신호도 함께 사라집니다
가장 쉬운 답은 운영에서 정한 그 케이스를 E2E에서 빼는 것이었습니다. 빼고 나면 배포는 나가지만 E2E를 돌려도 그 케이스가 아직 실패하는지 알 수 없습니다. 실패를 판단하는 일과 배포를 막는 일이 한 조건에 묶여 있어서 생긴 문제였습니다.
그래서 요구를 다시 적었습니다. E2E에서는 실패로 기록이 남아야 하고 배포는 나가야 합니다. 이렇게 적고 나면 케이스를 넣을지 뺄지가 아니라 E2E를 배포 흐름의 어디에 둘지가 질문이 됩니다.
E2E를 빌드에 묶으면 롤백이 테스트 코드까지 끌고 갑니다
E2E를 빌드의 한 단계로 넣으면 구성은 단순합니다. E2E를 통과한 빌드만 배포물이 됩니다. 논의에서는 새 기능을 운영에 배포한 뒤 고객사가 그 기능 전체를 되돌려 달라고 하는 경우를 따져 봤습니다. 지금까지는 이전 시점의 코드로 돌아가 다시 배포하면 끝났습니다. E2E가 빌드 조건이면 이전 시점의 앱도 E2E를 통과해야 하므로 되돌릴 때 E2E 코드까지 다시 고쳐야 합니다. 롤백 절차에 테스트 수정이 끼어드는 셈입니다.
반대쪽 끝에는 배포 뒤에 E2E를 따로 돌리는 안이 있었습니다. E2E 코드를 별도 모듈로 떼어 배포와 관계없이 실행하고 결과를 로그로 남기면 개발자는 언제든 로그를 보고 고칠 수 있습니다. 배포는 막지 않지만 배포 전에 문제를 거르는 역할은 내려놓는 안입니다.
배포 전 환경에서 돌리면 그 환경의 데이터가 기준이 됩니다
배포하기 전에 E2E를 돌릴 수 있는지도 살폈습니다. E2E를 로컬 서버가 아니라 배포된 환경을 대상으로 돌린다면 배포 전에는 주소로 들어가도 이전 버전이 열립니다. 빌드, 업로드, E2E 순서는 결국 배포 뒤의 E2E입니다. 빌드, E2E, 성공 시 업로드 순서로 두면 E2E가 어디에 접속하느냐는 질문이 남습니다.
그 답은 스테이징 같은 배포 전 환경입니다. 스테이징에서 돌리면 스테이징 데이터와 서버 환경이 기준이 됩니다. 테스트가 보는 화면은 그 환경에 쌓인 데이터로 그려지므로 코드가 그대로여도 데이터가 바뀌면 결과가 달라질 수 있습니다.
시나리오를 쓸 때도 데이터가 걸렸습니다. 콘텐츠 목록에 유형이 다른 카드가 섞여 있으면 “세 번째 카드를 누른다”는 시나리오는 다음에 어떤 검증이 이어질지 미리 알 수 없습니다. 그 자리에 어떤 유형이 올지는 데이터가 정하기 때문입니다. 특정 콘텐츠를 누르는 시나리오로 고정하자는 의견과 임의의 카드를 누른 뒤 유형에 맞는 검증으로 분기하는 쪽이 사용자를 더 닮았다는 의견이 나왔고 둘 다 두기로 했습니다. 고정한 시나리오는 매번 같은 경로를 확인하고 임의 카드 시나리오는 사용자가 실제로 고르는 경로에 가깝습니다.
E2E를 사내 서버 배포와 고객사 배포 사이에 둡니다
논의 끝에 정리한 흐름은 이렇습니다. dev 브랜치에 push하면 Git 러너가 사내 서버에 배포하고 러너에 건 트리거가 그 서버를 대상으로 E2E를 실행합니다. E2E가 성공하면 수동으로 빌드해 고객사 서버에 배포합니다.
flowchart LR
Push["dev 브랜치 push"] --> Internal["Git 러너: 사내 서버에 배포"]
Internal --> E2E["러너 트리거: 사내 서버 대상 E2E"]
E2E -->|"성공"| Build["수동 빌드"]
Build --> Client["고객사 서버에 배포"]이 배치는 앞의 문제를 하나씩 비켜 갑니다. 고객사에 나가는 빌드에 E2E가 끼지 않으니 이전 시점으로 되돌려 배포할 때 E2E 코드를 먼저 고치지 않아도 됩니다. E2E는 이미 배포된 사내 서버를 대상으로 돌아 접속할 곳이 분명합니다. 고객사 배포가 사람이 하는 단계라서 구조로 보면 E2E 결과는 배포를 자동으로 막는 조건보다 배포 전에 읽는 기록에 가깝습니다. 다만 사내 서버가 곧 고객사 배포 앞의 스테이징이라 그 서버의 데이터가 기준이 된다는 점은 그대로 남았습니다.
Page Object와 시나리오 파일로 회귀 범위를 읽게 했습니다
같은 시기에 사내 문서로 E2E 코딩 컨벤션을 썼습니다. QA팀이 넘겨준 사용자 시나리오는 scenarios 폴더의 spec 파일 하나로 옮기고 시나리오가 지나는 화면은 pages 폴더에 화면마다 클래스 하나로 둡니다. 페이지 클래스의 필드에는 그 화면에서 쓰는 요소를, 메서드에는 사용자 행동을 둡니다. 시나리오는 PageManager에서 페이지 객체를 받아 행동 메서드를 차례로 부르고 권한, 상황, 입력값 같은 맥락은 테스트 위 주석에 번호를 매겨 적습니다. 이슈 트래커의 이슈 하나에 대응하는 테스트는 issues 폴더에, 상수 데이터는 파일 하나에 따로 둡니다.
import { test, type Locator, type Page } from '@playwright/test'
// pages/ListPage.ts: 화면의 요소는 필드에, 사용자 행동은 메서드에 둔다
export class ListPage {
private readonly addButton: Locator
constructor(private readonly page: Page) {
this.addButton = page.locator('button[data-id="item_add_button"]')
}
async clickAdd() {
await this.addButton.click()
}
async clickEdit(itemName: string) {
const row = this.page.locator('tr', { hasText: itemName })
await row.locator('button', { hasText: '수정' }).click()
}
}
// scenarios/item-create.spec.ts: QA 시나리오의 단계를 주석과 메서드 호출로 옮긴다
/**
* 관리자 계정으로 로그인
* 1. 목록에서 추가를 누른다
* 2. 담당자만 정해져 있어 필수 정보만 입력하고 저장한다
*/
test('관리자가 필수 정보만으로 항목을 추가한다', async ({ page }) => {
const pageManager = new PageManager(page)
await pageManager.getLoginPage().authenticate(ADMIN_ACCOUNT)
await pageManager.getListPage().clickAdd()
await pageManager.getItemFormPage().submit(REQUIRED_FIELDS_ONLY)
})화면이 바뀌면 고칠 곳은 그 화면의 페이지 클래스 하나입니다. Playwright 문서도 페이지 객체가 요소 선택자를 한곳에 모아 유지보수를 단순하게 한다고 설명합니다. 시나리오 파일은 QA 문서와 같은 단계로 읽히므로 실패한 테스트의 파일과 주석을 보면 QA 시나리오의 어느 단계가 깨졌는지 이어집니다. 거꾸로 바뀐 페이지 클래스를 쓰는 시나리오를 찾으면 그 변경이 닿는 회귀 범위가 나옵니다.
이후에는 E2E가 맡을 범위를 줄였습니다
2026년 SK AX GAS Position Play는 조건이 달랐습니다. 개발계 백엔드는 가상 데스크톱(VDI) 안에서만 접속됐습니다. 스테이징도 없어 피드백은 운영에서 바로 받았습니다. E2E는 배포된 서버가 아니라 e2e 모드로 빌드한 앱과 MSW service worker 위에서 돌렸습니다. 응답 데이터는 모킹 핸들러가 정하므로 2024년처럼 어느 서버의 데이터를 기준으로 삼을지 고를 필요가 없었습니다.
검증은 unit과 골든, integration, e2e 세 층으로 나누고 같은 상태를 두 층에서 보지 않게 했습니다. e2e 층은 화면 사이를 한 번 지나가며 연결만 보고 위젯 안의 상태는 integration이 봅니다. 어느 층이 깨졌는지를 보고 버그인지 의도한 변경인지 가늠합니다. 층을 고르는 기준과 차트 검증, 커버리지는 SK AX GAS Position Play의 화면을 더할 때마다 회귀 범위를 고정하기 절에 정리했습니다.
QA와 주고받는 방향은 반대가 됐습니다. 2024년에는 QA 시나리오를 받아 spec으로 옮겼고 2026년에는 테스트 제목을 QA와 기획이 쓰는 용어로 맞춘 뒤 테스트 주석과 실행 결과에서 시나리오 Excel을 만들었습니다. 제품 결정을 기다리는 케이스는 따로 목록으로 두었습니다.
확인 방법과 한계
2024년 부분은 본인이 쓴 E2E 코딩 컨벤션 문서(2024-04-09 판)와 2024-04-23부터 06-19까지의 작업 노트가 근거이고 해당 서비스의 코드와 실행 기록은 대조하지 못했습니다. 그래서 정리한 흐름이 실제 러너 설정으로 구현됐는지, E2E 케이스가 몇 개였고 얼마나 통과했는지는 확인하지 못했습니다. 노트에는 성공하면 배포한다고만 적혀 있어 고객사가 허용한 실패 케이스를 그 단계에서 어떻게 넘겼는지도 남아 있지 않습니다. 컨벤션의 예시 코드는 사내 샘플 저장소의 것이라 서비스 테스트 코드에 그대로 쓰였는지도 알 수 없습니다.
환경 제약도 있었습니다. 서비스가 Node 14에서 돌아 Playwright를 1.33에 고정했습니다. Playwright는 1.34부터 Node.js 14를 지원하지 않습니다(릴리스 노트). yarn start로 실행하면 localhost:3000에 접속되지 않고 Playwright의 webServer 설정이 적용되지 않는 문제도 있었지만 원인과 해결은 기록에 없습니다.
SK AX GAS Position Play 부분은 포트폴리오 페이지에 정리한 코드 대조가 근거입니다.