
본 글에서는 Cypress가 무엇인지, 왜 Cypress를 선택했는지, 그리고 사용자 인증 플로우 테스트를 어떻게 구성했는지, 중간에 어떤 문제가 있었고 그것을 어떻게 해결했는지를 중심으로 정리해보았다.
Cypress는 JavaScript 기반의 엔드투엔드(E2E) 테스트 프레임워크이다. 실제 브라우저 환경에서 사용자의 행동을 시뮬레이션하며, DOM 요소를 선택하고, 클릭하거나 입력하는 등 다양한 UI 동작을 검증할 수 있다.
Cypress는 다음과 같은 장점이 있다
- 실시간 리로딩: 테스트를 작성하거나 수정할 때마다 자동으로 테스트가 실행된다.
- 실제 브라우저에서 테스트 수행: 브라우저 기반으로 테스트가 진행되어 실제 사용자 경험과 유사하다.
- 강력한 디버깅 도구: Chrome DevTools와 통합되어 에러 추적이 용이하다.
- 비동기 제어 간결함:
.then(),await없이 Cypress 명령어들이 체이닝되고 실행 순서가 보장된다.
현재 프로젝트는 사용자 중심의 콘텐츠 플랫폼으로, 로그인, 회원가입, 플레이리스트 생성 등 사용자 인터랙션이 많다. 이처럼 상태 변화가 중요한 기능을 테스트하려면 실제 브라우저 환경에서의 시뮬레이션이 필요하다.
Jest와 같은 유닛 테스트 도구는 주로 로직 테스트에 적합하지만, Cypress는 전체 흐름을 UI 단에서 테스트할 수 있어서 도입하게 되었다.
실제 사용자 플로우에서는 로그인 → 접근 → 로그아웃 등의 흐름이 하나의 사이클로 구성된다. 이를 테스트 코드에서도 반영하면 다음과 같은 이점이 있다
1. 상태 관리 일관성: 로그인 상태를 유지하며 여러 페이지를 순회할 수 있다.
2. 의존성 감소: 각 테스트가 독립적이지 않고, 흐름 자체를 하나의 시나리오로 테스트함으로써 실제 유저 플로우를 반영할 수 있다.
3. 반복 작업 방지: 로그인 로직을 매 테스트마다 반복할 필요 없이, 세션 단위로 묶어서 재사용할 수 있다.
초기에는 cy.session()과 cy.window().localStorage.setItem()을 활용하여 Supabase 로그인 후 받은 토큰을 직접 localStorage에 저장하는 방식으로 인증 상태를 유지했다. 하지만 localStorage 방식은 Cypress의 세션 캐시와는 별개로 작동하여 테스트 간에 상태 일관성을 보장하기 어렵고, 테스트 성능도 떨어졌다.
이후에는 Supabase의 로그인 API에 직접 요청을 보내고, 응답받은 토큰을 Cypress 내부에서 처리한 후 localStorage에 저장하는 방식으로 전환하였다. 즉, 사용자 UI를 경유하지 않고 직접 로그인 API를 호출하여 인증을 처리한 뒤, 이후 테스트가 인증된 상태로 동작하도록 구성했다.
const loginWithoutUI = () => {
const email = '*비밀*';
const password = '*비밀*';
return cy.request({
method: 'POST',
url: `${Cypress.env("SUPABASE_URL")}/auth/v1/token?grant_type=password`,
body: { email, password },
headers: {
apikey: Cypress.env("SUPABASE_ANON_KEY"),
'Content-Type': 'application/json',
},
}).then((res) => {
const { access_token, refresh_token } = res.body;
cy.window().then((win) => {
const authData = {
access_token,
refresh_token,
expires_at: Math.floor(Date.now() / 1000) + 3600,
expires_in: 3600,
token_type: 'bearer',
user: res.body.user,
};
win.localStorage.setItem(
Cypress.env("SUPABASE_AUTH_TOKEN_KEY"),
JSON.stringify(authData)
);
});
});
};
이 방식을 통해 로그인 UI를 거치지 않고도 상태 유지가 가능한 테스트 흐름을 구성할 수 있었다.
beforeEach(() => {
cy.session('logged-in', loginWithoutUI);
cy.visit("/");
cy.get('[data-testid="nav-생성"]').click();
cy.url().should("include", "/create");
});
초기에는 cy.request()를 통해 로그인 요청을 보내고 localStorage에 토큰을 저장했지만, 테스트가 시작될 때마다 다시 로그인 과정을 거쳐야 했기 때문에 속도가 느리고 비효율적이었다.
그래서 Cypress의 `cy.session()을 사용하기로 했다. 하지만 처음에는 cy.session()을 쓰더라도 테스트 시작 시점의 URL이 /로 초기화되면서 /create 진입 직후 lock()이 실행되기 전에 nav를 눌러버리는 문제가 있었다. 이 때문에 url.should("include", "/create") 검사에서 실패가 자주 발생했다.
이를 해결하기 위해 다음과 같은 조치를 취했다
1. 잠금 상태가 적용될 때까지 특정 DOM 요소 존재 여부로 대기
cy.get("#playlistTitle").should("exist");
이렇게 하면 Create 페이지가 완전히 렌더링되고 useEffect(() => lock(), [])가 실행된 이후로 테스트가 안정화되었다.
2. 테스트 시작 시 정확한 URL로 진입 후 URL 확인
cy.visit("/");
cy.get('[data-testid="nav-생성"]').click();
cy.url().should("include", "/create");
이러한 흐름을 유지하면 currentUrl 변수에 정확하게 /create가 담기고 이후 비교 시에도 문제가 없다.
Cypress를 직접 써보며 얻은 가장 큰 교훈은 "눈으로 보이는 모든 사용자 흐름을 자동화할 수 있다"는 점이다. 특히 사용자 인증은 테스트 환경에서 항상 까다로운 영역이었지만, Supabase API를 직접 호출하고, 세션과 localStorage를 적절히 활용하면 충분히 깔끔하게 구성할 수 있었다.
앞으로도 페이지 보호, 권한 분기, 에러 처리 등 사용자 흐름 테스트를 더욱 정교하게 구성해 나가며 Cypress를 더 잘 활용해보고 싶다.