Page Object에서 이런 코드를 발견했다
def click_save_button(self):
try:
button = self.page.locator(self.locators.SAVE_BUTTON)
button.clikc() # ← 오타! click이 아니라 clikc
except Exception:
logger.warning("저장 버튼을 찾을 수 없음")
return None
button.clikc()은 AttributeError를 발생시킨다
except Exception이 이걸 잡아서 "저장 버튼을 찾을 수 없음"으로 로깅한다
로그만 보면 로케이터 문제 같다
실제로는 오타다
이 문제가 왜 오래 숨어 있었는가
except Exception은 Python의 거의 모든 예외를 잡는다
Playwright 에러만 잡으려고 작성한 코드가 코드 자체의 버그도 같이 삼킨다
| 잡으려는 예외 | 같이 잡히는 예외 |
|---|---|
| `PlaywrightTimeoutError` (요소 대기 실패) | `AttributeError` (오타, 없는 속성) |
| `PlaywrightError` (브라우저 연결 끊김) | `NameError` (정의 안 된 변수) |
| `TypeError` (잘못된 인자 전달) | |
| `KeyError` (없는 딕셔너리 키) |
디버깅 난이도가 급격히 올라간다
코드 버그인데 UI 문제로 보이니까 로케이터를 바꿔보고, 대기 시간을 늘려보고, MCP snapshot을 찍어본다
원인은 오타 한 글자인데 2시간을 쓸 수 있다
해결: 예외 범위를 Playwright로 한정
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import Error as PlaywrightError
def click_save_button(self):
try:
button = self.page.locator(self.locators.SAVE_BUTTON)
button.click()
except PlaywrightTimeoutError:
logger.warning("저장 버튼 대기 시간 초과")
return None
except PlaywrightError as e:
logger.warning(f"Playwright 오류: {e}")
return None
AttributeError는 잡히지 않는다
오타가 있으면 스택 트레이스가 즉시 터진다
바로 수정할 수 있다
9개 파일 전수 교체한 과정
30곳 이상의 except Exception을 한 번에 교체해야 했다
한 번에 하면 중간에 깨지는 코드가 생길 수 있다
2단계로 나눴다
Step 1: ElementNotFoundError 시그니처 유연화
기존:
class ElementNotFoundError(Exception):
def __init__(self, element_name: str, locator_used: str):
# locator_used가 필수 → 단일 메시지로 호출 불가
변경:
class ElementNotFoundError(Exception):
def __init__(self, element_name: str, locator_used: str = ""):
if locator_used:
message = f"요소 '{element_name}' 찾기 실패 (locator: {locator_used})"
else:
message = element_name # 단순 메시지로 사용
이제 raise ElementNotFoundError("단순 메시지")로도 호출할 수 있다
기존 2인자 호출도 그대로 동작한다
Step 2: 실제 교체
# Before
raise Exception("프로젝트를 찾을 수 없습니다")
# After
raise ElementNotFoundError("프로젝트를 찾을 수 없습니다")
# Before
except Exception:
pass
# After
except (PlaywrightTimeoutError, PlaywrightError):
pass
이 순서가 중요하다
Step 1 없이 Step 2만 하면, ElementNotFoundError("메시지") 호출에서 TypeError: __init__() missing 1 required positional argument 가 발생한다
부수 효과: 모달 dismiss 통합
except Exception 교체 작업 중에 모달 dismiss 코드가 3곳에 흩어져 있는 걸 발견했다
# 페이지 이동마다 이 3줄이 반복
self.dismiss_free_trial_modal()
self.dismiss_feature_release_modal()
self.close_tutorial_overlay()
한 메서드로 통합했다
def ensure_no_blocking_overlay(self) -> None:
self.dismiss_free_trial_modal()
self.dismiss_feature_release_modal()
self.close_tutorial_overlay()
각 메서드는 모달이 없으면 즉시 반환하므로 성능 영향이 거의 없다
체크리스트
| 항목 | 확인 |
|---|---|
| `except Exception` 대신 구체적 예외 타입을 사용하는가 | |
| `except (PlaywrightTimeoutError, PlaywrightError)`로 범위를 한정하는가 | |
| `raise Exception` 대신 도메인 예외(`ElementNotFoundError`)를 사용하는가 | |
| 대규모 시그니처 변경은 2단계(인터페이스 확장 → 호출부 교체)로 나누는가 |
except Exception은 편하다
하지만 코드 버그를 숨겨서 디버깅 비용을 10배로 만든다
'TIL > Claude Code' 카테고리의 다른 글
| [TIL][Playwright] pages/ 전체 210개 wait_for_timeout을 0개로 제거한 4단계 과정 (0) | 2026.04.14 |
|---|---|
| [TIL][Playwright] E2E 검증에서 가장 위험한 코드는 "항상 통과하는 검증"이다 (10) | 2026.04.12 |
| [TIL][Playwright] 모달이 버튼 인덱스를 밀어낸다 — nth() 로케이터의 함정 (0) | 2026.04.04 |
| [TIL][Claude Code] Playwright MCP 로 E2E 디버깅하기 — browser_snapshot 워크플로우 (2) | 2026.04.02 |
| [TIL][Claude Code] "이게 최선이야?" 를 자동으로 묻게 만들기 (0) | 2026.03.30 |