
카카오 로그인 구현중 문제가 발생했다.
카카오 SDK를 사용하여 주소로 직접 요청하지 않아도 자동으로 카카오 OAuth 인증 페이지로 리다이렉트되도록 설정했고, 로그인 버튼 클릭 시 카카오 로그인이 동작하도록 기능을 구현했다.
또한 로그인하지 않았거나 로그아웃 상태일 때만 로그인 모달이 표시되도록 조건을 설정했다.
로그인/로그아웃 기능을 개발한 뒤 확인해보니, 백엔드에서 제공한 /me 경로가 쿠키가 없는 요청에도 항상 200 응답을 반환하고 있었다.

요청(request)을 보면 쿠키가 포함되어 있지 않음에도 불구하고 응답이 200으로 반환되고 있었다.
즉, 실제 로그인 상태와 관계없이 항상 로그인된 사용자로 처리되고 있었다.
이 상황에서 프론트 코드 문제인지 백엔드 문제인지 바로 판단하기 어려웠다.
백엔드 측에서 문제가 해결되었는지 확인할 수 있는 방법을 만들어 달라고 요청하여 나는 프론트 로직이 정상 동작하는지 확인할 수 있도록 테스트 코드를 작성하여 검증 방법을 제공하기로 했다.
나는 Jest와 React Testing Library로 프론트 코드를 테스트했다.
Jest는 Meta(Facebook)에서 개발한 JavaScript 테스트 프레임워크로, 프론트엔드 테스트에서 널리 사용되는 도구이다. 주로 함수 로직과 같은 단위(Unit) 테스트를 작성할 때 사용한다.
React Testing Library는 UI 컴포넌트를 테스트하기 위한 라이브러리로, 함수 로직뿐만 아니라 사용자의 실제 동작(버튼 클릭, 화면 렌더링 등)을 기준으로 컴포넌트 동작을 검증할 수 있도록 도와준다.

이번 테스트에서는 유닛 테스트와 컴포넌트 테스트를 사용했다.
순수 로직 단위(로그인 상태 판단, axios 설정, /me 응답처리 로직 등)는 유닛 테스트로 작성했고, DOM과 직접적으로 연결되는 부분(모달 표시 여부, 버튼 클릭, /me 응답에 따른 화면 변화 등)은 컴포넌트 테스트로 작성하였다.
통합 테스트나 E2E 테스트도 존재하지만, 이번 테스트의 목적은 프론트 로직이 정상 동작하는지 검증하는 것이었기 때문에 위 방식으로 충분하다고 판단했다.
테스트에서는 매처를 사용해 결과를 검증했다. 매처는 expect() 뒤에 붙어 기대값을 검증하는 함수이다.
나는 매처를 사용하여 다음과 같은 부분을 확인하였다.

프론트에서는 위 결과처럼 모든 테스트가 통과했다.
테스트 결과 프론트 로직은 정상적으로 동작하고 있다는 것을 확인할 수 있었다.
따라서 로그인 상태와 관계없이 항상 성공 응답을 반환하는 문제는 프론트 로직이 아니라 /me API의 쿠키 검증 로직 문제일 가능성이 높다고 판단했다.
이 결과를 바탕으로 백엔드 측에 /me 엔드포인트에서 쿠키를 확인하도록 수정 요청을 했다.
테스트는 성공했지만 앞으로 코드가 복잡해지고 확장성을 고려했을 때 리팩토링이 필요해 보였다.
이전에는 하나의 테스트에서 모달 존재 여부 + 버튼 클릭 + 함수 호출을 모두 검증하고 있었다.
test("로그인 모달 테스트", () => {
render(<LoginModal />);
// 모달 존재 확인
expect(screen.getByText("서비스를 이용하려면 로그인해주세요.")).toBeInTheDocument();
// 카카오 로그인 버튼 클릭
fireEvent.click(screen.getByRole("button", { name: "카카오로 로그인" }));
// 카카오 SDK 호출 확인
expect((window as any).Kakao.Auth.authorize).toHaveBeenCalled();
});
이 경우 테스트가 실패하면 어떤 동작에서 문제가 발생했는지 파악하기 어려웠다.
그래서 각 동작별 테스트로 분리하였다.
test("로그인 모달이 표시되어야 한다", () => {
render(<LoginModal />);
expect(screen.getByText("서비스를 이용하려면 로그인해주세요.")).toBeInTheDocument();
});
test("카카오 로그인 버튼이 표시되어야 한다", () => {
render(<LoginModal />);
expect(screen.getByRole("button", { name: "카카오로 로그인" })).toBeInTheDocument();
});
test("카카오 로그인 버튼 클릭 시 Kakao SDK가 호출된다", () => {
render(<LoginModal />);
fireEvent.click(screen.getByRole("button", { name: "카카오로 로그인" }));
expect((window as any).Kakao.Auth.authorize).toHaveBeenCalled();
});
이렇게 분리하면 테스트 실패 시 어떤 기능이 문제인지 바로 확인할 수 있다.
동일한 입력에 항상 동일한 결과를 반환하도록 순수 함수 형태로 로직을 분리하였다.
예를 들어 /me 응답을 통해 로그인 상태를 판단하는 로직을 분리하였다.
test("user 값이 존재하면 로그인 상태이다", () => {
expect(isLoggedIn({ id: 1 })).toBe(true);
});
test("user 값이 없으면 로그인 상태가 아니다", () => {
expect(isLoggedIn(null)).toBe(false);
});
컴포넌트 테스트에서는 실제 컴포넌트를 렌더링한 뒤 screen을 사용하여 사용자가 실제로 보게 되는 요소 기준으로 검증하였다.
test("로그인 모달이 화면에 표시된다", () => {
render(<LoginModal />);
expect(
screen.getByText("서비스를 이용하려면 로그인해주세요.")
).toBeInTheDocument();
});
test("GitHub 로그인 버튼 클릭 시 OAuth 요청이 발생한다", async () => {
render(<LoginModal />);
const githubButton = screen.getByRole("button", { name: "깃허브로 로그인" });
await userEvent.click(githubButton);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining("/auth/github"),
expect.any(Object)
);
});
});
이렇게 하면 사용자가 실제로 보는 화면 기준의 테스트를 진행할 수 있다.
test("GitHub 로그인 요청 실패 시 에러 메시지가 표시된다", async () => {
global.fetch = jest.fn().mockRejectedValueOnce(new Error("Network error"));
render(<LoginModal />);
fireEvent.click(screen.getByRole("button", { name: "깃허브로 로그인" }));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});
expect(screen.getByText("로그인 요청에 실패했습니다.")).toBeInTheDocument();
});
로그인 요청이 실패했을 경우 사용자에게 어떤 UI가 표시되는지 확인하기 위해 API 호출 실패 상황에 대한 테스트도 작성하였다.
테스트 코드는 코드 개발 외에 추가로 작성 시간이 필요하기 때문에 처음에는 번거롭다고 느껴졌다. 그래서 이전에는 테스트 없이 기능 구현에만 집중하는 경우가 많았다.
하지만 프로젝트를 진행하면서 에러나 버그가 자주 발생했고, 테스트 코드를 통해 문제가 발생했을 때 빠르게 동작을 확인할 수 있다는 점에서 도움이 된다고 느꼈다.
또한 테스트 코드를 작성하면서 기능의 동작을 다시 한 번 정리하게 되었고, 다른 개발자와 협업할 때도 코드의 의도를 더 명확하게 전달할 수 있었다.
뿐만 아니라 테스트 코드를 직관적으로 작성하면 비개발 직군도 어떤 기능이 구현되어 있는지 확인할 수 있기 때문에 협업 과정에서도 큰 도움이 될 것이라 생각했다.