AI가 테스트 코드를 ‘짜주는’ 것까지는 이제 익숙하죠. 그런데 요즘은 한 걸음 더 나갑니다 — AI가 브라우저를 직접 열고, 클릭하고, 잘 되는지 확인하기 시작했어요.
신기하긴 한데, 막상 “그래서 실무에 쓸 수 있나?“를 물으면 답이 잘 안 나옵니다. 도구 이름은 많이 들리는데 각각 뭘 하는 건지, 어디까지 믿어도 되는지는 정리된 글이 드물거든요. 이 글이 그 지형도입니다.
시리즈로 따라오셨다면 지난 편의 “초안은 AI, 확정은 사람” 원칙이 브라우저로 확장되는 편이고, 검색으로 처음 오셨어도 각 도구를 따로따로 이해할 수 있게 썼습니다. Playwright를 한 번도 안 써보셨어도 괜찮아요. 필요한 개념은 나올 때마다 한 줄씩 풀어드립니다.
실습 코드: frontend-testing-lab — codegen은 Playwright에 내장된 명령이고 MCP 서버는 저장소 밖에서 붙이는 것이라,
step-20은 앞 편과 같은 상태입니다. 본문의 접근성 스냅샷은 이 저장소의 데모 앱을 그대로 찍은 것이고요.
이 글이 답하는 질문 세 가지입니다.
- 내가 클릭한 걸 그대로 테스트 코드로 받아 적을 수 있을까? (codegen)
- AI에게 “이 화면 좀 확인해줘"라고 말로 시킬 수 있을까? (MCP 에이전트)
- 셀렉터가 깨지면 AI가 알아서 고쳐준다던데, 진짜일까? (자가치유)

codegen — 내 클릭을 코드로 받아 적기#
codegen은 Playwright에 처음부터 들어 있는 기능입니다. 브라우저를 하나 띄워주고, 우리가 거기서 클릭하고 입력하는 걸 옆에서 코드로 받아 적어줘요. 테스트를 처음 쓸 때 가장 막막한 게 “이 버튼을 코드로 어떻게 가리키지?“인데, 그 부분을 대신해주는 셈이죠.
AI는 아직 등장하지 않습니다. 그냥 녹화기예요. 그런데도 이 편의 첫 자리에 둔 이유가 있는데, 뒤에서 이야기할 AI 도구들이 여기서 나온 코드를 다듬는 방식으로 일하거든요. 출발점부터 보는 게 순서에 맞습니다.
써보는 건 한 줄이면 됩니다.
npx playwright codegen localhost:5173브라우저가 열리고, 클릭·입력이 실시간으로 코드로 기록됩니다. 데모 앱에서 검색해보면 이런 코드가 생겨요.
// codegen이 기록한 코드
await page.getByRole('searchbox', { name: '검색' }).click()
await page.getByRole('searchbox', { name: '검색' }).fill('carol')codegen도 role 기반 로케이터를 우선 제안합니다(로케이터 편의 원칙과 동일). 초안 만들기엔 훌륭하지만, 불필요한 클릭이나 취약한 셀렉터가 섞일 수 있으니 다듬어서 쓰세요.
어서션 기록 — 클릭만 담기는 게 아닙니다#
codegen을 “동작 녹화기"로만 아는 분이 많은데, 레코더 창 위쪽 툴바에는 어서션(검증) 기록 버튼이 있습니다. 아이콘을 하나 고른 뒤 페이지에서 확인하고 싶은 요소를 클릭하면, 검증 코드가 그 자리에 끼워져요.
기본은 세 가지입니다.
| 버튼 | 확인하는 것 |
|---|---|
assert visibility | 이 요소가 화면에 보이는가 |
assert text | 이 요소가 그 텍스트를 담고 있는가 |
assert value | 입력칸에 그 값이 들어 있는가 |
이게 왜 중요하냐면, 어서션 없는 녹화 결과는 테스트가 아니기 때문입니다. 클릭과 입력만 줄줄이 나열된 파일은 “끝까지 에러 없이 실행됐다"밖에 말해주지 않아요. 화면에 엉뚱한 게 떠 있어도 초록불입니다. 1편부터 이야기해온 “테스트가 있다 vs 테스트가 지킨다"의 차이가 여기서도 그대로예요.
툴바에는 네 번째 버튼
Assert snapshot도 있습니다(1.49부터). 페이지의 접근성 트리를 통째로 떠서toMatchAriaSnapshot()에 박아두는 방식인데, 설명이 codegen 문서가 아니라 aria 스냅샷 문서에 있어 놓치기 쉬워요. ‘접근성 트리’가 뭔지는 조금 뒤 MCP 이야기에서 자세히 다룹니다.
두 번째로 알아두면 좋은 건 로그인 상태 재사용이에요. 로그인 뒤에 있는 화면을 기록하려는데 매번 로그인부터 해야 한다면 금세 지칩니다.
# 한 번 로그인한 뒤 세션을 파일로 저장
npx playwright codegen https://example.com --save-storage=auth.json
# 다음부터는 로그인된 상태로 시작
npx playwright codegen --load-storage=auth.json https://example.comauth.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 등)에 서버를 등록하고 —
# 예: 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을 입력한 상태예요.
- region "사용자 검색":
- heading "사용자 검색" [level=2]
- text: 검색
- searchbox "검색": bob
- button "검색어 지우기": ✕
- list:
- listitem: Bob Lee [email protected]익숙하지 않으세요? role과 접근 가능한 이름입니다. 접근성 쿼리 편에서 getByRole('button', { name: '검색어 지우기' })로 요소를 찾았던 그 정보 그대로예요. 스크린 리더가 읽는 것도, 우리 테스트가 찾는 것도, 이제 AI 에이전트가 보는 것도 같은 트리입니다.
그래서, 접근성이 나쁘면 AI도 못 씁니다#
이건 말로만 하면 잘 안 와닿으니 실제로 부숴봤습니다. 지우기 버튼을, AI에게 UI를 맡기면 흔히 나오는 그 div 버튼으로 바꾸고 같은 스냅샷을 찍었어요.
// 화면상으로는 똑같이 보이는 코드
<div className="clear-btn" onClick={clearQuery}>✕</div>- region "사용자 검색":
- heading "사용자 검색" [level=2]
- text: 검색
- searchbox "검색": bob
- text: ✕ # ← 버튼이 사라졌습니다
- list:
- listitem: Bob Lee [email protected]button "검색어 지우기"였던 자리가 그냥 text: ✕가 됐습니다. 누를 수 있는 것인지, 눌렀을 때 무슨 일이 일어나는지, 이름이 뭔지 — AI가 알 방법이 없어요. “검색어를 지워줘"라는 지시는 여기서 막힙니다.
정리하면 이렇습니다. 스크린 리더 사용자가 못 쓰는 화면은, AI 에이전트도 못 씁니다. 둘 다 같은 트리를 읽으니까요. 접근성을 “여력 되면 하는 것"으로 미뤄둔 팀이라면, AI 자동화를 도입하는 순간 그 청구서를 받게 됩니다. 반대로 이 시리즈를 따라 role과 이름을 챙겨온 분들은 이미 준비가 끝나 있어요.
접근성이 좋은 코드를 짜야 하는 이유가 하나 더 늘었다는 게 조금 씁쓸하긴 합니다. 사람을 위해서라는 이유만으로 충분했어야 하는데 말이죠. 그래도 뭐, 명분은 많을수록 좋으니까요.
그럼 AI는 “이 버튼"을 어떻게 지목하나#
트리를 읽는 것까지는 알겠는데, 그다음이 궁금해집니다. 좌표를 안 쓴다면 “이 버튼을 눌러라"를 대체 무슨 말로 전달할까요.
답은 번호표예요. MCP가 에이전트에게 넘기는 스냅샷에는 각 줄에 [ref=e6] 같은 짧은 표가 하나씩 붙습니다(위 두 출력에는 없는데, 저 둘은 MCP가 아니라 Playwright의 ariaSnapshot() API로 직접 뽑은 것이라 그래요). AI는 조작할 때 이 번호를 도구의 target 인자로 넘깁니다.
# 스냅샷이 내주는 줄 (참조 포함)
- button "검색어 지우기" [ref=e6]
# 에이전트가 부르는 도구
browser_click(element: "검색어 지우기 버튼", target: "e6")element는 “무엇을 왜 누르는지” 사람이 읽을 설명이고, 실제 지목은 target이 합니다(예전 버전에서는 이 인자 이름이 ref였어요). 좌표가 어디에도 없다는 점을 눈여겨보세요 — 버튼이 화면 어디로 옮겨가든, 색이 바뀌든 상관없이 같은 지시가 통합니다.
그럼 테스트를 통째로 맡겨도 될까 — 아직은 아닙니다#
여기까지 보면 “그럼 테스트를 전부 에이전트한테 시키면 되겠네” 싶은데, 한 가지 성질 때문에 그러기 어렵습니다.
에이전트는 같은 지시에도 매번 조금씩 다른 길로 걷습니다. 오늘은 검색창을 바로 클릭했다가, 내일은 탭으로 이동한 뒤 입력할 수도 있어요. 사람이 시킨 일은 똑같이 해냈으니 잘못한 건 아닌데, 테스트로서는 곤란합니다. 어제 통과하고 오늘 실패했을 때 “코드가 망가진 건가, 그냥 다른 길로 간 건가"를 알 수 없거든요. 이런 성질을 어려운 말로 결정성이 약하다고 합니다. 같은 입력에 같은 결과가 나오는 게 결정적인 거고요.
그래서 역할을 나눠 씁니다.
| 구분 | 잘 맞는 일 | 안 맞는 일 |
|---|---|---|
| AI 에이전트 | 처음 보는 화면 둘러보기, 시나리오 초안 뽑기, “이거 되나?” 빠르게 확인 | PR을 막는 최종 관문 |
| 코드로 고정된 테스트 | 매번 똑같이 도는 검사, 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를 확인해야 하고, 이 확인을 건너뛰기 시작하면 스위트는 조용히 의미를 잃습니다.
결국 로케이터 편의 결론으로 돌아옵니다. 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가 만든 코드의 함정 — 접근성·엣지케이스 누락
낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.
