「프론트엔드 테스트 제대로 하기」 시리즈의 20편입니다. 전체 목차 보기 · 용어집

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

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

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

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

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

  • 내가 클릭한 걸 그대로 테스트 코드로 받아 적을 수 있을까? (codegen)
  • AI에게 “이 화면 좀 확인해줘"라고 말로 시킬 수 있을까? (MCP 에이전트)
  • 셀렉터가 깨지면 AI가 알아서 고쳐준다던데, 진짜일까? (자가치유)
codegen·에이전트·자가치유 셀렉터로 이어지는 Playwright와 AI의 결합 지형을 정리한 다이어그램
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 기반 로케이터를 우선 제안합니다(로케이터 편의 원칙과 동일). 초안 만들기엔 훌륭하지만, 불필요한 클릭이나 취약한 셀렉터가 섞일 수 있으니 다듬어서 쓰세요.

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

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

기본은 세 가지입니다.

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

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

툴바에는 네 번째 버튼 Assert snapshot도 있습니다(1.49부터). 페이지의 접근성 트리를 통째로 떠서 toMatchAriaSnapshot()에 박아두는 방식인데, 설명이 codegen 문서가 아니라 aria 스냅샷 문서에 있어 놓치기 쉬워요. ‘접근성 트리’가 뭔지는 조금 뒤 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 [email protected]

익숙하지 않으세요? role과 접근 가능한 이름입니다. 접근성 쿼리 편에서 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 [email protected]

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, 확정은 사람"이 브라우저에서도 그대로 반복됩니다.

탐색과 게이트의 역할 분담 다이어그램 - 왼쪽 탐색 영역에서는 AI 에이전트가 자연어 지시로 브라우저를 몰며 초안을 만들고 경로가 매번 달라도 괜찮지만, 그 산출물은 가운데의 사람 검토를 거쳐 오른쪽 게이트 영역의 코드로 고정된 테스트가 되어 CI에서 매번 같은 검사를 반복합니다. 탐색은 AI, 확정은 사람이라는 원칙이 흐름으로 표현되어 있습니다
탐색과 게이트의 역할 분담 다이어그램 - 왼쪽 탐색 영역에서는 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를 확인해야 하고, 이 확인을 건너뛰기 시작하면 스위트는 조용히 의미를 잃습니다.

결국 로케이터 편의 결론으로 돌아옵니다. 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가 만든 코드의 함정 — 접근성·엣지케이스 누락

낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.