bkdragon's log

Storybook Interactions 본문

React

Storybook Interactions

bkdragon 2024. 11. 16. 20:01

storybook 의 play 함수를 사용하면 스토리가 렌더링 된 이후 사용자 상호작용을 시뮬레이션할 수 있다.

설정

play 함수를 사용하기 위해서는 스토리북 설정 파일에서 interactions 플러그인울 추가해야한다.

npm install @storybook/test @storybook/addon-interactions --save-dev
// .storybook/main.js
module.exports = {
    stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
    addons: ["@storybook/addon-interactions"],
};

방법

사용자 상호작용 시뮬레이션의 전반적인 과정을 먼저 살펴보고 예제 코드로 넘어가자.

  1. 스토리 내에서 상호작용을 할 요소를 찾는다.

    • play 함수의 파라미터 중 canvasElement 스토리에 렌더링되는 DOM 요소이다, within 을 사용해서 canvasElement 안의 영역으로 범위를 지정하고 찾을 수 있다.
  2. 상호작용을 발생시킨다.

    • userEvent, fireEvent 등의 함수를 사용해서 클릭, 입력, 스크롤 등의 상호작용을 시뮬레이션할 수 있다.
  3. 상호작용의 결과를 확인한다.

    • assert , matchers 를 사용해서 상호작용의 결과를 확인할 수 있다.

코드

이제 예제 코드를 살펴보자.
Menu 목록과 각 메뉴를 클릭하는 함수를 인자로 제공받는 MenuList 컴포넌트가 있다. 이 컴포넌트가 렌더링 되고 다른 메뉴를 클릭하는 상호작용을 시뮬레이션 해보자,

import { within, expect, waitFor, fireEvent, fn } from "@storybook/test";

const menus = [
    { name: "Home", path: "/" },
    { name: "Profile", path: "/profile" },
    { name: "Settings", path: "/settings", children: [{ name: "Sub Settings", path: "/sub-settings" }] },
];

export const WithInteractions: Story = {
    name: "상호작용 테스트",
    parameters: {
        docs: {
            description: {
                story: "이 스토리는 메뉴 리스트의 상호작용을 테스트합니다. 'Profile' 메뉴와 'Home' 메뉴를 순서대로 클릭하여 onClickMenu 함수가 호출되는지 확인합니다.",
            },
        },
    },
    args: {
        menus: menus,
        currentPath: "/",
        onClickMenu: fn(),
    },
    play: async ({ args, canvasElement, step }) => {
        const canvas = within(canvasElement);

        await waitFor(() => {
            expect(canvas.getByText("Profile")).toBeInTheDocument();
            expect(canvas.getByText("Settings")).toBeInTheDocument();
        });

        await new Promise((resolve) => setTimeout(resolve, 1000));

        const profileMenu = canvas.getByText("Profile");
        await step("Profile 메뉴 클릭", () => {
            fireEvent.click(profileMenu);
        });

        await new Promise((resolve) => setTimeout(resolve, 1000));
        const settingsMenu = canvas.getByText("Settings");
        await step("Settings 메뉴 클릭", () => {
            fireEvent.click(settingsMenu);
        });
        await waitFor(() => {
            expect(canvas.getByText("Sub Settings")).toBeInTheDocument();
        });

        await new Promise((resolve) => setTimeout(resolve, 1000));
        const subSettingsMenu = canvas.getByText("Sub Settings");
        await step("Sub Settings 메뉴 클릭", () => {
            fireEvent.click(subSettingsMenu);
        });

        expect(args.onClickMenu).toHaveBeenCalledTimes(2);
    },
    render: (args) => {
        const [currentPath, setCurrentPath] = useState(args.currentPath);

        const handleClickMenu: ComponentProps<typeof MenuList>["onClickMenu"] = (menu) => {
            args?.onClickMenu?.(menu); // Mock 함수 호출 추적
            setCurrentPath(menu.path); // 실제 UI 상태 변경
        };

        return <MenuList {...args} currentPath={currentPath} onClickMenu={handleClickMenu} />;
    },
};

스토리가 렌더링 된 이후 play 함수가 실행된다.
waitFor은 특정 동작을 기다리는 함수이다. 여기서는 메뉴 요소가 렌더링 될 때까지 기다린다. (사실 렌더링 다 되고 실행되는거라 필요없기 하다.)
우선 Profile 버튼을 찾아서 클릭한다. Profile 버튼은 chidren 이 없기 때문에, onClickMenu 함수가 호출될 것이다. (children 이 있는 경우 chidren 목록이 나타나는 내부적인 로직이 실행된다.)
그 다음은 Settings 버튼을 찾아서 클릭한다. 이번에는 children 이 있기 때문에, 내부적인 로직이 실행되어 Sub Settings 버튼이 나타난다.
마지막으로 Sub Settings 버튼을 찾아서 클릭한다. Profile과 마찬가지로 onClickMenu 함수가 호출될 것이다.

그럼 결과적으로 onClickMenu 함수가 두 번 호출될 것이다.

정리

상호작용을 직접 시뮬레이션 해볼 수도 있지만, play 함수를 사용하니 자동화할 수 있었다. 스토리북은 정말 막강한 툴인 것 같다.