← 블로그 목록

실무 가이드 — TC 작성부터 Jenkins CI 배포까지

2026-07-21
PlaywrightJenkinsCI/CDBDD트러블슈팅

1편에서 단일 제품 자동화를 처음 구축했고, 2편에서 여러 제품으로 확장하는 프레임워크 구조를 다뤘다. 구조 얘기는 충분히 했으니, 이번엔 TC 하나를 실제로 어떻게 코드로 만들고 CI까지 올리는지를 다룬다.

스크립트 작성 방식 두 가지

우리 팀은 두 가지 방식을 혼용한다.

.spec.ts 방식은 가장 빠르다. BDD 흐름은 주석으로 표현한다.

test('동적 탐지 결과 검색', async ({ page }) => {
  /* Given 탐지 기간 설정 */
  await page.goto('incidents/alerts');

  /* When 필터 선택 */
  await page.getByText('특정 탐지 항목', { exact: true }).click();

  /* Then 결과 테이블 검증 */
  await expect(page.getByRole('table')).toContainText('특정 탐지 항목');
});

.feature 방식은 팀원이 비개발자일 때 유용하다. Gherkin 구문으로 시나리오를 작성하면 playwright-bdd가 실행 가능한 코드를 자동 생성한다. 개발자 중심 팀이라면 솔직히 .spec.ts가 훨씬 빠르다.

Codegen 초안을 그대로 쓰면 안 되는 이유

Playwright Codegen으로 클릭하면 초안이 뚝딱 나온다. 근데 그걸 그대로 커밋하면 CI에서 자주 깨진다.

Codegen은 지금 이 순간의 DOM 구조로 셀렉터를 잡는다. UI가 조금만 바뀌면 바로 터진다. 실제로 이런 코드가 생성된 걸 본 적 있다.

// ❌ UI 변경 한 번이면 끝
page.locator('div:nth-child(6) > ul > li:nth-child(2)')
page.getByRole('treeitem', { name: 'caret-down 메뉴명' })

초안에서 이런 셀렉터를 찾아서 안정적인 버전으로 바꿔야 한다.

// ✅ 이걸 써야 UI가 바뀌어도 살아남는다
page.getByRole('button', { name: '저장' })
page.getByText('정책 이름', { exact: true })
page.getByTitle('삭제')

페이지 이동, 검색 버튼 클릭, 저장 후에는 네트워크 대기도 반드시 넣는다. 로컬에서는 넘어가도 CI 서버에서 타임아웃이 나는 경우가 의외로 많다.

await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');

로컬에서 확인하는 방법

# 화면 보면서 실행
npx playwright test tests/4_Detection/search.spec.ts --headed

# BDD Feature 실행
npx bddgen && npx playwright test .features-gen/

# 실패 케이스 Trace 확인
npx playwright show-trace test-results/<실패_폴더>/trace.zip

Trace 뷰어가 생각보다 유용하다. 각 액션 시점의 DOM 스냅샷, 네트워크 요청, 콘솔 로그를 타임라인으로 볼 수 있어서 원격 CI에서 실패한 케이스도 로컬에서 재현할 수 있다.

CI 환경에서 주의할 설정

timeout: 70000,                          // 로컬보다 넉넉하게
retries: process.env.CI ? 2 : 0,        // CI에서만 재시도 2회
forbidOnly: !!process.env.CI,           // test.only 실수 방지
workers: globalThis.workerCount,         // CPU 코어 기반 자동 설정

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

retries: 2는 간헐적인 네트워크 지연을 걸러낸다. 근데 retry로 통과하는 케이스가 많아진다면 그건 TC 자체의 문제다. 신호로 읽어야 한다.

forbidOnlytest.only를 실수로 커밋했을 때 CI 빌드를 즉시 실패시킨다. 한 번 겪어보면 왜 이게 필요한지 바로 납득된다.

반복해서 만나는 실패 패턴

증상 원인 해결
locator.click: Test ended / Timeout 접힌 트리 노드 또는 Codegen 생성 셀렉터 getByText('이름', { exact: true }) 또는 getByTitle()로 교체
ReferenceError: X is not defined 코드 끝 오타 (한글 자음, 백틱 등) 해당 라인 오타 제거
toContainText 원격에서만 실패 로컬↔CI 서버 간 DB 데이터 표기 차이 하드코딩 문구 완화
toBeVisible 원격 타임아웃 CI 서버 네트워크·렌더링 지연 검증 전 waitForLoadState('networkidle') 추가
Headless에서만 SVG/말줄임표 깨짐 Headless 렌더링 차이 .first() 계열로 변경

구조보다 중요한 건 유지보수성이다. 완벽한 TC보다 내일도 고칠 수 있는 TC가 낫다.

다음 편에서는 브라우저 바깥의 영역을 다룬다. 탐지 결과를 확인하려면 먼저 탐지가 일어나야 하는데, 그 탐지를 원격 에이전트에서 직접 일으켜야 하는 상황이 있다.