# Playwright × AI — codegen·MCP·자가치유 셀렉터

> AI가 브라우저를 직접 조작하는 시대. Playwright codegen과 MCP 에이전트를 실무 기준으로 짚고, MCP가 스크린샷이 아니라 접근성 트리를 읽는다는 점, 자가치유 셀렉터가 왜 Playwright 기능이 아닌지까지 정리합니다.

**Published:** 2026-08-06 | **Updated:** 2026-08-06

---


> 「프론트엔드 테스트 제대로 하기」 시리즈의 **20편**입니다. [전체 목차 보기](/series/프론트엔드-테스트-제대로-하기/) · [용어집](/posts/frontend-testing-glossary/)

AI가 테스트 코드를 '짜주는' 것까지는 이제 익숙하죠. 그런데 요즘은 한 걸음 더 나갑니다 — AI가 **브라우저를 직접 열고, 클릭하고, 잘 되는지 확인**하기 시작했어요.

신기하긴 한데, 막상 "그래서 실무에 쓸 수 있나?"를 물으면 답이 잘 안 나옵니다. 도구 이름은 많이 들리는데 각각 뭘 하는 건지, 어디까지 믿어도 되는지는 정리된 글이 드물거든요. 이 글이 그 지형도입니다.

시리즈로 따라오셨다면 [지난 편](/posts/frontend-testing-ai-generate/)의 "초안은 AI, 확정은 사람" 원칙이 브라우저로 확장되는 편이고, 검색으로 처음 오셨어도 각 도구를 따로따로 이해할 수 있게 썼습니다. Playwright를 한 번도 안 써보셨어도 괜찮아요. 필요한 개념은 나올 때마다 한 줄씩 풀어드립니다.

> 실습 코드: [frontend-testing-lab](https://github.com/IsaacEryn/frontend-testing-lab/tree/step-20) — codegen은 Playwright에 내장된 명령이고 MCP 서버는 저장소 밖에서 붙이는 것이라, `step-20`은 앞 편과 같은 상태입니다. 본문의 접근성 스냅샷은 이 저장소의 데모 앱을 그대로 찍은 것이고요.

이 글이 답하는 질문 세 가지입니다.

- 내가 클릭한 걸 그대로 테스트 코드로 받아 적을 수 있을까? (**codegen**)
- AI에게 "이 화면 좀 확인해줘"라고 말로 시킬 수 있을까? (**MCP 에이전트**)
- 셀렉터가 깨지면 AI가 알아서 고쳐준다던데, 진짜일까? (**자가치유**)

{{< img src="images/contents/ai-playwright.png" alt="codegen·에이전트·자가치유 셀렉터로 이어지는 Playwright와 AI의 결합 지형을 정리한 다이어그램" >}}

---

## codegen — 내 클릭을 코드로 받아 적기

**codegen**은 Playwright에 처음부터 들어 있는 기능입니다. 브라우저를 하나 띄워주고, 우리가 거기서 클릭하고 입력하는 걸 **옆에서 코드로 받아 적어줘요.** 테스트를 처음 쓸 때 가장 막막한 게 "이 버튼을 코드로 어떻게 가리키지?"인데, 그 부분을 대신해주는 셈이죠.

AI는 아직 등장하지 않습니다. 그냥 녹화기예요. 그런데도 이 편의 첫 자리에 둔 이유가 있는데, 뒤에서 이야기할 AI 도구들이 **여기서 나온 코드를 다듬는 방식**으로 일하거든요. 출발점부터 보는 게 순서에 맞습니다.

써보는 건 한 줄이면 됩니다.

```bash
npx playwright codegen localhost:5173
```

브라우저가 열리고, 클릭·입력이 실시간으로 코드로 기록됩니다. 데모 앱에서 검색해보면 이런 코드가 생겨요.

```ts
// codegen이 기록한 코드
await page.getByRole('searchbox', { name: '검색' }).click()
await page.getByRole('searchbox', { name: '검색' }).fill('carol')
```

codegen도 role 기반 로케이터를 우선 제안합니다([로케이터 편](/posts/frontend-testing-playwright-locators/)의 원칙과 동일). **초안 만들기**엔 훌륭하지만, 불필요한 클릭이나 취약한 셀렉터가 섞일 수 있으니 다듬어서 쓰세요.

### 어서션 기록 — 클릭만 담기는 게 아닙니다

codegen을 "동작 녹화기"로만 아는 분이 많은데, 레코더 창 위쪽 툴바에는 **어서션(검증) 기록** 버튼이 있습니다. 아이콘을 하나 고른 뒤 페이지에서 확인하고 싶은 요소를 클릭하면, 검증 코드가 그 자리에 끼워져요.

기본은 세 가지입니다.

| 버튼 | 확인하는 것 |
|---|---|
| `assert visibility` | 이 요소가 화면에 보이는가 |
| `assert text` | 이 요소가 그 텍스트를 담고 있는가 |
| `assert value` | 입력칸에 그 값이 들어 있는가 |

이게 왜 중요하냐면, **어서션 없는 녹화 결과는 테스트가 아니기 때문**입니다. 클릭과 입력만 줄줄이 나열된 파일은 "끝까지 에러 없이 실행됐다"밖에 말해주지 않아요. 화면에 엉뚱한 게 떠 있어도 초록불입니다. [1편](/posts/frontend-testing-why-now/)부터 이야기해온 "테스트가 있다 vs 테스트가 지킨다"의 차이가 여기서도 그대로예요.

> 툴바에는 네 번째 버튼 `Assert snapshot`도 있습니다(1.49부터). 페이지의 접근성 트리를 통째로 떠서 `toMatchAriaSnapshot()`에 박아두는 방식인데, 설명이 codegen 문서가 아니라 [aria 스냅샷 문서](https://playwright.dev/docs/aria-snapshots)에 있어 놓치기 쉬워요. '접근성 트리'가 뭔지는 조금 뒤 MCP 이야기에서 자세히 다룹니다.

두 번째로 알아두면 좋은 건 **로그인 상태 재사용**이에요. 로그인 뒤에 있는 화면을 기록하려는데 매번 로그인부터 해야 한다면 금세 지칩니다.

```bash
# 한 번 로그인한 뒤 세션을 파일로 저장
npx playwright codegen https://example.com --save-storage=auth.json

# 다음부터는 로그인된 상태로 시작
npx playwright codegen --load-storage=auth.json https://example.com
```

`auth.json`에는 **쿠키와 토큰이 그대로 들어갑니다.** 공식 문서도 이 파일을 `.gitignore`에 넣고 로컬에만 두라고 못 박아요. 실수로 커밋하면 계정을 통째로 넘겨주는 셈이니 저장소에 올라가지 않게 하세요.

그 밖의 옵션은 필요해질 때 찾아보시면 됩니다. 처음 읽으실 때는 지나치셔도 좋아요.

| 옵션 | 쓰임 |
|---|---|
| `--device "iPhone 11"` | 모바일 기기 화면으로 기록 |
| `--color-scheme dark` | 다크 모드 상태로 기록 |
| `--viewport-size "1280, 720"` | 특정 해상도로 기록 |
| `--target` | 출력 언어 바꾸기 (기본은 TypeScript, python·java 등도 가능) |
| `--test-id-attribute` | 프로젝트가 쓰는 테스트 ID 속성 지정 |

여기까지가 AI 없이 되는 일입니다. 이제 진짜 AI가 등장할 차례예요.

---

## MCP 에이전트 — 말로 시키는 브라우저

이번엔 코드를 한 줄도 안 쓰고, **말로 시켜서** 브라우저를 움직여봅니다.

먼저 용어 하나. **MCP**(Model Context Protocol)는 AI에게 도구를 쥐여주는 **연결 규격**입니다. AI는 원래 글자만 주고받을 뿐이라 혼자서는 브라우저를 열 수 없어요. 그래서 "브라우저를 열어라", "이걸 눌러라" 같은 명령을 주고받는 약속을 정해둔 건데, 이 약속을 지키는 프로그램을 **MCP 서버**라고 부릅니다. USB 규격 같은 거예요 — 규격이 같으면 어떤 AI 도구든 같은 서버를 꽂아 쓸 수 있습니다.

그중 브라우저를 담당하는 게 **Playwright MCP**입니다. 이걸 연결해두면 AI가 페이지를 열고, 요소를 찾고, 눌러보고, 결과를 확인할 수 있게 돼요.

연결은 한 줄입니다. 에이전트 도구(Claude Code 등)에 서버를 등록하고 —

```bash
# 예: Claude Code에 Playwright MCP 연결
claude mcp add playwright -- npx @playwright/mcp@latest
```

"대시보드 열어서 carol 검색하고, 결과에 Carol Park만 남는지 확인해줘"라고 지시하면, 에이전트가 브라우저를 조작하며 검증하고 그 과정을 로케이터 코드로 정리해줍니다. 나온 코드를 사람이 다듬어 스펙 파일로 굳히는 거죠 — **탐색은 AI, 확정은 사람**.

### AI는 화면이 아니라 접근성 트리를 읽습니다

여기서 많은 분이 오해하는 지점이 있어요. AI가 브라우저를 조작한다고 하면 대개 **스크린샷을 보고 좌표를 클릭하는 그림**을 떠올립니다. Playwright MCP는 그렇게 동작하지 않아요.

공식 README가 첫 문단에서 못 박습니다 — 이 서버는 LLM이 웹 페이지와 상호작용하게 하되 "구조화된 **접근성 스냅샷**을 통해서, 스크린샷이나 비전 모델 없이" 한다고요. 특징 목록에도 **"픽셀 기반 입력이 아니라 Playwright의 접근성 트리를 사용한다"**\ 고 적혀 있습니다. 좌표 기반 도구가 아예 없는 건 아닌데, `--caps=vision`으로 **일부러 켜야** 나옵니다. 기본값은 꺼져 있어요.

그럼 AI에게 실제로 넘어가는 게 뭘까요. 우리 데모 앱을 그대로 찍어봤습니다. 검색창에 `bob`을 입력한 상태예요.

```yaml
- region "사용자 검색":
  - heading "사용자 검색" [level=2]
  - text: 검색
  - searchbox "검색": bob
  - button "검색어 지우기": ✕
  - list:
    - listitem: Bob Lee bob@example.com
```

익숙하지 않으세요? **role과 접근 가능한 이름**입니다. [접근성 쿼리 편](/posts/frontend-testing-a11y-queries/)에서 `getByRole('button', { name: '검색어 지우기' })`로 요소를 찾았던 그 정보 그대로예요. 스크린 리더가 읽는 것도, 우리 테스트가 찾는 것도, 이제 AI 에이전트가 보는 것도 **같은 트리**입니다.

### 그래서, 접근성이 나쁘면 AI도 못 씁니다

이건 말로만 하면 잘 안 와닿으니 실제로 부숴봤습니다. 지우기 버튼을, AI에게 UI를 맡기면 흔히 나오는 그 div 버튼으로 바꾸고 같은 스냅샷을 찍었어요.

```tsx
// 화면상으로는 똑같이 보이는 코드
<div className="clear-btn" onClick={clearQuery}>✕</div>
```

```yaml
- region "사용자 검색":
  - heading "사용자 검색" [level=2]
  - text: 검색
  - searchbox "검색": bob
  - text: ✕          # ← 버튼이 사라졌습니다
  - list:
    - listitem: Bob Lee bob@example.com
```

`button "검색어 지우기"`였던 자리가 그냥 **`text: ✕`**\ 가 됐습니다. 누를 수 있는 것인지, 눌렀을 때 무슨 일이 일어나는지, 이름이 뭔지 — AI가 알 방법이 없어요. "검색어를 지워줘"라는 지시는 여기서 막힙니다.

정리하면 이렇습니다. **스크린 리더 사용자가 못 쓰는 화면은, AI 에이전트도 못 씁니다.** 둘 다 같은 트리를 읽으니까요. 접근성을 "여력 되면 하는 것"으로 미뤄둔 팀이라면, AI 자동화를 도입하는 순간 그 청구서를 받게 됩니다. 반대로 이 시리즈를 따라 role과 이름을 챙겨온 분들은 **이미 준비가 끝나 있어요.**

접근성이 좋은 코드를 짜야 하는 이유가 하나 더 늘었다는 게 조금 씁쓸하긴 합니다. 사람을 위해서라는 이유만으로 충분했어야 하는데 말이죠. 그래도 뭐, 명분은 많을수록 좋으니까요.

### 그럼 AI는 "이 버튼"을 어떻게 지목하나

트리를 읽는 것까지는 알겠는데, 그다음이 궁금해집니다. 좌표를 안 쓴다면 "이 버튼을 눌러라"를 대체 무슨 말로 전달할까요.

답은 **번호표**예요. MCP가 에이전트에게 넘기는 스냅샷에는 각 줄에 `[ref=e6]` 같은 짧은 표가 하나씩 붙습니다(위 두 출력에는 없는데, 저 둘은 MCP가 아니라 Playwright의 `ariaSnapshot()` API로 직접 뽑은 것이라 그래요). AI는 조작할 때 이 번호를 도구의 `target` 인자로 넘깁니다.

```text
# 스냅샷이 내주는 줄 (참조 포함)
- button "검색어 지우기" [ref=e6]

# 에이전트가 부르는 도구
browser_click(element: "검색어 지우기 버튼", target: "e6")
```

`element`는 "무엇을 왜 누르는지" 사람이 읽을 설명이고, 실제 지목은 `target`이 합니다(예전 버전에서는 이 인자 이름이 `ref`였어요). 좌표가 어디에도 없다는 점을 눈여겨보세요 — 버튼이 화면 어디로 옮겨가든, 색이 바뀌든 상관없이 같은 지시가 통합니다.

### 그럼 테스트를 통째로 맡겨도 될까 — 아직은 아닙니다

여기까지 보면 "그럼 테스트를 전부 에이전트한테 시키면 되겠네" 싶은데, 한 가지 성질 때문에 그러기 어렵습니다.

**에이전트는 같은 지시에도 매번 조금씩 다른 길로 걷습니다.** 오늘은 검색창을 바로 클릭했다가, 내일은 탭으로 이동한 뒤 입력할 수도 있어요. 사람이 시킨 일은 똑같이 해냈으니 잘못한 건 아닌데, **테스트로서는 곤란합니다.** 어제 통과하고 오늘 실패했을 때 "코드가 망가진 건가, 그냥 다른 길로 간 건가"를 알 수 없거든요. 이런 성질을 어려운 말로 **결정성이 약하다**고 합니다. 같은 입력에 같은 결과가 나오는 게 결정적인 거고요.

그래서 역할을 나눠 씁니다.

| 구분 | 잘 맞는 일 | 안 맞는 일 |
|---|---|---|
| **AI 에이전트** | 처음 보는 화면 둘러보기, 시나리오 초안 뽑기, "이거 되나?" 빠르게 확인 | PR을 막는 최종 관문 |
| **코드로 고정된 테스트** | 매번 똑같이 도는 검사, CI 게이트 | 아직 뭘 검사할지 모르는 탐색 단계 |

머지 게이트는 "어제와 오늘 같은 검사"라야 신뢰가 쌓입니다. 그래서 **탐색은 에이전트에게 맡기고, 게이트는 코드로 굳혀두는 것**이 지금으로선 안전한 배분이에요. 앞 편의 "초안은 AI, 확정은 사람"이 브라우저에서도 그대로 반복됩니다.

{{< img src="images/contents/explore-vs-gate.png" alt="탐색과 게이트의 역할 분담 다이어그램 - 왼쪽 탐색 영역에서는 AI 에이전트가 자연어 지시로 브라우저를 몰며 초안을 만들고 경로가 매번 달라도 괜찮지만, 그 산출물은 가운데의 사람 검토를 거쳐 오른쪽 게이트 영역의 코드로 고정된 테스트가 되어 CI에서 매번 같은 검사를 반복합니다. 탐색은 AI, 확정은 사람이라는 원칙이 흐름으로 표현되어 있습니다" >}}

---

## 자가치유 셀렉터 — Playwright에는 없습니다

**자가치유**(self-healing)는 셀렉터가 깨졌을 때 AI가 비슷한 요소를 찾아 대신 이어가는 기능을 말합니다. 버튼 클래스 이름 하나 바뀌었다고 테스트가 우수수 빨간불이 되는 경험을 해보셨다면 꽤 솔깃한 이야기죠.

그런데 결론부터 말씀드리면 — **Playwright에는 이 기능이 없습니다.** 검색하다 보면 자가치유와 Playwright가 한 문장에 나오는 글이 많아서 기본 기능이라고 오해하기 쉬운데, 아니에요. 이걸 넣어달라는 기능 요청이 실제로 올라온 적이 있는데(이슈 #33586), 메인테이너가 "Playwright의 범위를 벗어난다"고 답하며 그날 바로 닫았어요. 문서에도 self-healing이라는 개념 자체가 없습니다.

자가치유를 내세우는 건 Healenium·Katalon·mabl 같은 **별도 제품들**이고, 대부분 상용이에요(Healenium만 오픈소스 코어가 무료이고 Pro가 유료 티어입니다).

헷갈리기 쉬운 게 두 가지 있습니다.

하나는 로케이터가 요소를 **찾는 시점**이에요. Playwright 로케이터는 요소를 미리 붙잡아두는 게 아니라, 클릭·입력 같은 액션을 실행하는 바로 그 순간에 다시 찾습니다. 이걸 **지연 평가**라고 불러요. 덕분에 화면이 리렌더돼도 낡은 참조를 붙들고 실패하는 일이 없는데, 편리하긴 해도 "셀렉터가 깨졌을 때 대체품을 찾아준다"와는 전혀 다른 이야기입니다.

다른 하나는 Playwright MCP README에 "self-healing tests"라는 표현이 한 번 등장한다는 점이에요. 문맥은 **에이전트 루프의 활용 사례**를 나열하는 대목이지, Playwright의 기능 설명이 아닙니다.

도구를 도입한다면 위험도 함께 봐야 합니다. 자가치유의 본질은 **"원래 찾던 것과 비슷한 걸 골라 계속 진행한다"**\ 예요. 잘 맞으면 유지보수가 줄지만, 엉뚱한 요소를 골라도 테스트는 **초록불로 끝납니다.** 게이트가 통과를 알려주는데 정작 검증한 대상이 다른 물건인 거죠. 그래서 치유가 일어났다는 기록이 남으면 사람이 diff를 확인해야 하고, 이 확인을 건너뛰기 시작하면 스위트는 조용히 의미를 잃습니다.

결국 [로케이터 편](/posts/frontend-testing-playwright-locators/)의 결론으로 돌아옵니다. Playwright가 이 문제에 내놓는 답은 자가치유가 아니라 **"처음부터 잘 안 깨지는 로케이터를 쓰라"**\ 예요. 공식 문서가 권하는 순서도 그대로입니다 — `getByRole()`을 최우선으로, 그다음 텍스트·라벨·placeholder, `getByTestId()`는 마지막.

그리고 앞 절에서 본 것처럼, role과 이름으로 짠 로케이터는 AI 에이전트가 페이지를 읽는 방식과 정확히 같은 층위에 있습니다. **최고의 자가치유는 애초에 치유가 필요 없는 셀렉터**예요.

---

## 한 장 요약

- **codegen**(`npx playwright codegen URL`): 클릭·입력을 실시간으로 로케이터 코드로 기록 — 초안용으론 훌륭, 다듬어서 쓸 것
- codegen은 **어서션도 기록**합니다(visibility·text·value) — 어서션 없는 녹화 결과는 테스트가 아니라 실행 기록일 뿐
- 로그인 뒤 화면은 `--save-storage`/`--load-storage`로 재사용 — 단 그 파일은 **쿠키·토큰 덩어리**이니 반드시 `.gitignore`
- **MCP 에이전트**: 스크린샷이 아니라 **접근성 트리**를 읽습니다(좌표 도구는 `--caps=vision` 옵트인) — 스크린 리더가 읽는 것, 우리 테스트가 찾는 것, AI가 보는 것이 전부 같은 트리
- 그래서 **접근성이 나쁘면 AI 자동화도 막힙니다** — div 버튼은 스냅샷에서 `text: ✕`로만 보여 누를 수 있는지조차 알 수 없음(실측)
- 탐색과 게이트는 역할이 다릅니다 — 에이전트는 **결정성이 약해** 매번 다른 길로 걸을 수 있으니, CI 게이트는 코드로 고정된 테스트가 안전
- **자가치유는 Playwright 기능이 아닙니다**(메인테이너가 "범위 밖"이라며 요청 종료) — 별도 상용 도구의 영역이고, '조용히 엉뚱한 요소'를 골라도 초록불이 되는 위험이 따름
- 최고의 자가치유는 처음부터 견고한 **role 기반 셀렉터**

---

## 감독의 자리에서

화려한 도구일수록 검증이 중요해요. 다음 편은 그 위험을 정면으로 — AI가 만든 코드의 함정입니다.

AI가 대신 클릭해주는 걸 보면 신기하지만, 가끔 엉뚱한 버튼을 누르고 "성공했어요!" 하는 자신감까지 사람을 닮았어요. 그래서 우리가 감독으로 남습니다.

> **레벨업**: codegen과 에이전트를 초안 도구로 부리되, 확정은 직접 할 수 있습니다.

> **다음 편**: AI가 만든 코드의 함정 — 접근성·엣지케이스 누락

> 낯선 용어가 있었다면 — [용어집](/posts/frontend-testing-glossary/)에 전부 한 줄씩 정리돼 있어요.

