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에게는 실제로 뭐가 넘어갈까요. 먼저 우리 데모 앱을 Playwright의 ariaSnapshot()으로 찍어봤습니다. 테스트에서 toMatchAriaSnapshot()이 비교하는 것도 이 기본 스냅샷이에요. 검색창에 bob을 입력한 상태입니다.
- region "사용자 검색":
- heading "사용자 검색" [level=2]
- text: 검색
- searchbox "검색": bob
- button "검색어 지우기": ✕
- list:
- listitem: Bob Lee [email protected]익숙하지 않으세요? role과 접근 가능한 이름입니다. 접근성 쿼리 편에서 getByRole('button', { name: '검색어 지우기' })로 요소를 찾았던 그 정보 그대로예요. 스크린 리더가 읽는 것도, 우리 테스트가 찾는 것도 이 트리입니다.
Playwright MCP가 모델에게 건네는 건 여기서 조금 달라집니다. 같은 함수를 mode: 'ai'로 불러서 찍거든요. 같은 화면을 ai 모드로 찍으면 이렇게 나옵니다(Playwright 1.63, 페이지 전체에서 이 영역만 옮겼어요).
- region [ref=e6]:
- heading "사용자 검색" [level=2] [ref=e7]
- text: 검색
- searchbox "검색" [active] [ref=e8]: bob
- button "검색어 지우기" [ref=e9]: ✕
- list [ref=e10]:
- listitem [ref=e11]: Bob Lee [email protected]요소마다 [ref=e9] 같은 번호표가 붙었네요. AI가 요소를 가리킬 때 쓰는 건데, 조금 뒤에 따로 설명할게요([active]는 지금 포커스가 있다는 표시이고, region 이름은 바로 아래 제목과 겹쳐서 뺀 거예요). 그 밖의 뼈대는 같습니다. 버튼은 여전히 button "검색어 지우기"예요. 차이가 크게 벌어지는 건 접근성이 망가졌을 때입니다.
접근성이 나쁘면 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도 못 쓴다"고 결론을 냈는데, 에이전트 도구가 실제로 건네는 스냅샷으로 다시 찍어 보니 이야기가 달라졌습니다.
같은 화면을 세 가지 방법으로 찍어 나란히 놓았습니다(2026년 10월 2일, Playwright 1.63·Chrome DevTools MCP 1.9.0·Chrome 154).
| 찍은 방법 | 진짜 <button> | div 버튼 |
|---|---|---|
| Playwright 기본 스냅샷(테스트용) | button "검색어 지우기": ✕ | text: ✕ |
| Playwright MCP(ai 모드) | button "검색어 지우기" [ref=e9]: ✕ | generic [ref=e9]: ✕ |
| Chrome DevTools MCP | uid=3_0 button "검색어 지우기" | uid=3_0 StaticText "✕" |
Playwright MCP는 div를 버리지 않고 이름 없는 덩어리(generic)로 남긴 데다 번호표까지 붙였습니다. Chrome DevTools MCP는 클릭 표시 없는 글자(StaticText)로 넘겼고요(이 도구는 번호표를 uid라고 불러요). 그런데 두 MCP 모두, 그 번호로 ✕를 누르니 실제로 검색어가 지워졌습니다. 적어도 이 두 도구는 길을 막지 않았어요.
다만 번호는 제가 골라 넣었습니다. 모델이 "검색어를 지워줘"라는 말만 듣고 이 이름 없는 ✕를 골라낼지는 따로 재보지 않았어요. 고른다면 이름을 읽어서가 아니라 "검색창 옆 ✕는 대개 지우기 버튼"이라는 짐작 덕분일 겁니다.
ai 모드가 화면 쪽 정보를 더 담는 예가 두 가지 더 있습니다. 하나는 커서예요. CSS에
cursor: pointer(마우스를 올리면 손가락 모양)가 걸린 요소에는generic [ref=e9] [cursor=pointer]: ✕처럼 표시가 하나 더 붙습니다. 이 표시는 CSS 값만 보고 붙기 때문에, 커서가 기본 화살표인 진짜<button>에는 오히려 안 붙어요.다른 하나는 숨긴 요소입니다.
aria-hidden="true"로 숨겨도 화면에 보이면[aria-hidden]표시를 달고 남아요. 결과 목록에 걸어 보니 ai 모드에만list [aria-hidden]으로 남고, 기본 스냅샷과 Chrome DevTools MCP에서는 통째로 사라졌습니다. 화면에 보이니까 일부러 넣는다는 게 Playwright 팀의 설명이고요(PR #42268, 이 표시는 1.63부터).
정리하면, 에이전트가 받는 트리는 도구마다 다릅니다. 같은 div 버튼이 어떤 도구에선 글자로, 어떤 도구에선 번호표 달린 덩어리로 보여요. 그래도 세 방법 모두에서 변하지 않은 게 하나 있습니다. div 버튼은 어디서도 "검색어 지우기"라는 이름을 갖지 못했고, 진짜 <button>은 어디서나 button "검색어 지우기"로 보였어요. 도구가 바뀌어도 흔들리지 않는 건 시맨틱 쪽입니다.
사람 쪽 사정은 도구와 상관없이 분명합니다. 이 div는 Tab으로 닿지 않아요. 검색창에서 Tab을 누르면 ✕를 건너뜁니다. 스크린 리더도 이걸 버튼으로 알리지 않아서, 버튼 목록이나 버튼 이동 단축키로 찾을 수 없고요. NVDA처럼 클릭할 수 있는 요소를 "클릭 가능"이라고 알려 주는 스크린 리더라면 이 ✕도 그렇게 들릴 수 있지만, "검색어 지우기"라는 이름은 들려줄 수 없습니다.
접근성을 미뤄둔 팀이 AI 자동화를 도입하면 청구서를 받을까요? 쓰는 도구에 따라 받기도 하고 안 받기도 합니다. 그런데 청구서가 안 온다고 빚이 사라지는 건 아니죠. 에이전트가 ✕를 눌러 내든 말든, 그 버튼을 버튼으로 찾지 못하는 사람은 그대로 있습니다. 반대로 이 시리즈를 따라 role과 이름을 챙겨온 분들은 도구를 가릴 필요가 없어요. 어느 방법으로 찍어도 버튼은 버튼으로 보이니까요.
접근성이 좋은 코드를 짜야 하는 이유가 하나 더 늘었다는 게 조금 씁쓸하긴 합니다. 사람을 위해서라는 이유만으로 충분했어야 하는데 말이죠. 그래도 뭐, 명분은 많을수록 좋으니까요.
그럼 AI는 "이 버튼"을 어떻게 지목하나#
트리를 읽는 것까지는 알겠는데, 그다음이 궁금해집니다. 좌표를 안 쓴다면 "이 버튼을 눌러라"를 대체 무슨 말로 전달할까요.
답은 앞에서 본 번호표예요. ai 모드 스냅샷은 화면에 보이고 클릭이 닿는 요소마다 [ref=e9] 같은 짧은 표를 붙여 둡니다. AI는 조작할 때 이 번호를 도구의 target 인자로 넘기고요.
# 스냅샷이 내주는 줄 (참조 포함)
- button "검색어 지우기" [ref=e9]: ✕
# 에이전트가 부르는 도구
browser_click(element: "검색어 지우기 버튼", target: "e9")element는 "무엇을 왜 누르는지" 사람이 읽을 설명이고, 실제 지목은 target이 합니다(예전 버전에서는 이 인자 이름이 ref였어요). 좌표가 어디에도 없다는 점을 눈여겨보세요 — 버튼이 화면 어디로 옮겨가든, 색이 바뀌든 상관없이 같은 지시가 통합니다. 앞 실험에서 이름 없는 div까지 누를 수 있었던 것도 이 번호표 덕분이에요. 이름은 없어도 번호는 붙었으니까요.
그럼 테스트를 통째로 맡겨도 될까 — 아직은 아닙니다#
여기까지 보면 "그럼 테스트를 전부 에이전트한테 시키면 되겠네" 싶은데, 한 가지 성질 때문에 그러기 어렵습니다.
에이전트는 같은 지시에도 매번 조금씩 다른 길로 걷습니다. 오늘은 검색창을 바로 클릭했다가, 내일은 탭으로 이동한 뒤 입력할 수도 있어요. 사람이 시킨 일은 똑같이 해냈으니 잘못한 건 아닌데, 테스트로서는 곤란합니다. 어제 통과하고 오늘 실패했을 때 "코드가 망가진 건가, 그냥 다른 길로 간 건가"를 알 수 없거든요. 이런 성질을 어려운 말로 결정성이 약하다고 합니다. 같은 입력에 같은 결과가 나오는 게 결정적인 거고요.
그래서 역할을 나눠 씁니다.
| 구분 | 잘 맞는 일 | 안 맞는 일 |
|---|---|---|
| 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과 이름만큼은 어느 도구에서나 그대로 보였습니다. role과 이름으로 짠 로케이터가 기대는 곳도 바로 그 층위입니다. 최고의 자가치유는 애초에 치유가 필요 없는 셀렉터예요.
한 장 요약#
- codegen(
npx playwright codegen URL): 클릭·입력을 실시간으로 로케이터 코드로 기록 — 초안용으론 훌륭, 다듬어서 쓸 것 - codegen은 어서션도 기록합니다(visibility·text·value) — 어서션 없는 녹화 결과는 테스트가 아니라 실행 기록일 뿐
- 로그인 뒤 화면은
--save-storage/--load-storage로 재사용 — 단 그 파일은 쿠키·토큰 덩어리이니 반드시.gitignore - MCP 에이전트: 스크린샷이 아니라 접근성 트리를 읽습니다(좌표 도구는
--caps=vision옵트인) — 다만 모델에게 건네는 모양은 도구마다 다름. Playwright MCP는mode: 'ai'스냅샷이라 번호표(ref)·클릭 힌트·화면에 보이는 숨김 요소까지 담음 - 에이전트 도구마다 받는 트리가 다릅니다 — div 버튼이 기본 스냅샷에선
text: ✕, Playwright MCP에선 이름 없는generic, Chrome DevTools MCP에선StaticText였고, 두 MCP 모두 번호로 누를 수는 있었음(실측). 어느 쪽이든 "검색어 지우기"라는 이름은 사라지고, 키보드·스크린 리더 사용자는 버튼으로 찾지 못함 - 탐색과 게이트는 역할이 다릅니다 — 에이전트는 결정성이 약해 매번 다른 길로 걸을 수 있으니, CI 게이트는 코드로 고정된 테스트가 안전
- 자가치유는 Playwright 기능이 아닙니다(메인테이너가 "범위 밖"이라며 요청 종료) — 별도 상용 도구의 영역이고, '조용히 엉뚱한 요소'를 골라도 초록불이 되는 위험이 따름
- 최고의 자가치유는 처음부터 견고한 role 기반 셀렉터
감독의 자리에서#
화려한 도구일수록 검증이 중요해요. 다음 편은 그 위험을 정면으로 — AI가 만든 코드의 함정입니다.
AI가 대신 클릭해주는 걸 보면 신기하지만, 가끔 엉뚱한 버튼을 누르고 "성공했어요!" 하는 자신감까지 사람을 닮았어요. 그래서 우리가 감독으로 남습니다.
레벨업: codegen과 에이전트를 초안 도구로 부리되, 확정은 직접 할 수 있습니다.
다음 편: AI가 만든 코드의 함정 — 접근성·엣지케이스 누락
낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.

댓글 남기기