E2E
문제
Playwright로 프레임 성능이나 자동주행을 검증하는 E2E를 짜야 합니다. 도구는 익숙한데, 결과가 이상하게 나오거나 스크립트가 알 수 없는 이유로 죽는 일이 반복됩니다.
추천
Playwright는 사용자 브라우저 세션을 그대로 쓰는 도구(aside 등)로 못 하는 일에만 꺼냅니다. 녹화, trace, 디바이스 에뮬레이션, 격리 프로필, 그리고 CDP 스위치가 필요한 성능 측정이 여기 해당합니다 (언제 무엇을 쓰는가 참고). 그 안에서도 반복해서 걸리는 함정이 몇 가지 있습니다. 도구가 바뀌어도 아래 판단 기준은 그대로 남습니다.
이유
결과가 지나치게 깔끔하면 측정기를 의심합니다. 서로 다른 대상을 쟀는데 값이 같게 나오면, 차이가 없다고 결론짓기 전에 측정기가 차이를 통과시키지 않는 경우를 먼저 확인합니다. vsync·CPU 스로틀처럼 브라우저가 값을 뭉개는 스위치가 켜져 있으면 코드가 맞아도 값이 똑같이 나옵니다. 무엇을 재든 먼저 대조군으로 측정기 자체가 차이를 통과시키는지 확인합니다.
CPU 비용은 rAF 간격이 아니라 Profiler로 잽니다. rAF 간격은 컴포지터 부하에 좌우되므로
JS 실행 비용의 대리 지표가 되지 않습니다. teardown 이후가 오히려 더 느리게 나오는 현상도
여기서 생깁니다. 프레임 예산은 프레임타임으로, JS 비용 귀속은 Profiler.start/stop의 hitCount로
나눠서 잽니다.
해시 비교와 스크린샷은 다른 실행으로 분리합니다. 검증 도구 자신이 검증 대상을 건드릴 수
있습니다. page.screenshot()은 caret: 'hide' 처리 중 폼 요소에 빈 style=""을 남기고,
그 변경이 DOM 무결성 해시를 바꿉니다. CDP 레벨에서 일어나는 변경이라 MutationObserver로는
잡히지 않습니다. 같은 실행 안에서 해시를 재고 스크린샷도 찍으면 반드시 깨집니다.
rAF로 도는 화면의 자동주행은 한 번의 page.evaluate 안에서 완결합니다. 밖에서 폴링하며
조작하면 dt 클램프 때문에 폴링 사이에 흘려보낸 벽시계 시간이 진행으로 환산되지 않습니다.
많이 폴링해도 게임 내 경과는 훨씬 적게 나옵니다. 밀어 넣을 것은 입력이나 누적 상태여야
합니다. 밖에서 밀어 넣은 파생 상태는 다음 프레임에 지워집니다.
page.evaluate 안에서 화살표 함수를 쓰면 죽습니다. node --import tsx로 돌릴 때 본문에
const f = () =>나 function f(){}가 있으면 ReferenceError: __name is not defined가
납니다. esbuild의 keepNames 헬퍼가 브라우저에는 없기 때문입니다. 인라인 익명 화살표만 쓰거나
new Function(...)으로 소스 문자열을 넘깁니다. 문자열과 두 번째 인자를 같이 쓰는 조합은
오류 없이 undefined를 돌려줍니다. 실패가 드러나지 않아 가장 알아채기 어렵습니다.
예시
측정기 검증은 대조군으로 합니다. 검증 대상을 제거한 상태에서 같은 동작을 두 번 반복해 값이 갈리는지 봅니다.
대조군: 검증 대상 없이 스크린샷만 두 장 찍는다→ 해시가 다르면 범인은 코드가 아니라 검증 도구다자동주행은 이렇게 한 덩어리로 넣습니다.
page.evaluate(() => { // 입력·누적 상태만 밀어 넣고, 루프 전체가 이 안에서 돈다 // 밖에서 매 프레임 개입하지 않는다})하지 말 것
측정값이 똑같이 나온 것을 “차이가 없다”로 결론짓기. 먼저 측정기가 차이를 통과시키는지 대조군으로 확인합니다.
CPU 스로틀을 약하게 걸고 “재현 안 됨”으로 닫기. 약한 배율에서는 나타나지 않던 증상이 훨씬 높은 배율에서야 드러나는 경우가 있습니다. 스로틀은 충분히 세게 겁니다.
evaluate → click → evaluate로 타이밍을 재기. IPC 왕복이 끼어 수백에서 수천 ms 오차가
납니다.
이 함정들이 뒤섞여 나타나는 증상은 측정값을 믿을 수 없을 때와 브라우저 자동화가 어긋날 때에서 다시 다룹니다.