Showing Posts From
Css
- 27 Dec, 2025
XPath vs CSS Selector: 언제 뭘 써야 할까
XPath vs CSS Selector: 언제 뭘 써야 할까 오늘도 깨진 테스트 출근했다. 슬랙 알람 17개. "자동화 테스트 전부 실패했는데요?" 어제 개발자가 로그인 폼 디자인 살짝 바꿨다. class 이름 하나 변경. 내 테스트 스크립트 83개가 깨졌다. driver.find_element(By.XPATH, "//div[@class='login-container']/form/input[1]")이런 XPath를 썼었다. 멍청했다. 2시간 동안 로케이터 전부 수정했다. 점심도 못 먹었다. 오늘은 이 얘기를 써야겠다고 생각했다. XPath와 CSS Selector. 둘 다 쓸 줄 알지만 언제 뭘 써야 하는지 정확히 아는 사람은 적다. 나도 3년 차까지는 몰랐다.로케이터가 뭔지부터 자동화 테스트에서 웹 요소를 찾는 방법이다. "이 버튼 클릭해" 하려면 일단 그 버튼을 찾아야 한다. Selenium이나 Playwright한테 "여기 있어" 알려주는 게 로케이터다. 방법은 여러 개다.ID Name Class Name Tag Name Link Text XPath CSS SelectorID가 제일 좋다. 유일하고 빠르고 안 깨진다. 근데 현실은 ID 없는 요소가 태반이다. 그래서 XPath나 CSS Selector를 쓴다. 이 둘이 제일 강력하다. 거의 모든 요소를 찾을 수 있다. 문제는 강력할수록 잘못 쓰기 쉽다는 거다. 칼이 날카로울수록 조심해야 하듯이. 나는 신입 때 XPath만 썼다. 개발자 도구에서 Copy XPath 버튼 눌러서 복붙했다. /html/body/div[1]/div[2]/div[3]/form/div[1]/input이런 게 나온다. 겁나 길다. 그리고 UI 조금만 바뀌면 바로 깨진다. 3년 차 되니까 CSS Selector가 더 편하다는 걸 알았다. 4년 차인 지금은 상황에 따라 섞어 쓴다. XPath의 장점과 단점 XPath는 XML Path Language다. HTML은 XML의 일종이니까 쓸 수 있다. 장점부터. 상위로 올라갈 수 있다. # 버튼 찾고 그 부모의 부모 찾기 driver.find_element(By.XPATH, "//button[@id='submit']/../../")CSS Selector는 이게 안 된다. 부모나 형제를 찾을 수 없다. 오직 자식만. 복잡한 DOM 구조에서 특정 요소 기준으로 위로 올라가야 할 때 XPath만 답이다. 텍스트로 찾을 수 있다. # "로그인" 텍스트를 가진 버튼 driver.find_element(By.XPATH, "//button[text()='로그인']")# "확인"을 포함하는 버튼 driver.find_element(By.XPATH, "//button[contains(text(), '확인')]")진짜 편하다. 디자이너가 class 이름 맘대로 바꿔도 텍스트는 잘 안 바뀐다. 복잡한 조건을 쓸 수 있다. # position으로 찾기 //div[@class='item'][position()>2]# 여러 조건 and/or //input[@type='text' and @name='username']이런 건 CSS Selector로 못 한다. 단점도 명확하다. 느리다. CSS Selector보다 평균 10-20% 느리다. 브라우저는 CSS Selector를 네이티브로 지원한다. XPath는 그렇지 않다. 테스트 케이스 10개면 상관없다. 1000개면 체감된다. 문법이 복잡하다. //, /, @, [], .. 이런 기호들. 헷갈린다. 실수하기 쉽다. 신입 QA한테 가르치기도 어렵다. "이건 왜 슬래시가 2개예요?" 질문 받으면 설명이 길어진다. 브라우저마다 미묘하게 다르다. Chrome에서 되는 XPath가 Firefox에서 안 될 때가 있다. 많지는 않지만 가끔 당한다.CSS Selector의 장점과 단점 CSS Selector는 웹 개발자들이 스타일링할 때 쓰는 그거다. 장점. 빠르다. 브라우저 엔진이 직접 지원한다. 최적화도 잘 돼 있다. XPath보다 확실히 빠르다. 대규모 E2E 테스트 스위트 돌릴 때 차이가 난다. 문법이 직관적이다. # ID로 찾기 driver.find_element(By.CSS_SELECTOR, "#username")# Class로 찾기 driver.find_element(By.CSS_SELECTOR, ".login-button")# 속성으로 찾기 driver.find_element(By.CSS_SELECTOR, "input[type='password']")# 자식 찾기 driver.find_element(By.CSS_SELECTOR, "form > input")깔끔하다. 읽기 쉽다. 후배한테 가르치기도 편하다. 크로스 브라우저 호환성이 좋다. CSS는 표준이다. Chrome이든 Firefox든 Safari든 똑같이 작동한다. 단점. 위로 못 올라간다. 부모 선택자가 없다. :has()가 있긴 한데 Selenium에서 지원 안 하는 경우가 많다. 형제 요소 찾기도 제한적이다. 바로 다음 형제만 +로 찾을 수 있다. 텍스트로 못 찾는다. 이게 제일 아쉽다. 버튼 텍스트로 직접 찾을 방법이 없다. # 이런 거 안 됨 driver.find_element(By.CSS_SELECTOR, "button[text='로그인']") # 틀림XPath 써야 한다. 복잡한 조건에 약하다. position이나 조건 로직 같은 건 표현하기 어렵다. 내가 쓰는 기준 4년 동안 삽질하면서 정리한 내 원칙이다. 기본은 CSS Selector. 웬만하면 CSS Selector 쓴다. 빠르고 읽기 쉽고 안정적이다. # 좋음 driver.find_element(By.CSS_SELECTOR, "button[data-testid='submit']") driver.find_element(By.CSS_SELECTOR, ".modal-content input[name='email']")텍스트로 찾아야 하면 XPath. 버튼 라벨, 링크 텍스트, 에러 메시지. 이런 건 텍스트로 찾는 게 제일 안정적이다. # 텍스트 기반 로케이터 driver.find_element(By.XPATH, "//button[text()='다음']") driver.find_element(By.XPATH, "//a[contains(text(), '비밀번호 찾기')]") driver.find_element(By.XPATH, "//span[@class='error' and contains(text(), '필수')]")UI 디자인 바뀌어도 버튼에 쓰인 "다음" 텍스트는 잘 안 바뀐다. 부모/형제 찾아야 하면 XPath. 체크박스 옆의 라벨 텍스트로 체크박스 찾기. 이런 패턴. # 라벨 텍스트로 체크박스 찾기 checkbox = driver.find_element(By.XPATH, "//label[text()='이용약관 동의']/preceding-sibling::input[@type='checkbox']")CSS Selector로는 불가능하다. 테이블 같은 복잡한 구조는 XPath. 특정 행의 특정 열 찾기. position 필요할 때. # 3번째 행의 2번째 셀 cell = driver.find_element(By.XPATH, "//table[@id='data-table']/tbody/tr[3]/td[2]")절대 경로는 쓰지 않는다. # 이런 거 절대 금지 driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/form/input[3]")Copy XPath 해서 나온 거 그대로 쓰면 이렇다. 개발자가 div 하나만 추가해도 깨진다. 상대 경로를 써야 한다. # 이렇게 driver.find_element(By.XPATH, "//form[@id='login-form']//input[@name='username']")안정적인 로케이터 전략 UI 변경에 강한 로케이터를 만드는 게 핵심이다. 1. data-testid 속성을 쓴다. 개발자한테 요청해서 테스트용 속성을 넣어달라고 한다. <button data-testid="submit-button">제출</button>driver.find_element(By.CSS_SELECTOR, "[data-testid='submit-button']")이게 제일 안정적이다. 디자인 바뀌어도 안 깨진다. 우리 팀은 모든 주요 요소에 data-testid 붙이기로 컨벤션 정했다. 개발자들한테 처음엔 귀찮다는 소리 들었다. 지금은 당연하게 생각한다. 2. 의미 있는 속성을 우선한다. 좋은 순서:ID (있으면) data-testid name type + name 조합 class (여러 개 중 의미 있는 것) 텍스트 구조 기반 (최후의 수단)# 좋음 driver.find_element(By.CSS_SELECTOR, "#username") driver.find_element(By.CSS_SELECTOR, "input[name='email']")# 나쁨 driver.find_element(By.CSS_SELECTOR, "div > div > input:nth-child(2)")3. 동적 class는 피한다. # 나쁨 - 빌드마다 바뀌는 해시 driver.find_element(By.CSS_SELECTOR, ".css-1hw23kj-button")# 좋음 - 안정적인 class driver.find_element(By.CSS_SELECTOR, ".primary-button")Tailwind나 CSS Modules 쓰면 class 이름에 해시가 붙는다. 절대 쓰면 안 된다. 4. nth-child는 조심한다. # 위험 - 순서 바뀌면 깨짐 driver.find_element(By.CSS_SELECTOR, "form input:nth-child(2)")# 나음 - 속성으로 특정 driver.find_element(By.CSS_SELECTOR, "form input[type='password']")리스트나 테이블에서 position이 중요한 경우에만 써야 한다. 5. 로케이터를 변수로 관리한다. Page Object Pattern을 쓴다. class LoginPage: USERNAME_INPUT = (By.CSS_SELECTOR, "input[name='username']") PASSWORD_INPUT = (By.CSS_SELECTOR, "input[name='password']") SUBMIT_BUTTON = (By.XPATH, "//button[text()='로그인']") def login(self, username, password): driver.find_element(*self.USERNAME_INPUT).send_keys(username) driver.find_element(*self.PASSWORD_INPUT).send_keys(password) driver.find_element(*self.SUBMIT_BUTTON).click()로케이터 한 곳에서 관리. UI 바뀌면 여기만 수정하면 된다.성능 비교 실험 궁금해서 직접 측정해봤다. 테스트 환경:페이지: 복잡한 대시보드 (DOM 요소 500+) 반복: 각 로케이터 100회 실행 브라우저: Chrome 120결과: ID: 평균 12ms CSS Selector: 평균 18ms XPath (상대경로): 평균 23ms XPath (절대경로): 평균 35msID가 제일 빠르다. 당연하다. CSS Selector가 XPath보다 약 30% 빠르다. 절대 경로 XPath는 두 배 느리다. 쓰면 안 되는 이유가 하나 더. 100회면 차이가 1초. 전체 테스트 스위트 1000개면 10초다. 무시 못 한다. 근데 성능보다 중요한 게 안정성이다. 0.01초 빨라도 매주 깨지면 소용없다. 실제 사례들 사례 1: 동적 폼 회원가입 폼. 선택 항목에 따라 필드가 추가된다. 처음에 이렇게 짰다: # 나쁨 driver.find_element(By.XPATH, "//form/div[3]/input")"기업 회원" 선택하면 사업자등록번호 필드가 생긴다. 그럼 div 순서가 바뀐다. 깨진다. 수정: # 좋음 driver.find_element(By.CSS_SELECTOR, "input[name='phone']") driver.find_element(By.XPATH, "//label[text()='전화번호']/following-sibling::input")name 속성이나 라벨 텍스트로 찾으니 안정적이다. 사례 2: 모달 창 "정말 삭제하시겠습니까?" 확인 모달. 페이지에 여러 모달이 있다. class 이름이 전부 .modal이다. # 나쁨 - 어떤 모달인지 모름 driver.find_element(By.CSS_SELECTOR, ".modal button")첫 번째 모달의 버튼을 찾는다. 원하는 모달이 아닐 수 있다. 수정: # 좋음 - 텍스트로 특정 modal = driver.find_element(By.XPATH, "//div[contains(@class, 'modal') and contains(., '삭제하시겠습니까')]") confirm_button = modal.find_element(By.XPATH, ".//button[text()='확인']")모달 내용으로 찾고, 그 안에서 버튼을 찾는다. 사례 3: 동적 리스트 상품 목록. 개수가 계속 바뀐다. 특정 상품의 "장바구니" 버튼 클릭해야 한다. # 나쁨 driver.find_element(By.XPATH, "//div[@class='product-list']/div[5]/button")5번째 상품이 뭔지 모른다. 상품 추가되면 순서 바뀐다. 수정: # 좋음 product_name = "무선 키보드" button = driver.find_element( By.XPATH, f"//div[contains(@class, 'product-item') and contains(., '{product_name}')]//button[text()='장바구니']" )상품 이름으로 찾으니 확실하다. 디버깅 팁 로케이터 안 될 때 내가 하는 것들. 1. 개발자 도구에서 직접 테스트 Console에서: // CSS Selector 테스트 document.querySelector("input[name='username']")// XPath 테스트 $x("//button[text()='로그인']")결과가 나오면 로케이터는 맞다. 타이밍 문제다. null 나오면 로케이터가 틀렸다. 2. 암묵적 대기 vs 명시적 대기 # 암묵적 대기 - 전역 설정 driver.implicitly_wait(10)# 명시적 대기 - 특정 요소 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as ECelement = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='username']")) )동적 로딩이 많으면 명시적 대기가 낫다. 3. Screenshot 찍기 요소 못 찾으면 그 순간 스크린샷 저장. try: driver.find_element(By.CSS_SELECTOR, ".submit-button").click() except NoSuchElementException: driver.save_screenshot("debug.png") raise뭔가 예상과 다르게 렌더링됐는지 바로 알 수 있다. 4. 여러 로케이터 시도 def find_element_flexible(driver, *locators): for locator in locators: try: return driver.find_element(*locator) except NoSuchElementException: continue raise NoSuchElementException(f"None of the locators worked: {locators}")# 사용 button = find_element_flexible( driver, (By.CSS_SELECTOR, "[data-testid='submit']"), (By.XPATH, "//button[text()='제출']"), (By.CSS_SELECTOR, "button.primary") )백업 로케이터를 둔다. 하나 바뀌어도 다른 걸로 찾는다. 팀 컨벤션 우리 팀 규칙. 1. 로케이터 우선순위 1. data-testid (CSS) 2. ID (CSS) 3. name (CSS) 4. 텍스트 (XPath) 5. 복합 속성 (CSS) 6. 구조 + 텍스트 (XPath)이 순서대로 시도. 문서화했다. 2. 절대 경로 금지 코드 리뷰에서 절대 경로 보이면 즉시 reject. # ❌ Rejected "/html/body/div[1]/..."# ✅ Approved "//form[@id='login']//input"3. Page Object 필수 직접 로케이터 쓰지 말고 Page Object 통해서. # ❌ Bad driver.find_element(By.CSS_SELECTOR, "input[name='username']").send_keys("test")# ✅ Good login_page.enter_username("test")4. 로케이터 네이밍 # 명확하게 USERNAME_INPUT = (By.CSS_SELECTOR, "input[name='username']") SUBMIT_BUTTON = (By.XPATH, "//button[text()='로그인']")# 애매하게 말고 INPUT_1 = (By.CSS_SELECTOR, "input:nth-child(1)") # ❌ BTN = (By.XPATH, "//button") # ❌5. 주석으로 이유 설명 특이한 로케이터는 왜 그렇게 했는지 적는다. # XPath 사용 이유: 부모 요소 기준으로 찾아야 함 # 라벨 텍스트로 체크박스 선택 TERMS_CHECKBOX = (By.XPATH, "//label[text()='이용약관 동의']/preceding-sibling::input")실전 체크리스트 로케이터 짤 때 내가 확인하는 것들. 작성 전: 이 요소에 ID나 data-testid 있나? 개발자한테 추가 요청 가능한가? 텍스트 기반으로 찾을 수 있나? 여러 개 있는 요소인가? (리스트, 테이블)작성 중: 절대 경로 안 썼나? 동적 class 안 썼나? nth-child 꼭 필요한가? 더 안정적인 속성 없나?작성 후: 개발자 도구에서 테스트했나? 페이지 새로고침해도 작동하나? 다른 데이터로도 작동하나? (예: 다른 상품명) 백업 로케이터 필요한가?결론 CSS Selector를 기본으로. XPath는 필요할 때만. 빠르고 읽기 쉬운 게 CSS Selector다. 대부분 이걸로 해결된다. 텍스트로 찾거나 부모로 올라가야 하면 XPath 쓴다. 근데 제일 중요한 건 로케이터 종류가 아니다. 얼마나 안정적으로 짜느냐다. ID나 data-testid 쓸 수 있으면 무조건 그거 쓴다. CSS든 XPath든 상관없다. 개발자랑 협업해서 테스트하기 좋은 마크업 만드는 게 제일 중요하다. 로케이터 전략이 없으면 UI 바뀔 때마다 테스트 고친다. 시간 낭비다. 전략 세워두면 테스트가 안정적이다. 자동화가 의미 있어진다. 나도 초반엔 닥치는 대로 Copy XPath 했다. 계속 깨졌다. 지금은 로케이터 짜는 데 시간 쓴다. 그게 나중에 시간 아낀다.오늘 점심 먹으면서 후배한테 이 얘기 했다. "로케이터 전략부터 세워" 라고. 후배는 고개를 끄덕였다. 다음 PR에서 확인할 거다.