학습 / 치트 시트
XPath·CSS 셀렉터 치트 시트
Selenium, Playwright, 브라우저 DevTools에서 쓰는 XPath와 CSS를 나란히 정리했습니다. CSS로 표현할 수 있다면 CSS를 쓰세요. 더 짧고 개발자에게도 익숙한 문법입니다. 화면에 보이는 텍스트로 찾거나 부모·조상 요소로 거슬러 올라가야 할 때는 XPath를 씁니다.
XPath 1.0 (브라우저 구현 기준), 최신 Chrome, Firefox, Safari가 지원하는 CSS Selectors Level 4 기준으로 검증
기본
| 목적 | XPath | CSS |
|---|---|---|
| 태그로 요소 찾기 (어디서든) | //button | button |
| id로 | //*[@id='email'] | #email |
| 클래스로 (여러 클래스 중 하나) | //*[contains(concat(' ', normalize-space(@class), ' '), ' active ')] | .active |
| 직계 자식 | //ul/li | ul > li |
| 모든 하위 요소 | //form//input | form input |
| 여러 셀렉터 (OR) | //h1 | //h2 | h1, h2 |
| 모든 요소 | //* | * |
속성
| 목적 | XPath | CSS |
|---|---|---|
| 속성이 있음 | //button[@disabled] | button[disabled] |
| 같음 | //input[@name='q'] | input[name='q'] |
| 포함 | //a[contains(@href, 'orders')] | a[href*='orders'] |
| ~로 시작 | //a[starts-with(@href, 'https')] | a[href^='https'] |
| ~로 끝남 | //a[substring(@href, string-length(@href) - 3) = '.pdf'] | a[href$='.pdf'] |
| 두 조건 모두 (AND) | //input[@type='text' and @required] | input[type='text'][required] |
| 둘 중 하나 (OR) | //input[@type='email' or @type='tel'] | input[type='email'], input[type='tel'] |
| 부정 (NOT) | //input[not(@disabled)] | input:not([disabled]) |
| 테스트 ID | //*[@data-testid='submit'] | [data-testid='submit'] |
XPath 1.0에는 ends-with()가 없습니다. substring()을 쓰는 방식이 어디서나 동작하는 우회책입니다.
텍스트 (XPath 전용)
| 목적 | XPath | CSS |
|---|---|---|
| 텍스트 정확히 일치 (공백 정규화) | //button[normalize-space()='Sign in'] | 불가 |
| 텍스트 포함 | //p[contains(., 'expired')] | 불가 |
| 대소문자 무시하고 포함 | //p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'expired')] | 불가 |
| 자기 자신의 텍스트 노드만 | //label[text()='Email'] | 불가 |
. (문자열 값)은 자식 요소의 텍스트까지 포함하지만, text()는 해당 요소 자신의 텍스트 노드만 매칭합니다. normalize-space()나 .을 우선 사용하세요.
관계와 축(axis)
| 목적 | XPath | CSS |
|---|---|---|
| 부모 | //input[@id='email']/.. | :has(> #email) |
| 셀을 포함한 상위 행 | //td[normalize-space()='#1043']/ancestor::tr | tr:has(td[data-id='1043']) |
| 바로 다음 형제 | //label[.='Password']/following-sibling::input[1] | label + input |
| 뒤따르는 모든 형제 | //h2/following-sibling::p | h2 ~ p |
| 바로 앞 형제 | //button/preceding-sibling::input[1] | 불가 |
| 레이블 셀 옆의 셀 | //td[.='Diego Ramos']/following-sibling::td[1] | 불가(텍스트에서 시작) |
| 특정 자식을 포함한 요소 | //li[.//button[@aria-label='Remove']] | li:has(button[aria-label='Remove']) |
:has()는 최신 Chrome, Edge, Safari, Firefox(121 이상)에서 동작합니다. 구형 브라우저와 일부 도구는 지원하지 않습니다.
위치
| 목적 | XPath | CSS |
|---|---|---|
| 첫 번째 자식 행 (1부터 시작) | //tbody/tr[1] | tbody tr:first-child |
| N번째 행 | //tbody/tr[3] | tbody tr:nth-child(3) |
| 마지막 행 | //tbody/tr[last()] | tbody tr:last-child |
| 끝에서 두 번째 | //tbody/tr[last()-1] | tbody tr:nth-last-child(2) |
| 짝수 행 전부 | //tbody/tr[position() mod 2 = 0] | tbody tr:nth-child(even) |
| 페이지 전체에서 첫 번째 일치 요소 | (//button)[1] | 불가: 도구의 first()를 사용합니다 |
//tr[1]은 "페이지의 첫 번째 tr"이 아니라 "부모 안에서 첫 번째인 모든 tr"을 뜻합니다. 페이지의 첫 번째 tr이 필요하면 (//tr)[1]을 쓰세요.
상태 가상 클래스 (CSS)
| 목적 | XPath | CSS |
|---|---|---|
| 체크된 체크박스 또는 라디오 | //input[@checked] (initial state only) | input:checked |
| 비활성화됨 | //button[@disabled] | button:disabled |
| 필수 입력 필드 | //input[@required] | input:required |
| 빈 요소 | //td[not(node())] | td:empty |
XPath는 HTML에 작성된 그대로의 속성을 읽기 때문에, 사용자가 체크박스를 체크해도 @checked는 바뀌지 않습니다. CSS의 :checked는 현재 상태를 반영합니다.
DevTools에서 직접 해 보기
| 작업 | 코드 |
|---|---|
| CSS, 일치하는 모든 요소 (Console) | $$('tbody tr') |
| XPath, 일치하는 모든 요소 (Console) | $x("//td[.='Mei Lin']") |
| Elements 패널에서 검색 | Ctrl+F / Cmd+F, then type a selector or XPath |
| 일치 개수 세기 | $$('tbody tr').length |