"Life is Full of Possibilities" - Soul, 2020

IT (프론트엔드)

[미해결] Storybook은 정말 프로젝트 코드와 독립된 환경인 걸까? / Error: "createRoot(...): Target container is not a DOM element."

m2ndy 2024. 12. 31. 02:10
본 글은 2024.12.31에 마지막으로 수정되었습니다.

 

 

Storybook 버전 : 8.4.5

Typescript 버전 : 5.6.2

Vite 버전 : 5.4.10

 

 

<메리 트리스마스> 프로젝트에서 FeedItem 컴포넌트의 스토리북을 구현하던 중 이런 에러를 마주했다.

createRoot(...): Target container is not a DOM element.

 

 

preview.ts와 FeedItem 스토리북의 코드는 아래와 같았다.

// .storybook/preview.ts

import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import type { Decorator, Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { worker } from '../src/mocks/browser';
import '../src/styles/global.css';
import '../src/styles/reset.css';

// Start the MSW worker
worker.start();

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
};

const queryClient = new QueryClient();

export const decorators: Decorator[] = [
  (Story, context) => {
    const initialEntry = context.args.initialEntry || '/';
    return (
      <QueryClientProvider client={queryClient}>
        <MemoryRouter initialEntries={[initialEntry]}>
          <Story />
        </MemoryRouter>
      </QueryClientProvider>
    );
  },
];

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

 

// FeedItem.stories.tsx

import type { Meta, StoryObj } from '@storybook/react';
import FeedItem from './FeedItem';

const meta = {
  title: 'Feed/FeedItem',
  component: FeedItem,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
} satisfies Meta<typeof FeedItem>;

export default meta;
type Story = StoryObj<typeof FeedItem>;

const data = {
  id: 1,
  nickname: '사용자 이름',
  updatedAt: '2024-12-01',
  imageUrl: 'https://via.placeholder.com/150',
  likeCount: 42,
  content: '이것은 피드 아이템의 내용입니다.',
  treeImageCode: 'TREE_01',
};

export const Default: Story = {
  args: { feed: data },
};

 

에러가 발생한 main.tsx는 아래와 같고

const container = document.getElementById('root');

createRoot(container!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={true} />
      <LayoutVisibilityProvider>
        <App />
      </LayoutVisibilityProvider>
    </QueryClientProvider>
  </StrictMode>,
);

 

반면 에러가 발생하지 않은 main.tsx는 container 조건을 추가한 코드다.

const container = document.getElementById('root');

if (container) {
  createRoot(container).render(
    <StrictMode>
      <QueryClientProvider client={queryClient}>
        <ReactQueryDevtools initialIsOpen={true} />
        <LayoutVisibilityProvider>
          <App />
        </LayoutVisibilityProvider>
      </QueryClientProvider>
    </StrictMode>,
  );
}

 

 

 

 

그리고 에러가 발생하는 main.tsx 코드 상태에서 FeedItem의 코드를 하나하나 주석처리하며 실행해보았을 때, React Query 호출 부분을 제거하면 스토리북이 정상적으로 표시되었다.

 

// FeedItem.tsx

const FeedItem = ({ feed }: FeedItemProps) => {
  const { id, nickname, updatedAt, imageUrl, likeCount, content, treeImageCode } = feed;
  const { addLikeFeedMutation, deleteLikeFeedMutation } = useFeedMutation(); // React Query 호출

  // const [searchParams] = useSearchParams();
  // const treeId = Number(searchParams.get('treeId'));
  const treeId = 1; // 디버깅을 위한 임시 값입니다.

  const likedFeedList = JSON.parse(localStorage.getItem('liked_feeds') ?? '{}');
  const isSelected = likedFeedList[treeId] ? likedFeedList[treeId].includes(id) : false;
  
  const handleLiked = () => {
    if (isSelected) {
      deleteLikeFeedMutation({ feedId: id });
    } else {
      addLikeFeedMutation({ feedId: id });
    }
    manageLikedFeeds(treeId, id);
  };

  return (
    <div className={S.Layout}>
      <div className={S.Header}>
        <div className={S.NicknameBox}>
          <img src={treeImageCode} />
          <p className={S.BodyText}>{nickname}</p>
        </div>
        <p className={S.UpdatedAtText}>{formatDateTime(updatedAt)}</p>
      </div>
      <img src={imageUrl} draggable={false} className={S.Image} alt="feed" />
      <HeartWithCount isSelected={isSelected} count={likeCount} onClickIcon={handleLiked} />
      <p className={S.BodyText}>{content}</p>
    </div>
  );
};

 

 

 

 

 

스토리북 설정에서 이미 worker.start()로 msw를 시작하고 있지만

아래처럼 명시적으로 main.tsx에서 msw를 시작하는 경우에도 스토리북이 정상적으로 동작했다.

 

async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') return;

  const { worker } = await import('./mocks/browser');

  return worker.start();
}

enableMocking().then(() => {
  createRoot(document.getElementById('root')!).render(
    <StrictMode>
      <QueryClientProvider client={queryClient}>
        <ReactQueryDevtools initialIsOpen={true} />
        <LayoutVisibilityProvider>
          <App />
        </LayoutVisibilityProvider>
      </QueryClientProvider>
    </StrictMode>,
  );
});

 

스토리북은 리액트 프로젝트와 독립적인 환경에서 구동되는데, 왜 main.tsx 코드를 변경했을 때 스토리북 동작이 변경되는 걸까?

 

 

 

예상으로는 MSW가 초기화 되기 전 스토리북이 실행되어 스토리북에 설정해놓은 MSW worker는 실행되지 않고, main.tsx로 거슬러 올라가 main.tsx 코드를 실행하는 건데, 이때 스토리북은 root id가 "storybook-root"로 되어있어 document.getElementById('root')를 발견하지 못한다. !는 non-null assertion operator로, null이 아님을 단언하는 연산자인데 이 연산자 때문에 createRoot()에 주입되는 값이 없어서 에러가 발생한다고 추측하고 있다.

 

 

그렇다면 스토리북은 프로젝트 코드와 독립된 환경이 아닌걸까?!

 

궁금하기도 하고 아무리 찾아도 답이 안나와 Storybook 커뮤니티에 글을 작성해놓은 상태다. 누가 답변 좀 해주세요...🥲

 

 

 

https://github.com/storybookjs/storybook/discussions/30157

 

DOM-related Error with React Query and MSW in Storybook · storybookjs storybook · Discussion #30157

Summary I’m encountering the following error when using React Query with MSW (Mock Service Worker) in Storybook: createRoot(...): Target container is not a DOM element. The error occurs depending o...

github.com