← 블로그 목록

다중 제품으로 확장 — 공통 레포 기반 프레임워크 설계

2026-07-08
Playwright아키텍처CI/CDBDD세션관리

1편에서 단일 제품 자동화를 처음 구축한 과정을 썼다. 이번엔 그 다음에 생긴 문제다.

자동화가 잘 돌아가니까 "우리 제품도 해주실 수 있어요?"가 왔다. 한 팀, 두 팀, 세 팀. 결국 네 개 제품이 됐다.

처음엔 그냥 레포를 복사했다. 빠르니까. 그리고 두 달 뒤에 그 결정을 후회했다.

레포가 4개라는 건 공통 코드도 4벌이라는 뜻이다

playwright.config.ts를 하나 고치면 네 군데 다 반영해야 한다. 당연히 누락이 생긴다. 한 레포에서 고친 게 다른 레포엔 안 들어간 채로 CI가 돌아가고 있었다. 버그를 잡았는데 세 개 레포에선 여전히 같은 버그가 있는 상황.

공통 코드를 별도 common 레포에 모아두고, 변경이 생기면 CI가 각 제품 레포에 자동으로 배포하는 구조로 바꿨다.

+-------------------------------------------+
|           common 레포                      |
|  playwright.config.ts / global-setup.ts   |
|  common.ts / bdd-scripts/                 |
+-------------------------------------------+
                     │
             CI Pipeline (Push 감지)
                     │
     ┌───────────────┼───────────────┐
     ▼               ▼               ▼
+---------+    +---------+    +---------+
| 제품 A  |    | 제품 B  |    | 제품 C  |
+---------+    +---------+    +---------+

규칙은 딱 하나다. 공통 파일은 반드시 common 레포에서만 수정한다. 제품 레포에서 직접 손대면 다음 CI 실행 때 덮어써진다.

/etc/hosts 없이 도메인-IP 매핑하기

멀티 제품 환경에서 제일 귀찮은 게 서버마다 도메인-IP 매핑이었다. 보통 /etc/hosts를 수정하는데, CI 서버에서 권한 문제가 자주 났다. 환경마다 파일을 따로 관리해야 하는 것도 번거롭고.

Chromium에 --host-resolver-rules 옵션이 있다는 걸 알고 나서는 이 문제가 깔끔하게 해결됐다.

// playwright.config.ts
use: {
  launchOptions: {
    args: [
      `--host-resolver-rules=MAP ${globalThis.domain} ${globalThis.ip}`
    ],
  },
}

OS 레벨 DNS 조회 전에 이 규칙이 먼저 적용된다. 권한 없이도 되고, 제품별로 다른 매핑을 코드로 관리할 수 있다.

로그인을 TC마다 하지 않는 이유

제품이 4개가 되니 각 TC에서 로그인 절차를 밟으면 실행 시간이 눈에 띄게 늘어났다. global-setup.ts에서 딱 한 번 로그인하고 세션을 파일로 저장해두면 된다.

projects: [
  {
    name: 'product-a',
    use: {
      baseURL: `https://${globalThis.domain}/product-a/`,
      storageState: globalThis.storageState,       // auth.json
    },
  },
  {
    name: 'product-b',
    use: {
      baseURL: `https://${globalThis.domain}/product-b/`,
      storageState: globalThis.cmStorageState,     // cm-auth.json
    },
  },
]

제품마다 인증 구조가 달라서 storageState 파일을 분리했다. global-setup.ts에서 제품 타입을 감지하고 맞는 로그인 절차를 실행한다.

BDD와 Spec, 굳이 하나만 써야 하나

팀마다 선호가 달랐다. Gherkin 시나리오로 쓰고 싶은 팀도 있고, TypeScript 스펙으로 바로 쓰고 싶은 팀도 있었다. 두 방식을 환경 변수 하나로 전환할 수 있게 했다.

const useBdd = process.env.PLAYWRIGHT_MODE === 'bdd';

const bddProductA = useBdd
  ? defineBddConfig({
      outputDir: '.features-gen/product-a',
      features: 'features/product-a/**/*.feature',
      steps: 'features/product-a/**/steps/*.ts',
    })
  : './tests/product-a';

PLAYWRIGHT_MODE=bdd로 실행하면 .feature 파일 기반, 아무것도 없으면 .spec.ts 기반으로 동작한다. Jenkinsfile에서 환경 변수만 바꿔주면 CI에서도 두 모드를 모두 돌릴 수 있다.

그 외

워커 수 자동 결정: 로컬 개발 머신과 CI 서버의 코어 수가 다르다. 워커를 고정하면 한쪽에서 항상 낭비가 생긴다. setGlobalProductInfo()에서 CPU 코어 수를 읽어 적정 값을 계산하게 했다.

리포팅 파이프라인: Allure는 BDD 시나리오 단위로 시각화해주고, JUnit XML은 Jenkins 빌드 결과로 바로 읽힌다. 실패 케이스엔 스크린샷·영상·Trace가 자동 첨부된다.

reporter: [
  ["line"],
  ["allure-playwright"],
  ["html"],
  ["junit", { outputFile: "test-results/junit.xml" }],
],

screenshot: 'only-on-failure',
video: { mode: 'retain-on-failure' },
trace: 'retain-on-failure',

구조를 잡고 나니 새 제품이 추가돼도 할 일이 단순해졌다. CI 배포 타겟 하나 추가하고, 서버 설정 파일에 접속 정보 추가하고, product.txt에 제품명 한 줄 적으면 된다.

3편에서는 TC를 어떻게 작성하고 Jenkins CI까지 배포하는 실무 흐름을 다룬다.