안녕하세요 ~ 프론트엔드 개발자 이효린입니다 :D
혹시 이런 경험 있으신가요?
- 테스트 코드를 쓰고 싶지만, 작성 비용이 높아서 미뤄지는 상황
- 테스트를 도입했지만, 결국 몇 명만 작성하게 되는 구조
- AI로 테스트 코드를 생성해봤지만, 실제로는 수정이 더 오래 걸리는 경험
저희 팀도 비슷한 문제를 겪고 있었습니다.
저희 팀이 딱 이 세 가지를 겪고 있었어요. 😇
이 글은 QA 인력 없이 E2E 테스트를 도입하면서, AI가 정확한 테스트 코드를 생성할 수 있는 구조를 어떻게 설계했는지 정리한 글입니다.
왜 E2E였나요?
사실 처음부터 E2E를 생각한 건 아니었어요. 유닛 테스트, 통합 테스트도 고려했거든요.
근데 저희 팀 상황을 생각해보면,
- 수동 검증이 이미 "사용자 플로우 단위" 로 이루어지고 있었어요
- "이 버튼 누르면 이 모달이 뜨고, 확인 누르면 이 화면으로 가는지"를 확인하는 게 핵심이었거든요
유닛 테스트로는 이 흐름 전체를 검증하기 어려웠어요. E2E가 기존 수동 검증을 그대로 자동화해줄 수 있는 가장 직접적인 방법이라고 판단했습니다.
프레임워크는 Playwright를 선택했어요. Cypress도 검토했는데,
- 병렬 실행을 기본 지원해서 테스트 속도가 빠르고
- getByRole, getByLabel 같은 접근성 기반 selector가 기본 제공되고
- trace, screenshot, video 자동 수집 기능이 있어서 실패 원인을 파악하기 쉬웠거든요 :)
핵심 과제: 팀 전체가 테스트를 쓸 수 있어야 한다
Playwright를 도입하긴 했는데, 진짜 문제는 따로 있었어요.
Playwright 문법을 모르면 테스트를 쓸 수 없다는 것.
저희 팀은 QA 인력이 없어서, 개발자 전원이 테스트를 작성할 수 있어야 했어요. 근데 현실적으로 모든 팀원이 Playwright API를 깊이 익히기엔 시간이 부족했죠.
그래서 방향을 바꿨어요.
"사람이 Playwright를 배우는 대신, AI가 테스트 코드를 대신 작성하게 하자."
단, AI한테 "알아서 잘 써줘"라고 하면 안 된다는 걸 금방 깨달았어요. 규칙 없이 Playwright API를 그대로 쓰게 하면 AI도 매번 다른 스타일로 불안정한 코드를 생성하거든요. UI가 바뀌면 테스트도 다 터지고, 같은 패턴도 열 군데에 풀어서 작성하고요.
결론: AI가 올바른 코드를 생성하려면, AI가 따를 수 있는 규칙과 구조가 먼저 필요하다.
이 문제를 해결하기 위해 세 가지를 설계했습니다.
- Component Helper — AI가 사용할 추상화된 API
- Selector 우선순위 규칙 — AI가 어떤 selector를 써야 하는지의 기준
- Self-Healing 루프 — AI가 직접 실행하고, 실패를 분류해서 스스로 수정하는 자가 복구 구조
1. Component Helper: AI가 사용할 API 만들기
핵심 아이디어는 이거예요.
Playwright의 저수준 API를 감싸서, AI가 자연어에 가까운 메서드로 테스트를 작성하게 만들자.
// Before — AI가 저수준 API를 직접 조합
await page.getByLabel('이름').fill('테스트 에이전트');
await page.getByLabel('설명').fill('설명입니다');
await page.getByRole('button', { name: '저장' }).click();
await page.getByRole('button', { name: '생성' }).click();
// After — AI가 Helper 메서드만 호출
await form.fillFields({ 이름: '테스트 에이전트', 설명: '설명입니다' });
await form.submit('저장');
await dialog.clickConfirm('생성');
AI 입장에서 보면, form.fillFields()라는 하나의 메서드만 알면 폼 입력을 처리할 수 있어요. Playwright의 getByLabel, fill, click 같은 저수준 API 조합을 몰라도 되는 거죠. 저희 서비스는 사내 디자인 시스템을 100% 사용하고 있어서, Helper도 디자인 시스템 컴포넌트와 1:1로 매핑해서 만들었어요. 대표적인 것들을 소개하면,
Helper 디자인 시스템 컴포넌트 주요 메서드
| DialogHelper | Dialog / Modal | waitForOpen(), clickConfirm(), close() |
| FormHelper | Form / Input | fillFields(), submit(), expectErrors() |
| SelectHelper | Select / Combobox | selectByLabel(), selectFirstOption() |
| TableHelper | DataTable | getRowByText(), clickRowAction() |
| NavigationHelper | Tabs / Breadcrumb | clickTab(), expectUrlMatches() |
| ToastHelper | Toast / Notification | expectSuccess(), expectError() |
이 외에도 더 있지만, 이 6개가 가장 자주 쓰이는 핵심 Helper예요. 디자인 시스템과 1:1로 매핑되기 때문에 UI 컴포넌트가 추가되면 Helper도 함께 확장하는 구조입니다.
이 Helper들을 Playwright Fixture로 자동 주입해서, 테스트에서 별도 초기화 없이 바로 사용할 수 있게 했어요.
test('에이전트 생성', async ({ form, dialog, toast }) => {
await form.fillFields({ 이름: '테스트 에이전트' });
await form.submit('저장');
await dialog.clickConfirm('생성');
await toast.expectSuccess();
});
AI에게 이 6개 Helper의 메서드 목록만 가이드로 주면, Playwright를 전혀 모르는 상태에서도 정확한 테스트 코드를 생성할 수 있어요. 메서드명 자체가 자연어에 가깝기 때문에 AI가 "폼 입력 → fillFields", "모달 확인 → clickConfirm"으로 바로 매핑할 수 있거든요.
그리고 사람도 마찬가지예요. Playwright를 처음 쓰는 팀원도 Helper 메서드만 보면 바로 테스트를 작성할 수 있게 됐어요 :)
2. Selector 우선순위: AI가 어떤 요소를 잡아야 하는지의 기준
Helper로 API를 추상화했지만, Helper 내부에서 실제 DOM 요소를 찾는 방식도 규칙이 필요했어요. AI가 테스트를 작성할 때 selector를 자유롭게 선택하면 이런 문제가 생기거든요.
// AI가 자유롭게 selector를 선택하면...
// 의도: 데이터소스 목록에서 첫 번째 파일 클릭
// 실제 결과: LNB에 있는 요소를 가져옴 😅
await page.getByRole('listitem').first().click();
같은 페이지에 비슷한 구조가 여러 개 있으면, AI가 엉뚱한 요소를 잡는 코드를 생성해요. 그래서 AI가 따라야 할 Selector 우선순위 규칙을 정했습니다.
1순위: ARIA role → getByRole('button', { name: '저장' })
2순위: ARIA 속성 → getByLabel('이름')
3순위: data-slot → [data-slot="dialog-title"] (디자인 시스템 기반)
그리고 요소가 겹칠 때는 영역 스코프로 좁혀서 찾는 규칙도 추가했어요.
// 전체 페이지에서 찾지 않고, 특정 영역 안에서 찾기
const dataSourceArea = page.getByTestId('datasource-content');
await dataSourceArea.getByRole('button', { name: '폴더 생성' }).click();
`getByText(/A|B|C/)` 처럼 여러 후보를 한 번에 매칭하는 건 Strict Mode 위반이라 금지했어요. 에러 메시지는 개별 locator로 각각 검증하는 방식으로 통일했습니다.
이 규칙을 AI 에이전트의 가이드에 포함시켜서, AI가 생성하는 모든 테스트 코드가 동일한 selector 전략을 따르도록 만들었어요. 덕분에
AI가 작성한 코드도 사람이 작성한 코드와 일관된 스타일을 유지합니다.
3. Self-Healing 구조: AI가 스스로 테스트를 고치는 루프
AI가 테스트 코드를 생성했다고 해서 항상 한 번에 통과하는 건 아니에요. selector가 미묘하게 안 맞거나, 비동기 타이밍 이슈가 있거나, UI가 변경된 경우도 있거든요.
그래서 AI가 직접 테스트를 실행하고, 실패하면 원인을 분석해서 스스로 수정하는 self-healing 루프를 설계했어요.
작성 → 실행 → 분석 → 수정 루프
AI 에이전트가 테스트를 작성한 후 따르는 흐름이에요.
1. 테스트 코드 작성
2. 직접 실행 (pnpm exec playwright test)
3. 결과 확인
- 통과 → 완료!
- 실패 → 에러 로그 + trace 스크린샷 분석
4. 실패 원인 분류 → 수정 가능하면 코드 수정 후 2단계로
5. 최대 3회 반복. 그래도 실패하면 원인과 함께 리포트
핵심은 AI가 "실패했습니다"로 끝내는 게 아니라, 에러 로그와 Playwright trace의 스크린샷을 직접 분석해서 왜 실패했는지 판단한다는 거예요.
AI가 따르는 실패 분류 기준
AI가 모든 실패를 무작정 고치면 안 되겠죠. 실제 앱 버그를 테스트 코드 수정으로 덮어버리면 위험하니까요. 그래서 실패 원인을 4가지로 분류하는 기준을 만들었어요.
| 분류 | 의미 | AI 행동 |
| UI_CHANGE | UI가 의도적으로 바뀌어서 테스트가 깨진 것 | trace 스크린샷에서 현재 UI 확인 후 셀렉터 수정 |
| TEST_BUG | 테스트 코드 자체의 문제 (스코프 누락, 대기 부족 등) | 코드 수정 |
| APP_BUG | 앱에 실제 버그가 있는 것 | 수정하지 않고 리포트 |
| ENV_ISSUE | 서버 미기동, 네트워크 문제 등 | 수정하지 않고 리포트 |
여기서 앞서 만든 Selector 우선순위 규칙과 Helper 사용 규칙이 빛을 발해요. 예를 들어 strict mode violation(2개 이상 요소 매칭) 에러가 나면, AI는 규칙에 따라 영역 스코프를 좁히거나 placeholder로 특정하는 식으로 수정해요.
// 수정 전 — 2개 요소가 매칭돼서 실패
page.getByRole('tabpanel').locator('input[type="text"]')
// 수정 후 — Selector 규칙에 따라 placeholder로 특정
page.getByRole('tabpanel').getByPlaceholder('검색어를 입력하세요')
규칙이 없었다면 AI는 실패를 고칠 때마다 제각각 다른 방식으로 수정했을 거예요. 규칙이 있으니까 매번 일관된 방식으로 자가 수정할 수 있는 거죠.
실제로 얼마나 차이가 나나요?
이 효과는 직접 겪어봤을 때 확실히 느꼈어요.
같은 테스트 케이스 작성을 Claude에게 직접 요청했을 때는, 5~6번 수정을 주고받아도 계속 실패했어요. selector가 미묘하게 안 맞거나 타이밍 이슈가 있어도, 실행 결과를 보지 못하는 상태에서는 원인을 제대로 짚기가 어려웠던 거죠.
반면 self-healing 루프가 갖춰진 에이전트에게 같은 작업을 요청하니 1번에 통과했어요. 에이전트가 직접 실행하고, trace 스크린샷을 보
면서 원인을 분류하고, 규칙에 따라 수정하는 과정을 자율적으로 돌렸기 때문이에요.
"AI가 실행 결과를 볼 수 있느냐" 가 이 정도 차이를 만들어요.
Helper가 변경을 흡수하는 구조
그리고 UI가 변경되었을 때도, Helper 구조 덕분에 수정 범위가 최소화돼요.
// 버튼 텍스트가 '저장' → '등록'으로 바뀐 경우
// Helper 내부만 수정하면 끝
class FormHelper {
async submit(label = '등록') { // 한 줄만 변경
await this.page.getByRole('button', { name: label }).click();
}
}
테스트 코드 자체는 `await form.submit()`이라 수정할 필요가 없어요. AI가 self-healing 루프에서 Helper 내부만 고치면 전체 테스트가 복구되는 구조예요.
AI 에이전트로 테스트 코드 자동 생성하기
위의 세 가지 구조가 갖춰지고 나서, 본격적으로 자연어 입력 → 테스트 코드 변환 AI 에이전트를 만들었어요. AI 에이전트에게 앞서 설계한 Helper 목록, Selector 규칙, 금지 패턴을 가이드로 전달하고, 이렇게 자연어로 입력하면
에이전트 생성 페이지에서
- 이름, 설명, 모델을 입력하고
- 저장 버튼 누르고
- 생성 모달에서 확인 누르고
- 성공 토스트가 뜨는지 확인하는 테스트 작성해줘
아래처럼 규칙을 준수하는 완성된 테스트 코드가 나옵니다.
test('에이전트 정상 생성', async ({ page, form, dialog, toast }) => {
await page.goto('/ai-agent/create');
await form.fillFields({
'에이전트 이름': '테스트 에이전트',
'설명': '테스트용 에이전트입니다',
});
await form.submit('저장');
await dialog.clickConfirm('생성');
await toast.expectSuccess();
});
AI가 저수준 Playwright API를 자유롭게 조합하는 게 아니라, 미리 정의된 Helper 메서드와 규칙 안에서 코드를 생성하기 때문에 결과물의 품질이 일정해요. 기존에 테스트 코드를 전혀 안 쓰던 팀원도 AI를 통해 테스트를 작성할 수 있게 됐습니다 :D
CI는 어떻게 연동했나요?
처음에 "모든 PR마다 E2E 실행"을 고려했는데, 한 번 돌리는데 40초 정도 걸리다 보니 개발 속도에 영향이 있더라고요.
그래서 "필요할 때 돌리되, 결과는 반드시 공유한다" 는 전략을 택했어요.
현재 구현: 라벨 기반 실행 + Mattermost 알림
PR에 e2e 또는 release 라벨을 붙이면 테스트가 자동으로 실행되고, 결과가 팀 메신저 채널로 바로 날아와요.
on:
pull_request:
types: [labeled] # e2e 또는 release 라벨 추가 시 실행
실행 결과는 이렇게 전달돼요.
- ✅ 전체 통과 → "E2E 테스트 통과 (25/25)"
- ❌ 실패 시 → 실패한 테스트 목록 + trace 링크
핵심 기능을 건드린 PR, 릴리즈 직전 PR에만 붙이는 방식이라 개발 속도를 유지하면서 필요한 시점에만 검증할 수 있어요.
실제로 개발서버 -> 운영서버로 배포 시 사용하고 있어요 !

목표: 매일 오전 7시 자동 실행
현재 구현 중인 다음 단계는 야간 스케줄 실행이에요. 매일 오전 7시에 전체 테스트를 돌리고, 결과를 팀 채널에 쏴주는 구조를 목표로 하고 있습니다.
schedule:
- cron: '0 22 * * *' # 매일 오전 7시 (KST)
출근하면 "어젯밤 빌드는 이상 없음" 알림이 와 있는 그림이에요 😄
처음부터 모든 걸 자동화하려고 하면 부담이 커요. 지금처럼 "핵심 PR에만 라벨 붙여서 검증"하는 것부터 시작하는 게 팀에 부담 없이 E2E를 정착시키는 방법이었던 것 같아요.
현재 상황은?
인프라가 갖춰진 현재 상황을 공유하면,
- 총 38개 테스트, 867줄
- 커버리지: 약 22% (AI Agent / Data Source / Chat / Workflow 기본 플로우)
- 수동 검증 시간: 약 10분 → 30~60초로 단축
커버리지가 아직 낮은 건 사실이에요. 하지만 인프라(Helper, Selector 규칙, Self-Healing, CI)가 갖춰져 있으니까 AI로 테스트를 추가하는 비용이 많이 낮아졌어요. 로드맵상 목표는 42개 파일 / 250+ 테스트입니다 😤
마치며
이 과정에서 느낀 게 있어요.
AI를 활용한 테스트 자동화에서 가장 중요한 건 "AI에게 좋은 코드를 작성하는 환경을 만들어주는 것" 이었어요.
AI한테 "테스트 짜줘"라고 하는 건 쉽지만, 그렇게 나온 코드가 유지보수 가능하고 팀 컨벤션에 맞으려면 AI가 따를 수 있는 구조와 규칙이 먼저 필요하더라고요.
- Component Helper — AI가 사용할 추상화된 API
- Selector 우선순위 — AI가 어떤 요소를 어떻게 잡을지의 기준
- Self-Healing 루프 — AI가 직접 실행하고, 실패를 분류하고, 스스로 수정하는 자가 복구 구조
이 세 가지를 갖추고 나니, AI가 생성한 코드도 사람이 작성한 것과 동일한 품질을 유지할 수 있게 됐어요. 그리고 그 결과, Playwright를 모르는 팀원도 AI를 통해 테스트를 작성할 수 있는 구조가 만들어졌습니다.
완벽한 커버리지보다, 팀 전체가 AI를 활용해 부담 없이 테스트를 추가할 수 있는 구조를 먼저 만드는 것. 그게 QA 없는 팀에서 테스트를 정착시키는 첫 번째 단계였던 것 같아요 :)
궁금한 점이 있으시면 댓글로 남겨주세요! 🙌
