Showing Posts From
바꿀
- 03 Dec, 2025
개발자가 UI 한 줄 바꿀 때마다 내 스크립트는 왜 폭탄이 될까
개발자가 UI 한 줄 바꿀 때마다 내 스크립트는 왜 폭탄이 될까 새벽 2시의 슬랙 알람 새벽 2시에 핸드폰이 울렸다. Jenkins 알람. 또 깨졌다. "Nightly build failed. 67 tests broken." 눈을 비비고 노트북을 켰다. 테스트 리포트를 열었다. 로그인 버튼을 못 찾는다고 한다. 전부. "ElementNotInteractableException: element not found: #login-btn" 어제 저녁까지 멀쩡했다. 67개 테스트가 동시에 깨질 리 없다. 무언가 바뀐 거다. 개발팀 슬랙을 확인했다. 밤 11시에 프론트엔드 개발자가 올린 커밋. "refactor: button ID 네이밍 컨벤션 변경 (#login-btn → #btn-login)" 이 한 줄. 이 한 줄 때문에 67개. 다시 잤다. 아침에 고치기로 했다.출근해서 본 현실 9시 50분 출근. 커피부터. 테스트 결과를 다시 봤다. 67개가 아니었다. 89개. 로그인 버튼만 바뀐 게 아니었다. 메뉴 버튼, 검색창, 확인 버튼. ID가 전부 바뀌었다. 프론트 개발자한테 물었다. "형, ID 바꿨어?" "응. 네이밍 규칙 통일했어. 코드 리뷰에서 지적받아서." "QA팀한테 얘기 없었는데." "아 미안. 그냥 ID만 바꾼 거라." 그냥 ID만. 그 말이 제일 무섭다. 테스트 스크립트 89개를 열었다. find_element_by_id를 찾았다. 247개. 하나씩 고쳐야 한다.왜 매번 깨지는가 이게 처음이 아니다. 지난달에는 class name이 바뀌었다. "btn-primary"가 "primary-button"이 됐다. 스크립트 52개 수정. 그 전 달에는 div 구조가 바뀌었다. XPath가 전부 틀어졌다. 스크립트 38개 수정. 매달 이런다. 개발자는 UI를 개선한다. 나는 스크립트를 고친다. 왜 이렇게 취약한가. 로케이터 전략이 문제다. 나는 ID로 찾고, class로 찾고, XPath로 찾는다. 개발자는 그걸 바꾼다. 끝. Selenium 코드를 봤다. driver.find_element_by_id("login-btn").click() driver.find_element_by_class_name("btn-primary").click() driver.find_element_by_xpath("//div[@class='container']/button[1]").click()이게 89개 파일에 흩어져 있다. 하나 바뀌면 전부 바꿔야 한다. 문제는 결합도다. 테스트 코드가 UI 구현에 직접 의존한다. UI가 바뀌면 테스트가 깨진다. 필연이다. 해결책은 간단하다. 추상화.페이지 객체 모델이 답이다 POM. Page Object Model. 들어는 봤다. 써본 적은 없었다. 개념은 단순하다. UI를 클래스로 감싼다. 테스트는 클래스를 쓴다. UI가 바뀌면 클래스만 고친다. LoginPage 클래스를 만들었다. class LoginPage: def __init__(self, driver): self.driver = driver self.login_button = (By.ID, "login-btn") self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") def click_login(self): self.driver.find_element(*self.login_button).click() def enter_username(self, username): self.driver.find_element(*self.username_input).send_keys(username)테스트 코드가 바뀌었다. # 전 driver.find_element_by_id("username").send_keys("test@test.com") driver.find_element_by_id("password").send_keys("password123") driver.find_element_by_id("login-btn").click()# 후 login_page = LoginPage(driver) login_page.enter_username("test@test.com") login_page.enter_password("password123") login_page.click_login()ID가 바뀌면 어떻게 되나. LoginPage만 고친다. 한 군데. 89개 테스트를 LoginPage를 쓰게 바꿨다. 이틀 걸렸다. 다음 날 ID가 또 바뀌었다. "#btn-login"이 "#submit-login"이 됐다. LoginPage 한 줄만 고쳤다. 30초. 효과는 즉시 나타났다. 로케이터 전략의 우선순위 POM을 쓰기 시작하면서 로케이터 전략을 다시 생각했다. 무엇으로 요소를 찾을 것인가. ID? Class? XPath? 우선순위를 정했다. 1순위: data 속성 가장 안정적이다. UI용이 아니라 테스트용이다. 개발자한테 부탁했다. "테스트용 속성 좀 넣어줘." <button id="btn-login" data-testid="login-button">로그인</button>data-testid는 바뀔 이유가 없다. UI 디자인과 무관하다. 테스트만을 위한 속성이다. self.login_button = (By.CSS_SELECTOR, "[data-testid='login-button']")처음엔 개발자가 귀찮아했다. "매번 넣어야 돼?" 설득했다. "한 번만 넣으면 됩니다. 안 바뀌잖아요." 점점 늘었다. 지금은 주요 버튼에 다 들어간다. 2순위: ID ID는 유일하다. 빠르다. 하지만 바뀐다. 프론트 개발자가 네이밍 규칙을 바꾸면 ID가 바뀐다. 리팩토링하면 ID가 바뀐다. ID를 쓸 땐 POM 안에만 쓴다. 바뀌면 POM만 고친다. 3순위: CSS Selector class는 자주 바뀐다. 디자인 시스템이 바뀌면 class가 바뀐다. 대신 구조로 찾는다. self.submit_button = (By.CSS_SELECTOR, "form.login-form button[type='submit']")form 안의 submit 버튼. 구조는 잘 안 바뀐다. 꼴찌: XPath XPath는 최후의 수단이다. 취약하다. 느리다. # 나쁜 예 driver.find_element_by_xpath("//div[@class='container']/div[2]/button[1]")div 하나만 추가돼도 깨진다. 순서가 바뀌면 깨진다. XPath를 써야 하면 상대 경로로. # 조금 나은 예 driver.find_element_by_xpath("//button[contains(text(), '로그인')]")텍스트로 찾는다. 텍스트는 안 바뀐다. (국제화하면 바뀐다. 그것도 문제다.) 우선순위를 정하니 스크립트가 안정적이 됐다. 깨지는 빈도가 70% 줄었다. 대기 전략도 중요하다 로케이터만 문제가 아니었다. 가끔 요소를 찾는데 "ElementNotInteractableException"이 뜬다. 요소는 있다. 근데 클릭이 안 된다. 왜냐. 아직 로딩 중이다. 옛날 코드를 봤다. driver.find_element_by_id("login-btn").click()요소가 나타날 때까지 안 기다린다. 바로 찾는다. 없으면 실패. time.sleep(3)을 넣었다. 3초 기다린다. time.sleep(3) driver.find_element_by_id("login-btn").click()문제가 생긴다.요소가 1초에 뜨면 2초를 낭비한다. 요소가 5초 걸리면 실패한다. 테스트가 느려진다. 전체 런타임 30분이 1시간이 됐다.해결책은 명시적 대기. Explicit Wait. from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as ECwait = WebDriverWait(driver, 10) element = wait.until(EC.element_to_be_clickable((By.ID, "login-btn"))) element.click()최대 10초 기다린다. 요소가 클릭 가능해지면 즉시 진행한다. 1초에 뜨면 1초만 기다린다. POM에 넣었다. class LoginPage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def click_login(self): button = self.wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-button']")) ) button.click()Flaky 테스트가 80% 줄었다. 런타임은 원래대로 돌아왔다. 개발팀과의 커뮤니케이션 기술만으로는 부족하다. 사람이 문제다. 개발자는 QA를 모른다. 테스트 스크립트가 있는지도 모른다. 버튼 ID 바꾸면 테스트가 깨지는지 모른다. 몰라서 그런다. 알려줘야 한다. 1. PR에 QA 태그 요청 프론트 개발자한테 부탁했다. "UI 바꾸는 PR엔 저 태그해주세요." 처음엔 깜빡했다. 그래도 계속 말했다. 지금은 습관이 됐다. 태그 받으면 PR을 본다. data-testid가 바뀌나 확인한다. 바뀌면 댓글 단다. "이 속성 테스트에서 쓰고 있어요. 바꾸면 스크립트 수정해야 합니다." 대부분은 안 바꾸는 쪽으로 간다. 가끔은 같이 수정한다. 2. 테스트 커버리지 공유 월요일 스탠드업 때 공유한다. "로그인 플로우 자동화 완료했습니다. 이제 이 부분 건드리면 자동으로 테스트됩니다." 개발자가 안다. 어느 부분이 자동화됐는지. 조심하게 된다. 3. 깨진 테스트 즉시 알림 Jenkins가 실패하면 슬랙에 알람 간다. 개발팀 채널에. "[E2E Test Failed] Login flow broken by commit abc123" 커밋 해시까지 보인다. 누가 깨트렸는지 바로 안다. 처음엔 싫어했다. "버그도 아닌데 왜 알람 와요?" 설명했다. "스크립트가 깨진 것도 비용입니다. 고치는 데 시간이 듭니다." 지금은 자기가 커밋하면 테스트 결과를 확인한다. 깨지면 바로 연락 온다. 4. 테스트용 속성 가이드 문서 Confluence에 문서를 만들었다. "E2E 테스트 친화적인 프론트엔드 개발 가이드"data-testid 네이밍 규칙 동적 ID 피하기 (타임스탬프, 랜덤 문자열) 테스트에서 사용 중인 셀렉터 목록신입 프론트 개발자 온보딩 때 읽게 했다. 리드 개발자가 코드 리뷰할 때 체크한다. 문화가 바뀌었다. 개발자가 먼저 물어본다. "이거 테스트에서 쓰는 거죠?" 그래도 깨질 때 모든 걸 해도 깨진다. 리팩토링. 디자인 시스템 전면 개편. 프레임워크 마이그레이션. 지난달에 React 16에서 18로 올렸다. 렌더링 방식이 바뀌었다. 타이밍이 달라졌다. 테스트 34개가 깨졌다. 어쩔 수 없다. 고친다. 하지만 POM 덕분에 빠르다. 34개 테스트를 고쳤지만 실제로는 5개 페이지 클래스만 수정했다. 전엔 34개 파일을 열어서 일일이 고쳤다. 지금은 5개. 시간이 1/7로 줄었다. 회고 개발자가 UI 바꿀 때마다 스크립트가 폭탄 되는 이유.로케이터가 취약하다. (ID, class 직접 의존) 중복이 많다. (같은 셀렉터가 여러 파일에) 추상화가 없다. (UI와 테스트가 직접 결합) 개발팀이 모른다. (테스트 영향을 생각 안 함)해결책.POM으로 추상화한다. 로케이터 우선순위를 정한다. (data-testid > ID > CSS > XPath) 명시적 대기를 쓴다. 개발팀과 소통한다.완벽할 순 없다. UI는 계속 바뀐다. 테스트도 계속 고쳐야 한다. 하지만 구조를 잘 만들면 유지보수가 쉽다. 한 군데만 고치면 된다. 자동화는 코드다. 코드는 설계가 중요하다. 설계 없이 짠 코드는 레거시가 된다. 테스트 코드도 마찬가지다. 지금 LoginPage 클래스를 본다. 깔끔하다. ID가 바뀌어도 여기만 고치면 된다. 89개 파일을 열던 시절이 기억난다. 지금은 1개만 연다. POM이 답이었다.새벽 알람은 여전히 온다. 하지만 이젠 30초면 고친다. 다시 잔다.