본 글은 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
'IT (프론트엔드)' 카테고리의 다른 글
API call 최적화를 고려한 자동 완성 Input ComboBox 구현 (React, Typescript, Vanilla-extract) (1) | 2025.01.05 |
---|---|
Javascript this 바인딩 (함수 호출 방식과 화살표 함수에 따라 달라지는 this) (0) | 2025.01.02 |
패턴으로 알아보는 전역 상태 라이브러리 (2) | 2024.12.23 |
Github Actions로 CI 구축 & 최적화 (CI 시간 단축, Chromatic 자동 배포, Github Bot으로 배포 주소 남기기) (0) | 2024.12.23 |
Lexical Environment (렉시컬 환경)에 대해 알아보자 (+호이스팅, TDZ) (0) | 2024.12.01 |