당사(주식회사 Joinclass)는 개발 과정의 많은 부분을 Claude Code와 같은 에이전트에게 맡기고 있습니다. 구현 자체는 몇 분 만에 올라오지만, 제가 '실제로 화면이 그렇게 작동하는지'를 확인하는 작업은 짧아지지 않습니다. 로컬 환경을 띄우고 로그인한 뒤, 여러 화면을 클릭하며 확인합니다. 이것을 하나씩 반복하다 보면 병목 현상은 완전히 사람 측에서 발생하게 됩니다.
그래서 저희가 사용하는 것이 Playwright의 recordVideo 기능을 활용하여 '사용자의 일련의 조작(스토리)'을 각각 하나의 영상으로 만드는 방법입니다. 리뷰하는 사람은 영상을 배속으로 보면서 '의도대로 작동했는지'만 판단하면 됩니다.
원래 이 메커니즘은 자사 서비스(자격 학습 앱 PasPoy)의 소개 영상을 만들기 위해 '실제 화면' 자료를 촬영하기 위해 작성한 스크립트였습니다. 그런데 찍은 영상을 보던 중, '이것을 그대로 인수 확인에 사용할 수 있겠다'는 것을 깨달았습니다. 본 기사에서는 그 스크립트를 기반으로 다음 내용을 작성합니다.
- 장면 단위로 녹화하는 구현 (실제로 작동하는 코드)
- 녹화 파일이 손상되거나 어긋나는 등의 함정들
- 리뷰용으로 재활용할 때의 설계 (인수 기준과 장면을 1대1로 매칭하기)
장면 정의(스토리) → 장면마다 새로운 context + recordVideo
↓
scene.run(page)으로 조작 재생
...
핵심은 1 장면 = 1 BrowserContext = 1 영상으로 만드는 것입니다. 하나의 긴 영상으로 만들 경우, 리뷰하는 사람이 '어디서부터 봐야 할지'를 찾는 시간이 생겨 결국 느려집니다.
당사에서 실제로 작동시키고 있는 스크립트입니다(.company/scripts/paspoy/record-screens.mjs, 인증 정보는 로컬 시드용 더미 데이터).
#!/usr/bin/env node
// 로컬 개발 환경(localhost + 로컬 Supabase)에서 실행합니다.
// 사전에 `npm run dev`와 `npm run seed:local`이 완료되어 있어야 합니다.
...
실행은 다음과 같습니다.
npm i -D @playwright/test
npx playwright install chromium
node record-screens.mjs ./screens
...
recordVideo로 생성된 webm 파일은 context를 닫는 시점에 쓰기가 완료됩니다. close 전에 복사하거나 이름을 변경하면, 0초짜리 영상이나 재생할 수 없는 파일이 됩니다. 위 코드에서는 page.video()를 먼저 가져온 후, close가 되어야 video.path()로 리네임을 합니다. 이 순서는 절대 바꾸지 마십시오.
처음에는 1080x1920으로 바로 찍으려고 하여 size: { width: 1080, height: 1920 }을 지정했습니다. 결과는 540x960의 화면이 왼쪽 상단에 작게 그려지고 나머지는 회색 여백이 되었습니다. size는 '출력 해상도'가 아니라 '캔버스의 크기'입니다. 뷰포트에 맞춰 찍고, 확대는 후속 단계에서 진행합니다.
# 540x960 → 1080x1920으로 확대하여 mp4로 변환 (Slack이나 PR에 붙이기 쉽게 하기)
for f in screens/*.webm; do
ffmpeg -y -i "$f" -vf "scale=1080:1920:flags=lanczos" \
...
try/catch를 장면 단위로 처리하는 이유는 이것 때문입니다. 리뷰 용도에서는 '퀴즈만 실패했다'는 것 자체가 정보가 됩니다. 로그에 quiz: Timeout ...이 남고, 나머지 장면의 영상은 정상적으로 확보됩니다.
클라이언트 측에서 지연되어 그려지는 그래프 등은 networkidle 이후에도 여전히 빈 공간이 있을 수 있습니다. 소개 영상용이라 고정된 sleep을 사용해 보여줄 시간을 만들었지만, 리뷰용으로 사용할 때는 await expect(page.getByRole('heading', { name: '정답률' })).toBeVisible()처럼 '보여야 할 것'을 기다리는 것이 더 안정적이며, 그 자체가 테스트가 됩니다.
찍는 메커니즘은 그대로 두고, 장면을 나누는 방식만 변경합니다. 당사에서는 다음 규칙을 정했습니다.
인수 기준 1개 = 장면 1개. 장면 이름은 AC-2_ranking-top3
기준 ID를 맞추는 방식으로 진행합니다. - 시퀀스(Scene)의 마지막에 expect를 하나 이상 포함시킵니다. 비디오는 '인간이 의도를 확인하는' 역할을, expect는 '기계가 사실을 확인하는' 역할로 분리합니다. 출력 경로는 커밋 해시별로 분리합니다 (screens/<sha>/). 이렇게 하면 수정 전후의 비디오를 나란히 비교할 수 있습니다.
이를 통해 리뷰하는 사람이 해야 할 일은 '실패 로그가 없는지'를 확인하고, 비디오를 2배속으로 재생하여 의도와 벗어나지 않았는지 여부만 판단하는 것이 됩니다. 화면을 직접 조작하는 시간이 사라져, 건당 리뷰 시간이 체감상 절반 이하로 줄었습니다 (엄밀하게 측정한 수치는 아닙니다).
저희 회사의 개발 에이전트 하네스(tetsuwa)에서도 인수 기준, 테스트, 스크린샷을 완료의 '증거'로 필수화하고 있습니다. 정지 이미지로는 전달하기 어려운 '조작 흐름' 확인에 이 녹화를 결합하여 사용합니다.
node record-screens.mjs ./screens를 실행했을 때, 시퀀스 수와 동일한 개수의 webm 파일이 생성되는지 확인합니다.
- 존재하지 않는 URL의 시퀀스를 하나 추가하여, 해당 시퀀스에서만 에러 로그가 발생하고 다른 비디오는 남아있는지 확인합니다.
ffprobe screens/quiz.webm을 사용하여 길이가 0초가 아닌지 확인합니다 (함정 1 확인).
이 방법은 자사 로컬 환경이나 스테이징(Staging)을 본인의 테스트 계정으로 녹화하기 위한 것입니다. 타사의 서비스를 자동 조작하는 용도로 사용할 경우, 이용 약관에 위배될 소지가 있습니다.
AI로 구현이 빨라질수록, 느린 것은 인간의 확인 과정입니다. 경영자의 관점에서 볼 때, 리뷰에서 줄여야 할 시간은 '판단' 자체가 아니라, 판단 이전에 소요되는 '준비' 시간입니다. 스토리 녹화는 그 준비 과정을 기계에 맡기는 가장 저렴한 방법이라고 생각합니다.
AI에게 구현을 맡긴 후, 리뷰(Review), 인수(Acceptance), CI와 같은 개발 프로세스 전체를 어떻게 재구성할지는 제가 쓴 책에서 체계적으로 정리했습니다.