들어가며
2025년 팀 스터디로 장비 관리용 웹 사이트를 만들어, 그 위에 테스트를 붙여보자는 방향으로 진행되었는데 상반기에 개인 사정으로 자리를 비우게 되어, 팀에서 진행한 테스트 코드 학습에 참여하지 못하여 뒤늦게 Playwright에 대해 알게 되었습니다.
이 글은 Playwright를 처음 사용해 보며 느낀 점과,
실제로 어떻게 동작하는지에 대해 정리한 기록입니다.
Playwright
Playwright는 웹 애플리케이션을 실제 사용자처럼 테스트할 수 있는 E2E(End-to-End) 테스트 도구입니다. 브라우저를 직접 실행해서 클릭, 입력, 페이지 이동 같은 동작을 자동화하고, 그 결과를 검증합니다.
“사용자가 이 화면에서 이 버튼을 누르면, 다음 화면이 제대로 나오는가”를 확인하는 데 초점이 맞춰져 있어 직관적이고 테스트 코드 자체가 선언적이여서 정확히 몰라도 이해하기 쉬웠고, 우리가 직접 서비스를 만들다보니 컴포넌트 단위의 테스트가 아니라 결국은 전체적으로 흐름을 파악하면서 QA를 진행하게 되다보니 프로젝트 환경에서 선택하게 되지 않았나 생각이 들었습니다.
playwright 실행하기
1. 설치하기
npm install -D @playwright/test
2. 테스트 설정하기(playwright.config.ts)
export default defineConfig({
// 1) 디렉토리 설정 및 테스트 시간 설정
testDir: “./tests”,
timeout: 30 * 1000 각 테스트 최대 시간(30초)
expect: { timeout : 5000} // 기대값 최대 시간 5초
// 2) 재시도 및 병렬 실행
retries: process.env.CI ? 2 : 0 // CI에서는 실패 시 2번 재시도
workers: process.env.CI ? 1 : undefined // CI에서는 1개만, 로컬에서는 자동
// 3)프로젝트 setup 설정
project: {
name: "setup",
testMatch: /global\.setup\.ts/, // global.setup.ts 파일만 실행
timeout: 6 * 60 * 1000, // 6분 타임아웃 (수동 로그인 고려)
use: {
headless: false, // 브라우저를 보이게 실행
...devices["Desktop Chrome"], // 데스크탑 Chrome 환경 시뮬레이션
},
}
// 4.크로니움 설정, e2e-tests
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: "tests/.auth/admin-auth.json",
// 저장된 인증 상태를 사용하여 모든 테스트가 로그인된 상태로 시작
},
dependencies: ["setup"],
{
name: "e2e-tests",
testMatch: /.*\.spec\.ts/, // *.spec.ts 파일만 실행
use: {
storageState: "tests/.auth/admin-auth.json", // 저장된 인증 상태 사용
},
dependencies: ["setup"], // setup이(로그인) 완료된 후 실행
}
3. 실행을 위한 script 작성하기
브라우저를 한번은 열어서 로그인을 진행하고 인증 파일을 생성한 후 테스트를 진행해야하기 때문에 e2e:setup,test:e2e를 순차적으로 진행해야하므로 각각 작성해야한 후 실행해주면 됩니다.
// package.json
{
"scripts": {
"test:e2e:setup": "playwright test --project=setup",
"test:e2e": "playwright test"
}
}
장비 관리 시스템 사이트에서 테스트하기
1. 시나리오 작성하기
1-1) 로그인- 대시보드 페이지 (localhost:3000/) 에 접근- 로그인 되어있는 지 상태 체크해서 리디렉션되었는지 체크하기- 로그인 버튼이 로드 되어있는 지 체크하기- 로그인이 완료되면 대시보드(/) 페이지로 리디렉션 하기
1-2) 대시보드- 로그인한 사용자는 대시보드에 장비현황과 장비 목록 리스트 노출- 비로그인 사용자는 로그인페이지로 리디렉션- 로그인한 관리자는 사용자가 보는 뷰에 관리자 네비게이션이 노출2. 테스트 코드 작성하기
문서나 다양한 예제에서는 로그인 테스트의 경우는 fill 함수를 이용해 특정 아이디, 비밀번호를 입력해서 로그인 절차를 진행하지만, 장비 관리 시스템 사이트 구글 oauth 로그인 방식을 이용하기 때문에 브라우저를 직접 열어서 로그인하도록 진행하게 되었습니다. 테스트를 하다보니, 브라우저로 로그인 할 때 이메일 입력 후 비밀번호 포커스만 가도 브라우저가 자꾸 닫히게 되어서 context도 추가해주었습니다.
const adminAuthFile = "tests/.auth/admin-auth.json";
setup("Setup: 관리자 로그인", async ({ page }) => {
// 1. 대시보드 페이지로 이동
await page.goto("/");
// 2. 미들웨어에 의해 로그인 페이지로 리디렉션되었는지 확인
await expect(page).toHaveURL(/\/login/);
// 로그인 페이지가 로드되었는지 확인 (heading "XE 장비 관리 시스템" 또는 로그인 버튼)
const loginButton = page.getByRole("button", { name: /google/i });
await expect(loginButton).toBeVisible({ timeout: 10000 });
// 3. 로그인 시 콜백 핸들러가 쿠키를 읽어 대시보드 페이지(/)로 리디렉션할 때까지 브라우저가 닫히지 않도록 context 이용
const context = page.context();
try {
await page.waitForURL(
(url) => {
try {
const urlString = typeof url === "string" ? url : url.toString();
// localhost:3000 도메인이 아니면 false (Google OAuth 등)
if (!urlString.includes("localhost:3000")) {
return false;
}
const urlObj = new URL(urlString);
const pathname = urlObj.pathname;
return pathname === "/" || pathname === "/dashboard";
} catch {
// URL 파싱 실패 시 false (다른 도메인에 있을 수 있음)
return false;
}
},
{ timeout: 300000 }, // 5분으로 증가
);
} catch {
// 타임아웃 발생 시 페이지 상태 확인
const pages = context.pages();
if (pages.length === 0 || pages[0].isClosed()) {
throw new Error("로그인 중 브라우저가 닫힘.");
}
// 페이지가 살아있으면 현재 URL 확인
const currentPage = pages[0];
const currentUrl = currentPage.url();
if (currentUrl.includes("localhost:3000")) {
const urlObj = new URL(currentUrl);
const pathname = urlObj.pathname;
if (pathname === "/" || pathname === "/dashboard") {
console.log("로그인 완료 (URL 확인됨):", currentUrl);
await currentPage.bringToFront();
} else {
throw new Error(
`로그인 타임아웃: 5분 내에 로그인이 완료 되지 않음. 현재 URL: ${currentUrl}`,
);
}
} else {
throw new Error(
`로그인 타임아웃: 현재 Google OAuth 페이지에 있음... 현재 URL: ${currentUrl}`,
);
}
}
// 4. 최종적으로 대시보드 페이지에 도달했는지 확인
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible();
// 5. 현재 페이지의 인증 상태를 파일에 저장
await page.context().storageState({ path: adminAuthFile });
});
import { expect, test } from "@playwright/test";
test.describe("대시보드 페이지", () => {
// 1. 비로그인
test("비로그인 사용자 - 로그인 페이지로 리다이렉트", async ({ page }) => {
// storageState 없이 테스트 (비로그인 상태)
await page.goto("/");
// 로그인 페이지로 리다이렉트되었는지 확인
await expect(page).toHaveURL(/\/login/);
await expect(page.getByRole("heading", { name: /로그인/i })).toBeVisible();
});
// 2. 일반 사용자(관리자 X)
test("일반 사용자 - 대시보드 화면 정상 노출 (관리자 메뉴 미노출)", async ({
page,
}) => {
// 인증된 상태로 대시보드 접속
await page.goto("/");
// 대시보드 제목 확인
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible({
timeout: 10000,
});
// 대시보드 설명 확인
await expect(
page.getByText("전체 장비 현황 및 사용자별 장비 할당 정보"),
).toBeVisible();
// 일반 사용자 메뉴는 표시되어야 함
await expect(page.getByRole("link", { name: "대시보드" })).toBeVisible();
await expect(page.getByRole("link", { name: "장비 목록" })).toBeVisible();
// 관리자 메뉴는 표시되지 않아야 함
// Header에서 관리자 여부를 확인하는 API 호출이 완료될 때까지 대기
await page.waitForTimeout(2000);
const adminMenus = ["사용자 관리", "장비 유형 관리", "장비 할당"];
// 네비게이션 내에서 관리자 메뉴가 없는지 확인
const navigation = page.locator("nav");
for (const menuLabel of adminMenus) {
await expect(
navigation.getByRole("link", { name: menuLabel }),
).not.toBeVisible();
}
});
// 3. 관리자
test("관리자 - 대시보드 화면 및 관리자 메뉴 노출", async ({ page }) => {
await page.goto("/");
// 대시보드 제목 확인
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible({
timeout: 10000,
});
// 대시보드 설명 확인
await expect(
page.getByText("전체 장비 현황 및 사용자별 장비 할당 정보"),
).toBeVisible();
// 일반 사용자 메뉴 확인
await expect(page.getByRole("link", { name: "대시보드" })).toBeVisible();
await expect(page.getByRole("link", { name: "장비 목록" })).toBeVisible();
// 관리자 메뉴 확인
// Header에서 관리자 여부를 확인하는 API 호출이 완료될 때까지 대기
await page.waitForTimeout(2000);
const adminMenus = ["사용자 관리", "장비 유형 관리", "장비 할당"];
// 네비게이션 내에서 관리자 메뉴가 표시되는지 확인
const navigation = page.locator("nav");
for (const menuLabel of adminMenus) {
await expect(
navigation.getByRole("link", { name: menuLabel }),
).toBeVisible();
}
});
});
Playwright를 사용하며 좋았던 점
짧게나마 사용해보며 느낀 Playwright의 장점은 다음과 같습니다.
1. 코드가 직관적이라 쓰는데 어렵지 않고 초기 진입 장벽이 아주 낮음
2. 실제 사용자 시나리오를 그대로 검증할 수 있음
장비 현황 사이트는 목록 조회, 필터, 상세페이지 이동과 같이 사용자 흐름 중심의 기능이 많은데 마우스로 직접 누른것 처럼 실제 사용 방식 그대로 테스트를 진행할 수 있습니다.
3. 화면 상태 변화 검증에 적합
장비 관리 사이트에서는 대여 중 / 사용 가능 / 수리 중 혹은 관리자, 사용자 같은 상태 표시를 뱃지의 텍스트, 컬러를 변경한 UI 상태 변화를 검증하기에 쉽습니다.
그럼에도 바로 적용하지 못했던 이유
1. UI 변경이 잦은 서비스의 경우 초기에 붙이기 힘듦
한번에 잘 짜여진 설계를 기반으로 개발하는 게 아닌, 요구사항으로 변경이 되는 서비스다보니 테스트 코드를 변경하면서 작성했다면 오히려 테스트 코드를 유지·수정하는 부담이 더 커졌을 것이라는 생각이 들었습니다.
2. 데이터 준비가 까다로움
데이터의 편집이 워낙 많이 필요한 프로젝트라 구현하는 단계에서는 더더욱 쉽지 않을 거라는 생각이 들어, 처음부터 만들기는 힘들고 기능이 다 붙이고 나서 마지막에 확인을 위해 붙이는게 맞았다는 생각이 들었습니다.
마치며
케이스에 따라 데이터가 자주 바뀌는 프로젝트 특성상, 처음부터 테스트를 함께 만들어가는 건 생각보다 쉽지 않았고 그래서 이번 프로젝트에서는 기능을 모두 구현한 뒤, 마지막으로 한 번 더 확인하는 용도로 쓰게 되었던 것 같습니다.
이제 서비스를 오픈하고 잠시 숨을 고른 만큼, 팀원들과 함께 테스트를 돌려보며 이 프로젝트를 다시 돌아볼 수 있으면 좋겠습니다.
읽어주셔서 감사합니다 :)

