Lobsters

My experience writing automated tests for a SPA

SPA 자동화 테스트를 만들며 배운 점

React 기반 오디오 플레이어 Réécoute의 전체 사용자 흐름을 Playwright와 실제 브라우저로 검사한 경험을 공유합니다. 테스트 데이터 격리, 외부 서비스 모의 객체, 병렬 실행, hydration으로 생기는 불안정성, CI에서 실패를 진단하는 방법을 설명합니다.

AI 요약

Réécoute는 2~3시간 길이의 녹음을 재생하는 React 기반 SPA입니다. 백엔드와 클라이언트 쪽 기능이 모두 복잡해 두 부분을 따로 검사하기보다 실제 브라우저에서 앱 전체를 실행하는 방식을 택했습니다. 테스트는 Playwright로 작성했고, 약 20개 파일에 파일당 1~4개 사례를 둡니다. 작은 동작을 하나씩 검사하기보다 장바구니에 상품을 담고 결제하는 식의 중요한 사용자 흐름을 긴 테스트로 확인하는 편입니다.

테스트 데이터를 격리하지 않는 방식

각 테스트는 기존 데이터에 기대지 않고 필요한 객체를 직접 만듭니다. 다른 테스트가 만든 데이터는 건드리지 않으며, 테스트 뒤에 데이터를 지우지도 않습니다. 데이터는 계속 쌓입니다. 데이터베이스 트랜잭션을 Playwright 테스트와 함께 쓰기 어렵고, 테스트마다 백엔드 인스턴스를 띄우면 느려지기 때문입니다. 작은 데이터셋만 쓰면 데이터가 많을 때 느려지는 쿼리도 놓칠 수 있습니다. 저자는 공개 데이터가 없는 Réécoute에서는 이 방식이 잘 맞는다고 설명합니다. createUser, createBand, createSession 같은 헬퍼로 테스트 데이터를 만듭니다. before나 after 훅은 쓰지 않습니다.

외부 서비스 모의 객체

S3, Stripe, Twilio 같은 외부 서비스에는 각각 전역 모의 객체를 둡니다. 매 테스트마다 모의를 새로 작성하는 방식 대신 현실적인 모의 객체를 공유해 실행 속도와 안정성을 챙기고, 인터넷 연결 없이도 테스트합니다. 이메일과 패스키처럼 복잡한 일부 사례에는 테스트 전용 매개변수나 HTTP 헤더를 사용합니다. 백엔드는 프로덕션 빌드에서 해당 값을 무시합니다. 헤드리스 Chromium에서 실제 패스키를 쓰기 어려워 패스키 라이브러리 주변에 가짜 클라이언트를 만든 사례도 소개합니다.

속도와 신뢰성

Playwright는 테스트 파일을 기본으로 병렬 실행합니다. Réécoute는 fullyParallel 설정도 켜 파일 안의 테스트까지 동시에 실행합니다. 브라우저 세 개나 네 개에서 모두 돌리는 기본 설정은 Chromium 하나만 쓰도록 바꿨습니다. 저자는 최신 브라우저 간 동작이 비슷해 테스트 시간이 3~4배 줄고 효과는 거의 유지된다고 설명합니다. 테스트 전체는 팬리스 M3 MacBook Air에서 20초 조금 넘게 걸립니다.

브라우저 테스트는 SPA 전체를 실행하므로 완벽한 안정성을 확보하기 어렵습니다. CI에서는 Playwright의 재시도 횟수를 2회로 두고, 실패한 테스트만 최대 두 번 더 실행합니다. 실제로 재시도까지 가는 경우는 드물지만, 저자는 재시도 없이 통과하도록 손볼 여지가 있다고 덧붙입니다.

불안정성의 주요 원인으로 hydration을 꼽습니다. 서버가 먼저 렌더링한 HTML을 보여준 뒤 React가 초기화되는데, 그 전에 사용자가 입력창에 글자를 넣으면 클라이언트 코드가 입력을 반영하지 못할 수 있습니다. 이를 막으려고 useReady 훅에서 컴포넌트가 준비될 때까지 입력창을 비활성화합니다. useEffect에서 1ms 타이머를 실행한 뒤 준비 상태를 켜는 방식입니다. Playwright는 실제 사용자처럼 입력창이 활성화될 때까지 기다린 다음 값을 입력합니다. 모든 폼을 JavaScript 없이도 제출 가능하게 만드는 대안도 있지만, 이 앱은 클라이언트 JavaScript 없이는 주요 기능을 제공하지 않아 구현하지 않았습니다.

CI와 테스트 보조 기능

Playwright는 실패한 테스트의 로그, 네트워크 요청·응답 본문, 스크린샷 등을 trace에 담습니다. HTML 테스트 보고서에는 대화형 실행기와 같은 UI가 들어가며, CI에서 보고서 디렉터리를 S3 호환 스토리지에 올리면 실패 원인을 확인하기 쉽습니다. 저자는 Playwright 전용 Docker 컨테이너를 앱 컨테이너와 분리해 실행합니다. API 전용 테스트도 Playwright의 request()로 작성합니다.

이메일 검사는 테스트 전용 API 경로에서 수신자별 최신 이메일을 가져와 인증 코드와 제목을 확인합니다. 해당 경로는 프로덕션 빌드에서 비활성화합니다. 오디오 플레이어의 선택·클립 생성 기능처럼 마우스 이동이 필요한 동작도 실제 이동과 클릭을 재현해 검사합니다. 중간에 짧은 대기 시간을 두는 방식이지만 저자는 예상보다 안정적으로 작동한다고 설명합니다.

코드 커버리지는 측정하지 않습니다. 대신 중요한 사용자 흐름과 주요 기능의 정상 경로를 확인하고, 드문 코드 경로보다 치명적인 버그를 막는 데 초점을 둡니다.

Lobsters 반응

  • @pzel — 훌륭한 접근입니다. 예전에 일하던 곳에서도 비슷한 방식을 썼습니다. 데이터를 지우지 않고 기존 서버 상태에 영향을 받지 않게 하며, 테스트마다 새 데이터를 만들면 테스트를 대규모로 병렬화하는 데도 도움이 됐습니다. 실제 사용자처럼 앱을 사용하는 테스트를 쓰는 게 제 사랑의 언어입니다 ;) 모의 객체에 대해서도 덧붙이면, 싱글턴 모의 객체가 실제 외부 시스템의 동작 변화를 반영하지 못한다는 불만이 많습니다. 개발자가 알아차리기 전까지는 실제 동작과 어긋날 수 있다는 우려입니다. 실행 시간도 이 접근을 채택할 때 흔히 나오는 반론인데, Playwright 병렬 실행이 도움이 되는 것 같습니다. 링크한 블로그 글에서 이 점을 조금 다뤘습니다. 덧붙임: 더 긴 댓글을 전에 썼는데, 진행 막대도 없이 멈춰 있는 Anubis 페이지에 걸렸습니다. 뒤로 가기를 눌렀더니 Lobsters 메인 페이지로 돌아갔습니다. 뭔가 문제가 있는 것 같습니다.

원문: Reécoute 기술 블로그 / 번역·요약: Trawling