지난주에 오래 방치했던 크롤링·자동발행 스크립트 뭉치를 다시 돌리려다 첫 줄에서 막혔다. pip install selenium 한 번 했을 뿐인데 Selenium 4가 깔렸고, 예전에 잘 돌던 코드가 AttributeError부터 DeprecationWarning까지 줄줄이 토해냈다. 세어 보니 손봐야 할 지점이 30군데였다. 그런데 다행히도 깨진 방식은 딱 네 가지 패턴으로 수렴했다. 이 글은 그 네 패턴을 어떻게 찾아서 한 번에 고쳤는지에 대한 기록이다.

먼저 전체 그림부터. 깨진 코드가 어떤 유형으로 나뉘고 각각 어떻게 바뀌는지를 한눈에 정리하면 이렇다.

flowchart TD
    A["Selenium 3 코드 30곳"] --> B{"어떤 API를 썼나?"}
    B -->|"find_element_by_id 등"| C["find_element(By.ID, ...)"]
    B -->|"executable_path=..."| D["Service 객체로 이관"]
    B -->|"chrome_options / desired_capabilities"| E["options 하나로 통합"]
    B -->|"암묵적 wait 의존"| F["명시적 WebDriverWait"]
    C --> G["Selenium 4 정상 동작"]
    D --> G
    E --> G
    F --> G
    classDef box fill:#e7f5ff,stroke:#1c7ed6,color:#10548f;
    classDef ok fill:#ebfbee,stroke:#2f9e44,color:#1b5e2f;
    class A,C,D,E,F box;
    class G ok;

find_element_by_id는 왜 통째로 사라졌나?

가장 많이 깨진 건 예상대로 find_element_by_* 계열이었다. 3.x에서는 driver.find_element_by_id("login")처럼 요소마다 전용 메서드가 있었다. 4.x에서는 이게 전부 제거됐다(deprecated를 넘어 아예 삭제). 대신 By 클래스로 찾는 방식(로케이터를 인자로 넘기는 통일된 방식)만 남았다.

# Selenium 3 (더 이상 안 됨)
el = driver.find_element_by_id("login")
btn = driver.find_element_by_css_selector(".submit")
rows = driver.find_elements_by_class_name("row")
 
# Selenium 4
from selenium.webdriver.common.by import By
el = driver.find_element(By.ID, "login")
btn = driver.find_element(By.CSS_SELECTOR, ".submit")
rows = driver.find_elements(By.CLASS_NAME, "row")

30곳 중 절반 이상이 이 케이스라 손으로 하나씩 고치긴 싫었다. 그래서 정규식 치환으로 밀었다. find_element_by_id\(find_element(By.ID, 식으로 접미사별로 매핑 규칙을 만들고, 편집기 전체 치환을 돌린 뒤 By import만 챙겼다.

flowchart LR
    A["find_element_by_ID_KIND(값)"] --> B["정규식 캡처"]
    B --> C["find_element(By.KIND, 값)"]
    C --> D["import By 추가"]
    classDef s fill:#fff3bf,stroke:#f08c00,color:#8a5a00;
    class A,B,C,D s;

접미사와 By 상수 대응만 표로 정리해 두면 치환 규칙이 그대로 나온다.

Selenium 3 접미사By 상수
_by_idBy.ID
_by_nameBy.NAME
_by_class_nameBy.CLASS_NAME
_by_css_selectorBy.CSS_SELECTOR
_by_xpathBy.XPATH
_by_link_textBy.LINK_TEXT

executable_path가 왜 경고를 뿜나?

두 번째 뭉치는 드라이버를 띄우는 부분이었다. 3.x에서는 webdriver.Chrome(executable_path="...")로 드라이버 실행파일 경로를 직접 넘겼는데 4.x는 이걸 Service 객체(드라이버 프로세스를 관리하는 래퍼)로 감싸도록 바꿨다.

# Selenium 3
driver = webdriver.Chrome(executable_path="C:/drivers/chromedriver.exe")
 
# Selenium 4
from selenium.webdriver.chrome.service import Service
service = Service(executable_path="C:/drivers/chromedriver.exe")
driver = webdriver.Chrome(service=service)

여기서 하나 배운 게 있다. Selenium 4.6부터는 Selenium Manager(드라이버를 자동으로 내려받아 맞춰주는 내장 기능)가 들어왔다. 그래서 경로를 아예 안 넘겨도 브라우저 버전에 맞는 드라이버를 알아서 챙긴다. 나는 이참에 하드코딩된 경로 상수를 지우고 webdriver.Chrome() 한 줄로 줄였다. 크롬이 자동 업데이트되면서 드라이버 버전이 어긋나 깨지던 고질병이 이걸로 사라졌다.

chrome_options랑 desired_capabilities는 어디로 갔나?

세 번째는 브라우저 옵션이었다. 3.x에서는 옵션을 chrome_options로 넘기고 헤드리스나 프록시 같은 능력치는 desired_capabilities라는 별도 딕셔너리로 넘겼다. 4.x는 이 둘을 options 하나로 합쳤다.

# Selenium 3
from selenium.webdriver import DesiredCapabilities
caps = DesiredCapabilities.CHROME.copy()
caps["acceptInsecureCerts"] = True
driver = webdriver.Chrome(chrome_options=opts, desired_capabilities=caps)
 
# Selenium 4
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.set_capability("acceptInsecureCerts", True)
driver = webdriver.Chrome(options=options)

chrome_options= 키워드 자체가 없어져서 이건 경고가 아니라 바로 에러가 났다. 그리고 헤드리스도 --headless가 아니라 --headless=new로 쓰라고 권장이 바뀌어서 겸사겸사 같이 손봤다.

눈에 안 보이게 깨진 건 없었나?

문법 에러는 실행하면 바로 티가 나니 오히려 쉬웠다. 진짜 성가신 건 조용히 어긋난 부분이었다. 4.x는 W3C 프로토콜(브라우저 자동화 표준 규약)을 엄격히 따르면서 요소가 뜨는 타이밍 판정이 예전과 미묘하게 달라졌다. 3.x 시절 암묵적 대기(implicit wait)에만 기대던 코드가 간헐적으로 NoSuchElement를 뱉었다.

# 타이밍에 기대지 말고 조건을 명시
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
 
el = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".result"))
)

간헐적으로 깨지는 부분은 전부 이 명시적 대기(원하는 조건이 만족될 때까지 기다리는 방식)로 바꿨다. 어차피 마이그레이션 안 했어도 언젠가 터졌을 불안한 코드라 이참에 정리한 셈이다.

마이그레이션이 남긴 것

30곳을 고치고 나니 코드가 오히려 짧아졌다. 드라이버 경로 관리 코드가 통째로 사라졌고 옵션 설정이 한 곳으로 모였다. 정리하면 이렇다.

  • find_element_by_*: 정규식 일괄 치환 + By import — 기계적으로 처리 가능
  • executable_path: Service로 감싸거나 아예 제거(Selenium Manager)
  • chrome_options / desired_capabilities: options 하나로 통합
  • 암묵적 대기 의존: WebDriverWait + expected_conditions로 명시화

교훈은 단순했다. 버전을 올릴 땐 릴리스 노트의 “Removed” 항목부터 읽자. 삭제된 API 목록만 훑어도 어디가 깨질지 절반은 미리 알 수 있었다. 다음엔 자동발행 파이프라인이 브라우저 자동화 없이 API만으로 돌아가게 바꿔볼 생각이다. 그러면 이런 마이그레이션 자체를 안 겪어도 되니까.