Page Object Model이란? Playwright×Python으로 코드 유지보수성을 높이는 설계 패턴 입문

Page Object Model(POM)은 테스트 자동화 코드의 유지보수성·재사용성·가독성을 극적으로 향상시키는, 현장에서 가장 널리 사용되는 설계 패턴입니다.

📌 이런 분께 추천합니다

  • 테스트 자동화를 막 시작했는데 코드 정리 방법을 모르는 분
  • 스크립트가 늘어날수록 유지보수가 힘들어지고 있는 분
  • Page Object Model이라는 말은 들었지만 구현 방법을 모르는 분
  • Playwright·Selenium에서 POM을 실전으로 활용하고 싶은 Python 엔지니어

✅ 이 글을 읽으면 알 수 있는 것

  • Page Object Model의 개념과 왜 필요한지를 이해할 수 있다
  • POM 없는 코드와 있는 코드를 비교해 차이를 실감할 수 있다
  • Playwright(Python)에서의 POM 구현 방법을 구체적으로 알 수 있다
  • 현장에서 바로 쓸 수 있는 베스트 프랙티스를 익힐 수 있다

👤
글쓴이 소개:QA 엔지니어로서 Selenium·Playwright·Python·pytest를 활용한 테스트 자동화를 실무에서 담당하고 있습니다. Page Object Model을 도입해 유지보수 비용을 대폭 줄인 경험을 바탕으로, 초보자도 실천할 수 있는 수준으로 해설합니다. 구현 코드는 GitHub에도 공개 중입니다.

📌 이 글의 결론

Page Object Model이란 「페이지마다 클래스를 만들고, 조작을 메서드로 정의하는」 설계 패턴입니다. UI가 바뀌어도 테스트 코드 수정은 한 곳으로 끝나며, 유지보수 비용을 극적으로 줄일 수 있습니다. 테스트 자동화를 장기적으로 운용할 예정이라면 POM 도입은 필수입니다.

테스트 자동화를 시작하면 처음에는 단순한 스크립트로 충분합니다. 하지만 테스트 케이스가 늘어나고 UI가 변경될 때마다 수십 곳의 코드를 수정해야 하는 상황에 빠지기 쉽습니다. 「자동화했는데 유지보수가 너무 힘들어…」——그 원인의 대부분은 설계 패턴 없이 테스트 코드를 작성하고 있기 때문입니다. 이 글에서는 그런 고민을 해결하는 Page Object Model(POM)에 대해, 개념부터 구현까지 친절하게 해설합니다.

Page Object Model이란?

Page Object Model(POM)이란, 웹 애플리케이션의 각 페이지를 클래스로 표현하고, 해당 페이지의 조작을 메서드로 정리하는 설계 패턴입니다. 2009년 Selenium 커뮤니티에서 제안되어, 현재는 Selenium·Playwright·Cypress 등 많은 테스트 프레임워크에서 표준 설계 방법으로 채택되고 있습니다.

📐 POM의 핵심 개념

🖥️ Page Object

각 페이지의 요소(로케이터)와 조작(메서드)을 모아 놓은 클래스

🧪 Test Code

Page Object의 메서드를 호출하기만 하면 됨. HTML 세부 사항을 몰라도 OK

✅ 결과

UI가 바뀌어도 수정은 Page Object만. 테스트 코드는 변경 불필요

💡 포인트:POM의 본질은 「테스트의 의도(무엇을 테스트하고 싶은가)」와 「구현의 세부(어떤 HTML을 조작하는가)」를 분리하는 것입니다. 이 분리가 있기 때문에 어느 쪽이 바뀌어도 영향 범위가 최소화됩니다.

POM 없는 코드:무엇이 문제인가?

먼저 POM을 사용하지 않는 코드를 살펴봅시다. 처음에는 단순해 보이지만, 테스트가 늘어날수록 심각한 문제가 발생합니다.

❌ POM 없음:흔히 볼 수 있는 작성 방법

test_login_no_pom.py

import pytest
from playwright.sync_api import sync_playwright

def test_login_success():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()

        # 로그인 페이지로 이동
        page.goto("https://example.com/login")

        # 이메일 주소 입력
        page.fill("#email", "user@example.com")

        # 패스워드 입력
        page.fill("#password", "secret123")

        # 로그인 버튼 클릭
        page.click("#login-btn")

        # 대시보드가 표시되는지 확인
        assert page.is_visible("#dashboard")
        browser.close()

def test_login_failure():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()

        # 같은 셀렉터(#email, #password, #login-btn)가 다시 등장!
        page.goto("https://example.com/login")
        page.fill("#email", "wrong@example.com")
        page.fill("#password", "wrongpass")
        page.click("#login-btn")

        # 에러 메시지 확인
        assert page.is_visible("#error-message")
        browser.close()

⚠️ 이 작성 방법의 문제점

  • 셀렉터(#email 등)가 여러 테스트에 흩어져 있다. HTML이 바뀌면 모든 테스트를 수정해야 한다
  • 같은 코드가 중복되어 있다(DRY 원칙 위반). 테스트가 100개가 되면 100곳을 수정해야 한다
  • 테스트 코드가 「무엇을 하는가」가 아니라 「어떻게 조작하는가」가 되어 있다. 가독성이 낮다
  • 브라우저 조작의 세부 사항이 테스트에 섞여 있어 테스트의 의도를 파악하기 어렵다

POM을 사용한 코드:어떻게 달라지는가?

같은 테스트를 POM으로 다시 작성해 봅시다. 코드는 조금 늘어나지만, 장기적인 유지보수 비용이 극적으로 줄어듭니다.

① Page Object 클래스를 만든다

pages/login_page.py

class LoginPage:
    """로그인 페이지의 Page Object 클래스"""

    def __init__(self, page):
        self.page = page

        # 로케이터를 여기에 집약(← 변경이 필요한 경우, 여기만 수정)
        self.email_input    = "#email"
        self.password_input = "#password"
        self.login_button   = "#login-btn"
        self.dashboard      = "#dashboard"
        self.error_message  = "#error-message"

    def navigate(self):
        """로그인 페이지로 이동"""
        self.page.goto("https://example.com/login")

    def login(self, email: str, password: str):
        """로그인 조작을 하나의 메서드로 정리"""
        self.page.fill(self.email_input, email)
        self.page.fill(self.password_input, password)
        self.page.click(self.login_button)

    def is_dashboard_visible(self) -> bool:
        """대시보드가 표시되어 있는지 확인"""
        return self.page.is_visible(self.dashboard)

    def is_error_visible(self) -> bool:
        """에러 메시지가 표시되어 있는지 확인"""
        return self.page.is_visible(self.error_message)

② 테스트 코드가 간결해진다

tests/test_login.py

import pytest
from playwright.sync_api import sync_playwright
from pages.login_page import LoginPage

@pytest.fixture
def login_page():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        login_page = LoginPage(page)
        login_page.navigate()
        yield login_page
        browser.close()

def test_login_success(login_page):
    # ← 셀렉터가 전혀 등장하지 않는다. 테스트의 의도만 작성되어 있다
    login_page.login("user@example.com", "secret123")
    assert login_page.is_dashboard_visible()

def test_login_failure(login_page):
    login_page.login("wrong@example.com", "wrongpass")
    assert login_page.is_error_visible()

✅ POM으로 바꾸어 얻은 개선점

  • 셀렉터가 LoginPage 클래스에 집약. HTML이 바뀌어도 수정은 한 곳만
  • 테스트 코드가 자연어에 가까워진다. login_page.login()을 호출하는 것만으로 무엇을 하는지 한눈에 알 수 있다
  • 새로운 테스트를 추가하기 쉬워진다. Page Object의 메서드를 호출하기만 하면 됨

권장 디렉터리 구성

POM을 실천할 때의 심플하고 사용하기 쉬운 디렉터리 구성입니다. 페이지가 늘어나도 관리하기 쉬운 구조입니다.

project/
├── pages/                    # Page Object 클래스를 모아 두는 디렉터리
│   ├── base_page.py          # 모든 Page에 공통되는 처리(공통 메서드)
│   ├── login_page.py         # 로그인 페이지
│   ├── dashboard_page.py     # 대시보드 페이지
│   └── profile_page.py       # 프로필 페이지
├── tests/                    # 테스트 코드를 모아 두는 디렉터리
│   ├── conftest.py           # fixture 공통 정의(browser, page 등)
│   ├── test_login.py         # 로그인 관련 테스트
│   ├── test_dashboard.py     # 대시보드 관련 테스트
│   └── test_profile.py       # 프로필 관련 테스트
├── pytest.ini                # pytest 설정 파일
└── requirements.txt          # 의존 패키지 목록

발전:Base Page 클래스로 공통 처리를 집약하기

여러 Page Object에 공통되는 처리(대기·스크롤·스크린샷 등)는, Base Page 클래스에 집약하면 재사용성이 더욱 높아집니다.

pages/base_page.py

class BasePage:
    """모든 Page Object의 기저 클래스:공통 처리를 모음"""

    def __init__(self, page):
        self.page = page

    def wait_for_element(self, selector: str, timeout: int = 5000):
        """요소가 표시될 때까지 대기"""
        self.page.wait_for_selector(selector, timeout=timeout)

    def take_screenshot(self, filename: str):
        """스크린샷 촬영"""
        self.page.screenshot(path=f"screenshots/{filename}.png")

    def scroll_to_bottom(self):
        """페이지 맨 아래로 스크롤"""
        self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")


class LoginPage(BasePage):
    """BasePage를 상속해 LoginPage를 만든다"""

    def __init__(self, page):
        super().__init__(page)   # ← BasePage의 초기화를 호출
        self.email_input    = "#email"
        self.password_input = "#password"
        self.login_button   = "#login-btn"

    def login(self, email: str, password: str):
        self.wait_for_element(self.email_input)  # ← 상속한 공통 메서드를 사용
        self.page.fill(self.email_input, email)
        self.page.fill(self.password_input, password)
        self.page.click(self.login_button)

💡 실무 Tip:Base Page에는 어떤 페이지에서도 사용할 것 같은 처리——대기 처리·스크린샷·로그인 상태 확인 등——을 모아 두는 것이 포인트입니다. 공통 처리가 한 곳이 되므로 변경이나 개선이 쉬워집니다.

POM의 장점과 단점

✅ 장점 ⚠️ 단점·주의점
  • UI가 바뀌어도 수정 장소가 한 곳으로 끝남
  • 테스트 코드의 가독성이 높아짐
  • 코드의 재사용성이 높아짐
  • 테스트 추가가 쉬워짐
  • 팀에서의 분업이 용이해짐
  • 초기 설정에 약간의 수고가 필요
  • 테스트가 적은 단계에서는 과도한 설계가 될 수 있음
  • Page Object가 너무 비대해지지 않도록 설계 필요
  • 팀 전원이 POM을 이해하지 않으면 효과가 반감

⚠️ 주의:POM은 만능이 아닙니다. 테스트가 10개 이하인 소규모 프로젝트에서는 POM을 도입하기보다 먼저 테스트를 늘리는 것을 우선합시다. 테스트 케이스가 20~30개를 넘기 시작하면 POM의 혜택이 커집니다.

현장에서 바로 쓸 수 있는 POM 베스트 프랙티스 5가지

① 로케이터는 클래스 변수로 모은다

셀렉터 문자열은 클래스 맨 위에 모아서 정의한다. 테스트 내에 셀렉터를 직접 작성하지 않는다.

② 메서드 이름은 사용자의 조작을 나타내는 동사로 명명한다

click_login_button()보다 login(email, password)처럼, 사용자의 행동 수준으로 명명한다.

③ assert는 테스트 코드 쪽에 작성한다

Page Object 내에 assert를 작성하지 않는다. Page Object는 조작과 상태 취득에 전념하고, 판정은 테스트에 맡긴다.

④ 1페이지 1클래스를 기본으로 한다

페이지가 늘어나도 클래스를 나누면 가독성이 좋아진다. 헤더·푸터 등 공통 UI는 별도 클래스로 한다.

⑤ conftest.py로 fixture를 공통화한다

브라우저 실행·페이지 생성 등의 공통 설정은 conftest.py의 fixture에 모아서, 각 테스트에서 재사용한다.

POM 도입 시 흔한 실패 패턴

실패 패턴 문제점과 대책
Page Object에 assert를 작성한다 조작과 검증이 혼재되어 읽기 어려워짐. assert는 테스트 코드에 작성한다
1 클래스에 모든 페이지를 모음 클래스가 비대해져 관리할 수 없게 됨. 1페이지 1클래스를 철저히 한다
테스트 내에 셀렉터를 직접 작성 POM의 의미가 없어짐. 셀렉터는 반드시 Page Object 내에 정의한다
메서드 이름이 구현 측에 치우침 click_button_id_42()가 아닌 submit_order()처럼 사용자 시점으로 명명한다
Base Page에 무엇이든 집어넣는다 Base Page도 비대해짐. 정말로 공통적인 처리만 놓는다

🔗 관련 기사도 함께 확인하세요

정리

📋 이 글의 정리

  • POM이란 「페이지마다 클래스를 만들고, 조작을 메서드로 정의하는」 설계 패턴
  • 셀렉터를 Page Object에 집약함으로써, UI가 바뀌어도 수정 장소가 한 곳으로 끝남
  • 테스트 코드에서 구현의 세부 사항이 사라져, 「무엇을 테스트하고 싶은가」를 쉽게 읽을 수 있게
  • Base Page 클래스를 사용하면 공통 처리를 더욱 집약할 수 있어 재사용성이 높아짐
  • assert는 Page Object에 작성하지 않고, 테스트 코드 측에 작성하는 것이 원칙
  • 테스트 케이스가 20~30개를 넘기 시작하면 POM 도입의 혜택이 커짐

POM은 한 번 익혀 두면 어떤 테스트 자동화 프로젝트에서도 활용할 수 있는 강력한 무기가 됩니다. 먼저 로그인 페이지 등 가장 자주 사용하는 페이지에서 딱 하나만 만들어 보는 것부터 시작해 보세요.

タイトルとURLをコピーしました