프로젝트를 진행하면서 공용 컴포넌트를 많이 도입했지만, 팀원들이 이를 사용하는 방법을 잘 이해하지 못해 만든 사람에게 질문이 계속 이어졌습니다. 이로 인해 실제 개발에 할애할 시간이 줄어들었습니다.
또한 공용 컴포넌트의 기능이 자주 변경되면서 버그가 자주 발생하는 문제도 겪었습니다.
그래서 스토리북으로 문서화를 진행하여 조금이나마 공통 컴포넌트를 사용하기 쉽게 바꿔보도록 하였습니다.
그 중 제가 모달과 토스트 컴포넌트를 맡아서 진행하기로 했습니다.
일단 먼저 스토리북을 세팅한 후 버튼 UI 컴포넌트를 가져와 버튼을 누르면 모달이 열리게끔 설정을 하려고 했습니다.
근데 여기서 문제가 생겼습니다.
저희 모달 같은 경우에는 React-Potal을 사용했기 때문에 id-modal을 가진 div를 따로 만들어줘야했습니다.
import React, { useEffect } from "react";
import type { Preview } from "@storybook/react";
import "../app/_styles/globals.css";
// 전역 decorator 추가
const WithModalRoot = (Story: React.ComponentType) => {
useEffect(() => {
// 'modal-root'가 존재하지 않으면 생성
if (!document.getElementById("modal")) {
const modalRoot = document.createElement("div");
modalRoot.setAttribute("id", "modal");
document.body.appendChild(modalRoot);
}
}, []);
return <Story />;
};
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [WithModalRoot], // Decorator를 추가하여 모든 스토리에 적용
};
export default preview;
그거에 대한 작업은 preview 파일에 위와 같이 코드를 작성하여 modal이란 아이디를 가진 div를 최상단에 만들어줘 모달이 정상적으로 열리게 해결해주었습니다.
그렇게 로그인, 회원가입, 회원정보 수정 모달을 전부 스토리를 연결하고 스토리 파일이 있는 위치에 .mdx 파일을 만들어줘 문서화를 진행하였습니다.
import { composeStories } from "@storybook/react";
import React from "react";
import { render, screen, userEvent } from "../../util/test-util";
import * as stories from "./Modal.stories";
const { Login } = composeStories(stories);
jest.mock("next/image", () => ({
__esModule: true,
default: (props: any) => {
const { src, alt } = props;
return <img src={src} alt={alt} />;
},
}));
describe("Login Modal 컴포넌트 테스트", () => {
it("버튼을 클릭하면 모달이 열려야 한다", async () => {
// GIVEN
render(<Login />);
// 초기 상태: 모달이 없는지 확인
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
// WHEN
const button = screen.getByRole("button", { name: /로그인/i });
await userEvent.click(button);
// 모달이 열렸는지 확인
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("x 버튼을 클릭하면 모달이 닫혀야 한다", async () => {
// GIVEN
render(<Login />);
// WHEN
const loginButton = screen.getByRole("button", { name: /로그인/i });
await userEvent.click(loginButton);
// 모달 열림 확인
const modal = screen.getByRole("dialog");
expect(modal).toBeInTheDocument();
// x 버튼 클릭
const closeButton = screen.getByRole("button", { name: /닫기/i });
await userEvent.click(closeButton);
// THEN
// 모달이 닫혔는지 확인
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
테스트 목표는 간단하게 정하였습니다
테스트 목표
1. 로그인 버튼을 클릭하면 모달이 열려야 한다.
2. 모달의 x 버튼을 클릭하면 모달이 닫혀야 한다.
이 테스트를 작성하는 도중 모달이 띄워지는걸 어떻게 테스트 도구가 확인하는지 잘 모르는 상태였는데
<Modal role="dialog"></Modal>
이런식으로 role을 이용해서 인식을 할 수 있도록 바꿔주면 되더라구요.
그렇게 목표에 맞게 테스트 코드를 작성해보았습니다.
이번에도 별 다를 거 없이 버튼 UI 컴포넌트를 가져와 버튼을 누르면 토스트 메시지가 보이게 스토리를 작성하여 다른 사람들이 UI를 확인할 수 있게끔 해놓았습니다.
이것도 똑같이 mdx 파일을 만들어 제가 설정해놓은 스토리들을 가져와서 문서화를 시켜주었습니다.
import { composeStories } from "@storybook/react";
import React from "react";
import { render, screen, userEvent, waitFor } from "../../util/test-util";
import * as stories from "./Toast.stories";
const { Success } = composeStories(stories);
describe("Toast 컴포넌트 테스트", () => {
it("Toast 클릭 시 Toast가 잘 뜨는지 확인", async () => {
// GIVEN
render(<Success />);
// WHEN
const button = screen.getByRole("button");
await userEvent.click(button);
//THEN;
await waitFor(() => {
expect(screen.getByText("성공 성공")).toBeInTheDocument();
});
expect(screen.getByText("성공 성공")).toBeInTheDocument();
});
it("Toast 클릭 후 3초 뒤 Toast가 사라지는지 확인", async () => {
// GIVEN
const { container } = render(<Success />);
// WHEN
const button = screen.getByRole("button");
await userEvent.click(button);
const toastContainer = container.querySelector("#toast-container");
// THEN
jest.advanceTimersByTime(3901);
await waitFor(() => {
expect(toastContainer?.hasChildNodes()).toBeFalsy();
});
expect(toastContainer?.hasChildNodes()).toBeFalsy();
});
});
테스트 목표
1. Toast 클릭 시 정상적으로 표시되는지 확인
2. Toast 클릭 후 3초 뒤에 자동으로 사라지는지 확인
버튼을 클릭하여 Toast가 잘 뜨는지 확인하고 3초간 기다렸다가 잘 사라지는지 확인하기 위한 테스트 코드를 작성하여 정상적으로 동작하는지 확인하였습니다.
이번에 간단하게 UI들을 유닛 테스트를 진행해보았습니다. 정말 많이 느낀 것은 예전 멘토님들께서 UI 컴포넌트는 헤드리스 컴포넌트로 작성하는 것이 중요하다고 말씀하셨던 내용이 실감이 나기 시작했습니다.
헤드리스 컴포넌트는 UI의 동작을 논리적인 부분과 시각적인 부분을 분리하여, 실제 화면 구성에 영향을 미치지 않도록 설계된 컴포넌트입니다. 이 접근 방식은 다음과 같은 장점이 있습니다:
테스트 용이성: UI 컴포넌트를 로직과 스타일로 나누어 작성하면, 로직을 독립적으로 테스트할 수 있어 테스트가 쉬워집니다. 예를 들어, 버튼이나 모달 컴포넌트의 동작을 별도로 테스트하고, UI는 나중에 스타일링을 통해 추가할 수 있습니다.
재사용성: 헤드리스 컴포넌트는 시각적인 부분에 의존하지 않으므로, 다른 프로젝트나 페이지에서도 쉽게 재사용할 수 있습니다.
유연성: 시각적인 스타일을 별도로 처리함으로써, 디자인을 변경할 때 컴포넌트의 로직을 변경할 필요 없이 스타일만 수정하면 되기 때문에 더 많은 유연성을 제공합니다.
UI 컴포넌트를 테스트하면서 헤드리스로 구성된 로직을 먼저 작성하고, 그 후에 실제 UI를 붙이는 방식으로 작업을 진행하니 코드의 품질이 높아지고, 테스트 코드도 더욱 명확해진다는 것을 느꼈습니다.