이 글에서 정리하는 내용
화면에는 요소가 보이는데 Playwright locator가 찾지 못하는 상황을 기준으로 원인을 좁히고, 실제 프로젝트에서 어떤 설정과 코드 구조를 확인해야 하는지 정리합니다. 단순 개념 소개가 아니라 오류가 난 순간 바로 확인할 순서와 선택 기준을 중심으로 설명합니다.
- locator가 요소를 찾지 못하는 대표 원인
- getByRole과 접근성 이름 확인하기
- 비동기 렌더링과 wait 기준 잡기
- 모달·목록·아이콘 버튼에서 자주 나는 문제
- 테스트 실패 로그를 볼 때 체크리스트
locator가 요소를 찾지 못하는 대표 원인

화면에는 요소가 보이는데 Playwright locator가 찾지 못한다면 먼저 DOM에 실제로 존재하는지, locator 기준이 맞는지, 클릭 시점에 actionability 조건을 만족하는지 나눠 봐야 합니다. Playwright locator는 동작 직전에 요소를 다시 찾고 자동 대기하지만, 잘못된 role 이름이나 범위 밖의 요소까지 대신 찾아주지는 않습니다.
먼저 trace viewer, screenshot, 오류 로그에 찍힌 locator 문자열을 같이 봅니다. 요소가 아직 렌더링되지 않았는지, iframe이나 modal 안에 있어서 범위를 좁혀야 하는지, 버튼 텍스트가 실제 접근성 이름과 다른지 확인하면 원인이 빠르게 줄어듭니다. 폼 검증 화면에서 실패한다면 React Hook Form 에러 메시지 표시 문제 해결처럼 메시지가 언제 렌더링되는지도 함께 확인합니다.
getByRole과 접근성 이름 확인하기
getByRole은 화면에 보이는 글자만이 아니라 접근성 role과 accessible name을 기준으로 요소를 찾습니다. 버튼 안의 텍스트, aria-label, 연결된 label, 아이콘 버튼의 이름이 실제 locator와 맞는지 먼저 확인해야 합니다.
예를 들어 아이콘만 있는 저장 버튼은 시각적으로는 버튼처럼 보여도 이름이 없으면 getByRole("button", { name: "저장" })에 걸리지 않습니다. 이때는 테스트를 억지로 바꾸기보다 실제 사용자 보조 기술에도 읽히도록 버튼 이름을 명확히 주는 쪽이 좋습니다.
await page.getByRole("button", { name: "저장" }).click();
await expect(page.getByRole("status")).toHaveText("저장 완료");
위 코드에서 getByRole은 버튼을 찾고, expect(...).toHaveText()는 상태 텍스트가 바뀔 때까지 재시도합니다. 단순히 waitForTimeout을 넣는 것보다 테스트가 기다리는 조건이 분명합니다.
텍스트나 role이 자주 바뀌는 영역이라면 data-testid를 계약으로 정할 수도 있습니다. 다만 버튼, 링크, 입력창처럼 사용자에게 의미가 있는 요소는 가능하면 role과 이름으로 찾는 편이 테스트 의도와 접근성 확인을 함께 남기기 좋습니다.
비동기 렌더링과 wait 기준 잡기
Playwright는 클릭, 입력, assertion에서 많은 조건을 자동으로 기다립니다. 그래도 API 응답 뒤에 목록이 생기는 구조라면 expect(locator).toBeVisible(), toHaveText(), toHaveCount()처럼 화면이 갖춰져야 하는 조건을 명시하는 편이 안전합니다.
page.waitForTimeout()은 마지막 확인용으로만 남기고, 평소에는 사용자가 볼 수 있는 결과를 기다립니다. navigation이 문제라면 URL이나 heading을 기다리고, 로딩 UI가 문제라면 loading이 사라지는 조건과 최종 콘텐츠가 보이는 조건을 따로 확인합니다.
모달·목록·아이콘 버튼에서 자주 나는 문제

모달은 열리기 전에는 DOM에 없거나 숨겨져 있을 수 있습니다. 먼저 모달을 여는 동작을 실행하고, 모달 컨테이너 안에서 getByRole을 다시 좁히면 같은 이름의 버튼이 여러 개 있을 때도 실패를 줄일 수 있습니다.
목록에서는 같은 텍스트가 여러 번 나올 수 있으므로 filter(), nth(), 부모 locator 범위 제한을 사용합니다. 아이콘 버튼은 접근성 이름이 없으면 테스트도 불안정해지므로 aria-label이나 보이는 텍스트 기준을 먼저 정리합니다.
테스트 실패 로그를 볼 때 체크리스트
마지막으로 체크리스트를 남겨두면 다음에 같은 증상을 만났을 때 훨씬 빠르게 움직일 수 있습니다. 첫째, 오류 메시지를 그대로 검색하기 전에 실행 환경을 확인합니다. 둘째, 설정 파일과 코드가 같은 기준을 바라보는지 봅니다. 셋째, 수정 후에는 로컬 실행, production build, 필요한 경우 배포 환경까지 나누어 검증합니다.
체크리스트를 만들 때는 “무엇을 바꿨는가”보다 “무엇을 확인했는가”를 먼저 적는 편이 좋습니다. 예를 들어 파일 위치를 확인했는지, 빌드 명령으로 재현했는지, 배포 환경에서도 같은 값이 들어가는지, 변경 후 다시 실패 조건을 실행했는지처럼 검증 행동을 남깁니다. 이런 기록이 있으면 다음에 비슷한 문제가 생겼을 때 해결 과정이 훨씬 짧아집니다.
가능하면 수정 전후를 한 줄로 기록해 두세요. 예를 들어 “저장 버튼 accessible name을 저장으로 수정”, “모달을 연 뒤 dialog 내부에서 버튼을 찾도록 범위 제한”, “목록 렌더링 후 toHaveCount로 개수 확인”처럼 남기면 됩니다. 이런 기록은 글을 읽는 독자에게도 그대로 적용 가능한 문제 해결 패턴이 됩니다.
반대로 피해야 할 방식은 오류가 사라질 때까지 설정을 무작위로 바꾸는 것입니다. 당장은 통과하더라도 왜 해결됐는지 모르면 다음 배포나 협업 환경에서 같은 문제가 다시 생깁니다. 작은 단위로 가설을 세우고 하나씩 확인하는 방식이 느려 보여도, 결과적으로는 가장 빠른 해결에 가깝습니다.
마지막으로, 해결이 끝났다고 판단하기 전에는 같은 증상을 일부러 한 번 더 재현해 보는 것이 좋습니다. 실패 조건을 다시 만들 수 있고, 수정 후 그 조건이 사라졌다면 해결 근거가 생깁니다. 이 확인 없이 화면이 한 번 정상으로 보였다는 이유만으로 끝내면, 실제 배포나 다른 브라우저에서 문제가 다시 나타날 수 있습니다.
이 글의 핵심은 특정 옵션 하나를 외우는 것이 아닙니다. 오류가 났을 때 어디서부터 확인해야 하는지 순서를 갖는 것입니다. 그 순서가 있으면 비슷한 문제가 다시 나와도 무작정 코드를 바꾸지 않고, 원인을 작게 나누어 안정적으로 해결할 수 있습니다.
함께 확인하면 좋은 글
- Vitest document is not defined 오류 해결: jsdom 설정 기준
- Jest console.error 테스트: try catch 에러 검증 방법
- Storybook Vite 동적 import 오류 해결: fetch 실패 원인