본문으로 건너뛰기
중급20

Page Object Model: 유지보수 가능한 테스트 구조 만들기

로케이터가 테스트 코드 곳곳에 흩어져 있으면 화면 하나가 바뀔 때마다 수십 곳을 고쳐야 한다. 화면을 하나의 객체로 감싸는 POM 패턴으로 이 문제를 해결한다.

  • #PageObjectModel
  • #POM
  • #테스트구조

개념

로케이터가 흩어져 있을 때의 문제

레슨 3에서 배운 안정적인 로케이터를 쓰더라도, 그 로케이터가 테스트 코드 수십 개 파일에 각각 따로 적혀 있으면 문제가 생긴다. 로그인 화면의 구조가 바뀌면, 로그인을 거치는 모든 테스트 파일을 하나하나 찾아 고쳐야 한다.

// 파일 A
await page.getByLabel("이메일").fill(email)
await page.getByLabel("비밀번호").fill(password)
await page.getByRole("button", { name: "로그인" }).click()
 
// 파일 B — 같은 로그인 로직이 또 반복됨
await page.getByLabel("이메일").fill(email)
await page.getByLabel("비밀번호").fill(password)
await page.getByRole("button", { name: "로그인" }).click()

Page Object Model(POM)이란

POM은 화면(페이지) 하나를 하나의 클래스(객체)로 감싸서, 그 화면의 로케이터와 동작을 한 곳에 모아두는 설계 패턴이다.

class LoginPage {
  constructor(page) {
    this.page = page
  }
 
  async login(email, password) {
    await this.page.getByLabel("이메일").fill(email)
    await this.page.getByLabel("비밀번호").fill(password)
    await this.page.getByRole("button", { name: "로그인" }).click()
  }
}

이제 테스트 코드는 로케이터를 직접 다루지 않고, 이 객체의 login() 메서드를 호출하기만 하면 된다.

test("로그인에 성공하면 대시보드로 이동한다", async ({ page }) => {
  const loginPage = new LoginPage(page)
  await loginPage.login("user@example.com", "password123")
  await expect(page).toHaveURL(/.*dashboard/)
})

무엇이 좋아지는가

로그인 화면 구조가 바뀌면, LoginPage 클래스 하나만 수정하면 된다. 이 클래스를 쓰는 테스트 파일이 100개여도, 로케이터를 일일이 찾아 고칠 필요가 없다.

POM 없이: 화면 변경 → 관련된 모든 테스트 파일을 찾아 수정
POM 사용: 화면 변경 → Page Object 클래스 하나만 수정

이건 모듈 9(레슨 9)에서 배운 "테스트 용이성이 높은 코드" 원칙을 테스트 코드 자체에 적용한 것이다 — 변경의 영향 범위를 한 곳으로 좁히는 설계다.

화면의 동작(행동)을 표현한다

POM에서 중요한 건 로케이터를 감추는 것뿐 아니라, 화면에서 할 수 있는 행동을 의미 있는 이름의 메서드로 표현하는 것이다.

class CartPage {
  async addCoupon(code) { /* ... */ }
  async removeItem(itemName) { /* ... */ }
  async getTotalPrice() { /* ... */ }
}

테스트 코드에서 cartPage.addCoupon("SAVE10")처럼 읽으면, 로케이터나 클릭 순서 같은 세부사항 없이도 무엇을 하는 테스트인지 바로 이해된다 — 이건 모듈 7(레슨 5)에서 배운 Given-When-Then의 가독성 원칙과 같은 방향이다.

POM의 한계

POM이 모든 문제를 해결하지는 않는다. 화면 구조 자체가 근본적으로 바뀌면 Page Object 클래스도 다시 짜야 하고, 화면 간 공통 요소 (헤더, 네비게이션)를 어떻게 재사용할지 등 여전히 설계 판단이 필요하다. POM은 "어디를 고쳐야 하는지 명확하게 만드는" 구조적 도움을 줄 뿐, 완벽한 안전망은 아니다.

실무에서 왜 필요한가

"자동화 테스트가 100개 있는데, 로그인 화면 하나 바뀌었다고 50개가 동시에 실패한다"는 상황에서, 그 수정이 파일 50개를 일일이 고치는 일인지 클래스 하나만 고치는 일인지는 팀의 생산성에 엄청난 차이를 만든다. POM 구조를 갖춘 자동화 스위트는 모듈 11(레슨 4)에서 배운 자동화 부채를 훨씬 적게 만든다.

실습 과제

과제 1 — Page Object 설계하기 (15분)

"회원가입 화면"(이메일, 비밀번호, 비밀번호 확인 입력 후 가입 버튼)에 대한 Page Object 클래스를 이 레슨의 LoginPage 예시를 참고해 설계한다. 최소 하나의 행동 메서드(signUp 등)를 포함한다.

과제 2 — 중복 제거 효과 설명하기 (10분)

"장바구니 담기" 동작이 상품 상세 페이지, 검색 결과 페이지, 추천 상품 위젯 세 곳에서 각각 다른 테스트 코드로 중복 작성되어 있다고 하자. 이걸 POM으로 리팩터링하면 어떤 이득이 생기는지 설명한다.

자가 체크리스트

  • 로케이터가 테스트 코드에 흩어져 있을 때 생기는 문제를 설명할 수 있다
  • Page Object Model이 이 문제를 어떻게 해결하는지 설명할 수 있다
  • Page Object의 메서드를 의미 있는 행동 단위로 설계할 수 있다
  • POM이 모든 유지보수 문제를 해결하지 않는다는 것을 이해한다

흔한 실수

  • Page Object 클래스에 로케이터만 모아두고, 의미 있는 행동 메서드로 감싸지 않는다. 그러면 테스트 코드에서 여전히 세부 동작 순서를 다 알아야 해서, POM의 가독성 이점을 놓친다.
  • 화면 하나에 Page Object 클래스 여러 개를 중구난방으로 만든다. 화면과 클래스를 1:1로 대응시키는 원칙을 지켜야 관리가 쉽다.
  • 공통 요소(헤더, 푸터)를 각 Page Object마다 중복해서 정의한다. 공통 부분은 별도로 분리하거나 상속·조합으로 재사용하는 것이 좋다.

참고 자료