Selenium·Playwright로 브라우저 자동화를 할 때 첫 번째 난관은 “요소를 어떻게 특정하는가”입니다. CSS 셀렉터와 XPath의 작성법을 이해하면 테스트 코드의 안정성이 크게 달라집니다. 이 글에서는 로케이터의 기초부터 깨지지 않는 작성법의 노하우까지 해설합니다.
📌 이 글의 대상 독자
| ✅ Selenium·Playwright를 막 시작해서 요소 지정 방법을 잘 모르는 분 |
| ✅ CSS 셀렉터와 XPath의 차이를 정리하고 싶은 분 |
| ✅ UI 변경 때마다 테스트 코드가 깨져서 곤란한 분 |
✅ Playwright의 새로운 로케이터(get_by_role 등)를 배우고 싶은 분 |
📖 이 글을 읽으면 할 수 있는 것
| ✔ HTML을 보고 CSS 셀렉터·XPath를 직접 작성할 수 있게 된다 |
| ✔ Selenium·Playwright 양쪽에서 로케이터를 올바르게 사용할 수 있게 된다 |
| ✔ 깨지기 어려운 로케이터 작성법을 선택할 수 있게 된다 |
✔ data-testid를 활용해 유지보수 비용을 줄일 수 있게 된다 |
필자는 여러 프로젝트에서 Selenium·Playwright를 사용한 브라우저 자동화 테스트를 실천해 왔습니다.
로케이터가 원인이 되어 E2E 테스트가 불안정해지는 경우는 매우 많으며, 이 글은 그 경험을 바탕으로 실무에서 사용할 수 있는 작성법에 집중해서 해설합니다.
✅ 이 글의 결론
- 로케이터 우선순위는
data-testid→ ID → CSS → XPath 순이 일반적 - Playwright에서는
get_by_role·get_by_text등 의미 있는 로케이터를 사용하면 깨지기 어렵다 - 절대 XPath·위치 의존 CSS를 피하면 장기 유지보수가 편해진다
로케이터란 무엇인가
로케이터(Locator)란 웹 페이지 상의 특정 요소(버튼·텍스트박스·링크 등)를 찾기 위한 지정 방법입니다.
Selenium이나 Playwright가 브라우저를 조작할 때, “어떤 요소를 클릭할 것인가”, “어떤 텍스트박스에 문자를 입력할 것인가”를 로케이터로 지정합니다.
# 로케이터 예시 (Selenium)
driver.find_element(By.ID, "login-btn") # ID로 탐색
driver.find_element(By.CSS_SELECTOR, "#form input[type='email']") # CSS로 탐색
driver.find_element(By.XPATH, "//button[@type='submit']") # XPath로 탐색
로케이터가 깨진다 = 테스트가 실패한다는 직접적인 관계가 있으므로, 깨지기 어려운 로케이터를 선택하는 것이 테스트 안정성을 크게 좌우합니다.
로케이터의 종류와 사용 구분
| 종류 | 예시 | 안정성 | 권장도 |
|---|---|---|---|
data-testid | [data-testid="login-btn"] | ⭐⭐⭐ 최고 | 🔴 최우선 |
| ID | #login-btn | ⭐⭐⭐ 높음 | 🟡 권장 |
| name 속성 | [name="username"] | ⭐⭐⭐ 높음 | 🟡 권장 |
| CSS 셀렉터 | .login-form input | ⭐⭐ 중간 | 🟡 허용 |
| XPath (상대) | //button[@type='submit'] | ⭐⭐ 중간 | 🟡 허용 |
| 텍스트 내용 | //button[text()='로그인'] | ⭐ 낮음 | ⚠️ 상황에 따라 |
| XPath (절대) | /html/body/div[2]/form/button | ⭐ 최저 | ❌ 비권장 |
CSS 셀렉터 작성법
CSS 셀렉터는 HTML 구조를 사용해 요소를 특정하는 방식입니다. 프런트엔드 개발자에게는 익숙하며, Selenium·Playwright 양쪽에서 사용할 수 있습니다.
기본 작성법
# HTML 예시:
# <input id="email" class="form-input" type="email" name="email" />
# ID로 지정 (# 붙이기)
"#email"
# 클래스로 지정 (. 붙이기)
".form-input"
# 속성으로 지정 ([ ]로 감싸기)
"[type='email']"
"[name='email']"
# 태그 + 클래스 조합
"input.form-input"
# 부모자식 관계 (스페이스 구분: 간접 자손)
".login-form input"
# 직접 자식 요소 (> 구분)
".login-form > input"
로케이터 선택 흐름
| data-testid 속성이 있는가? | |
| ✅ YES → data-testid 사용 (최우선) | ❌ NO → 다음 조건으로 ↓ |
| 고유한 ID가 있는가? | |
| ✅ YES → #id 사용 | ❌ NO → 다음 조건으로 ↓ |
| Playwright에서 role / label로 특정할 수 있는가? | |
| ✅ YES → get_by_role / get_by_label 사용 | ❌ NO → 다음 조건으로 ↓ |
| name·type 등의 속성으로 특정할 수 있는가? | |
| ✅ YES → CSS 셀렉터 사용 | ❌ NO → 다음 조건으로 ↓ |
| 🔴 최후의 수단: XPath (상대) 사용 | |
헷갈릴 때는 “사용자가 인식할 수 있는 요소”를 우선하는 생각이 Playwright에서는 권장됩니다. 화면을 보고 사용자가 “로그인 버튼”으로 인식한다면 get_by_role("button", name="로그인"), “이메일 주소 칸”으로 인식한다면 get_by_label("이메일 주소")가 자연스러운 선택입니다.
실무에서 자주 쓰는 패턴
# data-testid (가장 안정적)
"[data-testid='login-button']"
# 폼 입력 필드
"input[type='email']"
"input[type='password']"
"input[placeholder='이메일 주소']"
# 버튼
"button[type='submit']"
"button.btn-primary"
# 링크
"a[href='/dashboard']"
# 부분 일치 (^=시작, $=끝, *=포함)
"[data-testid^='item-']" # data-testid가 "item-"으로 시작하는 요소
"[class*='active']" # class에 "active"를 포함하는 요소
XPath 작성법
XPath는 XML·HTML 문서를 탐색하기 위한 쿼리 언어입니다. CSS 셀렉터보다 표현력이 높고, 텍스트 내용이나 요소의 형제 관계 등 CSS에서는 어려운 조건도 작성할 수 있습니다.
기본 작성법
# //태그[@속성='값'] ← 가장 기본적인 상대 XPath
"//button[@type='submit']"
"//input[@id='email']"
# 텍스트 내용으로 지정 (다국어 서비스에서는 주의)
"//button[text()='로그인']"
"//button[contains(text(),'로그')]" # 부분 일치
# 복수 조건 (and로 연결)
"//input[@type='text' and @name='username']"
# 부모 요소에서 지정 (./)
"//form[@id='login-form']//button[@type='submit']"
# 절대 XPath (비권장: HTML 구조가 바뀌면 바로 깨진다)
# /html/body/div[1]/main/form/button[2] ← 이것은 사용 금지
CSS vs XPath 사용 구분
| 상황 | CSS | XPath |
|---|---|---|
| ID·클래스·속성으로 특정 가능 | ◎ 짧게 쓸 수 있다 | ○ 약간 장황 |
| 텍스트 내용으로 특정하고 싶다 | ✕ 어렵다 | ◎ text()로 쓸 수 있다 |
| 부모·형제·조상 요소를 탐색하고 싶다 | ✕ 위 방향은 불가 | ◎ parent:: / following-sibling:: / ancestor::로 가능 |
| 가독성·유지보수성 | ◎ 보기 좋다 | △ 길어지기 쉽다 |
| 실행 속도 | ◎ 약간 빠르다 | △ 약간 느리다 |
ID·속성·클래스로 쓸 수 있다면 CSS 셀렉터를 우선합니다. 텍스트 내용으로 특정해야 하거나 CSS로는 구조상 쓸 수 없을 때 XPath를 사용합니다. 둘 다 쓸 수 있다면 가독성이 높은 것을 선택합시다.
Selenium에서의 로케이터 사용법
Selenium 4.x에서는 By 클래스를 사용해 요소를 지정합니다.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com/login") # 샘플 URL
# 요소를 취득해서 조작
email_input = driver.find_element(By.ID, "email")
email_input.send_keys("user@example.com") # 텍스트 입력
password_input = driver.find_element(By.CSS_SELECTOR, "input[type='password']")
password_input.send_keys("password123") # 텍스트 입력
submit_btn = driver.find_element(By.CSS_SELECTOR, "[data-testid='login-button']")
submit_btn.click() # 클릭
# 요소가 표시될 때까지 대기한 후 취득·조작 (권장)
wait = WebDriverWait(driver, 10)
error_msg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid='error-message']"))
)
assert "비밀번호가 올바르지 않습니다" in error_msg.text
Selenium 4 이후에는
driver.find_element("id", "email")과 같은 문자열 지정은 비권장입니다. 반드시 By.ID·By.CSS_SELECTOR 등의 상수를 사용하세요.Selenium vs Playwright: 대기 전략의 차이
| 비교 항목 | Selenium | Playwright |
|---|---|---|
| 기본 대기 | 없음 (즉시 탐색) | ◎ 자동 대기 (표준) |
| 명시적 대기 | WebDriverWait을 직접 작성 | 기본 불필요 (조작 전 자동 대기) |
| time.sleep 의존 리스크 | △ 쓰기 쉬움 | ○ 적음 |
Playwright에서의 로케이터 사용법
Playwright는 Selenium보다 새로운 로케이터 API를 가지고 있어, 의미 있는(시맨틱한) 로케이터를 사용할 수 있습니다. HTML 구조가 아닌 “역할”이나 “텍스트”로 지정하므로 디자인 변경에 강한 것이 특징입니다.
Selenium vs Playwright: 로케이터 기능 비교
| 기능 | Selenium | Playwright |
|---|---|---|
| CSS 셀렉터 | ✅ | ✅ |
| XPath | ✅ | ✅ |
| get_by_role / get_by_text | ❌ | ✅ |
| data-testid 전용 API | △ CSS로 대용 | ✅ get_by_test_id |
| 자동 대기 | △ 명시적으로 작성 필요 | ✅ 표준 내장 |
| Open Shadow DOM | △ 추가 절차 필요 | ✅ 투명하게 접근 가능 |
권장되는 로케이터
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com/login") # 샘플 URL
# data-testid로 지정 (가장 안정적)
page.get_by_test_id("login-button").click()
# role + name으로 지정 (ARIA 역할 활용)
page.get_by_role("button", name="로그인").click()
page.get_by_role("textbox", name="이메일 주소").fill("user@example.com")
# 텍스트 내용으로 지정
page.get_by_text("로그인").click() # 완전 일치
page.get_by_text("로그", exact=False).click() # 부분 일치
# label 텍스트로 폼 요소 지정
page.get_by_label("이메일 주소").fill("user@example.com")
# 플레이스홀더로 지정
page.get_by_placeholder("example@email.com").fill("user@example.com")
# CSS 셀렉터로 지정 (기존 방식도 사용 가능)
page.locator("[data-testid='login-button']").click()
page.locator("input[type='password']").fill("password123")
browser.close()
Playwright 로케이터 우선순위
| 우선도 | 로케이터 | 사용 장면 |
|---|---|---|
| ① 최우선 | get_by_test_id() | data-testid 속성이 정비된 경우. 개발팀과 연계해서 부여하는 것이 전제 |
| ② 권장 | get_by_role() | 버튼·텍스트박스·체크박스 등 |
| ③ 권장 | get_by_label() | 폼 입력 필드 (label 태그에 연결된 것) |
| ④ 허용 | get_by_text() | 텍스트 내용으로 특정 가능한 요소 (다국어 서비스는 주의) |
| ⑤ 허용 | locator()(CSS/XPath) | 위 방법으로 쓸 수 없을 때, 또는 Selenium과 공통화가 필요할 때 |
data-testid로 깨지지 않는 테스트 만들기
data-testid 속성은 테스트 전용 식별자로, 디자인 변경이나 리팩터링의 영향을 받지 않습니다. 개발팀과 협력해서 테스트 대상 요소에 부여함으로써 유지보수 비용을 크게 줄일 수 있습니다.
<!-- HTML 쪽에 data-testid 부여 -->
<button type="submit" data-testid="login-submit-btn">
로그인
</button>
<input type="email"
data-testid="login-email-input"
placeholder="이메일 주소" />
# Selenium에서 사용
driver.find_element(By.CSS_SELECTOR, "[data-testid='login-submit-btn']").click()
# Playwright에서 사용
page.get_by_test_id("login-submit-btn").click()
page.get_by_test_id("login-email-input").fill("user@example.com")
페이지명-컴포넌트명-액션 형식(예: login-email-input·checkout-submit-btn)으로 통일하면, 테스트 코드를 읽는 것만으로 무엇을 조작하는지 알 수 있습니다.실무에서는 버튼·입력 필드·링크 등 사용자가 조작하는 주요 인터랙티브 요소를 중심으로 부여하는 것이 일반적입니다. 장식용 div나 span 등 모두에 붙이면 관리 비용만 늘어날 뿐입니다.
로케이터에서 막히기 쉬운 포인트 7가지
⚠️ 로케이터에서 자주 있는 함정
- 절대 XPath를 사용해 버린다
브라우저의 DevTools에서 “XPath 복사”를 하면 절대 XPath가 생성되는 경우가 있습니다./html/body/div[2]/main/form/button[1]과 같은 형식은 HTML 구조가 조금만 바뀌어도 깨집니다. 반드시 속성을 사용한 상대 XPath로 다시 작성합시다. - 동적으로 바뀌는 class명·ID를 사용해 버린다
React·Vue 등의 프레임워크에서는 빌드마다class="btn-abc123"과 같은 해시가 붙은 클래스명이 생성되는 경우가 있습니다. 이런 값을 로케이터에 사용하면 매번 깨집니다.data-testid나 고정 속성을 사용합시다. - 복수의 요소가 매치되어도 find_element()는 첫 번째 1건을 반환한다
find_element(단수)는 복수 매치되어도 예외를 발생시키지 않고 첫 번째 1건을 반환합니다. 그 때문에 의도하지 않은 요소를 조작하고 있는 경우가 있습니다.find_elements(복수)로 매치 건수를 확인하고 로케이터를 더 구체적으로 좁히는 습관을 들입시다. - iframe 안의 요소를 취득할 수 없다
iframe 내부에 있는 요소는 그대로find_element해도 취득할 수 없습니다. Selenium에서는driver.switch_to.frame(), Playwright에서는page.frame_locator()로 전환이 필요합니다. - 요소가 표시되지 않았는데 조작하려 하고 있다
페이지 로딩 중이나 비동기 통신 중에 요소를 취득하면ElementNotInteractableException이 발생합니다. Selenium에서는WebDriverWait, Playwright에서는 자동 대기가 효과적이지만, 숨겨진 요소에는 추가 대응이 필요합니다. - 텍스트로 지정한 로케이터가 다국어 대응에서 깨진다
//button[text()='로그인']은 영어 표시일 때'Login'이 되어 실패합니다. 다국어 서비스에서는 텍스트가 아닌data-testid나 role을 사용합시다. - Shadow DOM 안의 요소를 취득할 수 없다
Web Components나 일부 라이브러리에서는 Shadow DOM이 사용됩니다. Playwright는 Open Shadow DOM을 표준으로 투명하게 다룰 수 있어, 많은 경우 일반적인page.locator()를 그대로 사용할 수 있습니다. 다만 Closed Shadow DOM은 취득할 수 없습니다. Selenium에서는 Shadow DOM 조작에 추가 절차가 필요합니다.
자주 묻는 질문 (FAQ)
Playwright Codegen(playwright codegen https://example.com)을 사용하면 브라우저를 조작하면서 로케이터를 자동 생성할 수 있습니다. 생성된 코드를 출발점으로 삼아 필요에 따라 data-testid 기반으로 수정하는 것을 권장합니다. Chrome DevTools 콘솔에서도 document.querySelector('.btn')이나 $x('//button')으로 그 자리에서 시험해볼 수 있습니다.
일반적으로 CSS 셀렉터가 약간 빠릅니다. 브라우저의 렌더링 엔진이 CSS에 최적화되어 있기 때문입니다. 다만 실제 테스트 실행 시간에 미치는 영향은 체감할 수 없을 정도로 작으므로, 성능보다 가독성·안정성을 우선해서 로케이터를 선택하는 것이 중요합니다.
CSS 셀렉터를 먼저 배우는 것을 권장합니다. 가독성이 높고 쓰기 쉬우며 실무에서도 자주 사용합니다. XPath는 “텍스트로 요소를 특정하고 싶다”, “부모 요소를 탐색하고 싶다”는 CSS로는 어려운 경우에 사용합니다. 둘 다 조금씩 익혀두면 실용적입니다.
Chrome DevTools에서 페이지를 열고 요소를 우클릭 → “검사”로 HTML을 확인할 수 있습니다. DevTools 콘솔에서 document.querySelector('#email')를 입력하면 CSS 셀렉터를 그 자리에서 시험해볼 수 있습니다. XPath는 $x("//button[@type='submit']")으로 확인할 수 있습니다.
ARIA 역할에 기반하며, button·textbox·checkbox·link·heading·combobox·listbox 등을 사용할 수 있습니다. 대부분의 HTML 요소에는 대응하는 역할이 있습니다. 자세한 내용은 Playwright 공식 문서의 “getByRole”을 참조하세요.
“테스트 자동화를 위해 요소에 식별자가 필요합니다. data-testid 속성을 주요 인터랙티브 요소(버튼·입력 필드·링크 등)에 부여해 주세요”라고 요청합니다. data-testid는 테스트 전용 속성으로 프로덕션 코드의 동작에 영향을 미치지 않는다는 점도 강조하면 받아들여지기 쉽습니다. 명명 규칙을 팀에서 미리 정해두면 원활합니다.
신규 프로젝트라면 Playwright 쪽이 로케이터의 표현력이 높고 깨지기 어려운 테스트를 작성하기 쉽습니다. get_by_role·get_by_test_id와 같은 시맨틱 로케이터가 표준으로 사용할 수 있습니다. 다만 기존 프로젝트에서 Selenium을 사용하고 있는 경우 data-testid × CSS 셀렉터로 충분한 안정성을 확보할 수 있습니다.
정리
| 포인트 | 내용 |
|---|---|
| 로케이터 우선순위 | data-testid → ID → CSS → XPath(상대) |
| CSS 셀렉터 | #id·.class·[속성]·부모자식 관계. 가독성이 높다 |
| XPath | 텍스트·부모 요소 등 CSS로 쓸 수 없는 조건에 사용. 절대 XPath는 비권장 |
| Playwright 로케이터 | get_by_test_id → get_by_role → get_by_label 순으로 우선 |
| data-testid | 개발자와 연계해서 테스트 전용 속성을 부여. 디자인 변경에 강함 |
| 피해야 할 것 | 절대 XPath·동적 클래스명·위치 지정(div[2] 등) |
로케이터 선택법을 개선하는 것만으로도 테스트의 안정성은 크게 달라집니다.
우선 현재 보유한 테스트 코드에서 절대 XPath나 동적 클래스명을 사용하고 있는 로케이터를 찾아 data-testid나 속성 기반의 로케이터로 바꾸는 것부터 시작해 보세요.
로케이터가 안정되면 다음 단계로 Page Object Model(POM)을 사용한 테스트 코드 정리에 임하면 더욱 유지보수하기 쉬워집니다.

