Stories/Stories for multiple components

김동현·2026년 3월 22일

여러 컴포넌트를 위한 스토리 (Stories for multiple components)

React Vue Angular Web Components More

서로 함께 작동하도록 설계된 컴포넌트들이라면, 두 개 이상의 컴포넌트를 한 번에 렌더링하는 스토리를 작성하는 것이 아주 유용해요. 예를 들어 ButtonGroup, List, Page 컴포넌트 같은 것들이 있겠죠.

서브컴포넌트 (Subcomponents)

문서화하려는 컴포넌트들이 부모-자식 관계일 때는 subcomponents 속성을 사용해서 한꺼번에 문서화할 수 있어요. 특히 자식 컴포넌트가 단독으로 쓰이지 않고 부모 컴포넌트의 일부로만 사용될 때 이 방법이 정말 유용하답니다.

ListListItem 컴포넌트를 예로 들어볼게요:

CSF 3 | CSF Next 🧪

List.stories.ts|tsx

import * as React from 'react';
 
// 여러분이 사용 중인 프레임워크(예: react-vite, nextjs, nextjs-vite 등)로 교체하세요.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
const meta = {
  component: List,
  subcomponents: { ListItem }, //👈 ListItem 컴포넌트를 서브컴포넌트로 추가합니다.
} satisfies Meta<typeof List>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Empty: Story = {};
 
export const OneItem: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem />
    </List>
  ),
};

이렇게 meta(또는 default export)에 subcomponents 속성을 추가하면, ArgTypesControls 테이블에 ListItem의 프롭스(props) 목록을 보여주는 별도의 패널이 생기게 돼요.

하지만 여러분, 서브컴포넌트는 오직 문서화 목적으로만 설계되었다는 점을 명심해야 해요. 그래서 몇 가지 제한 사항이 있답니다:

  • 서브컴포넌트의 argTypes는 (해당 기능을 지원하는 렌더러의 경우) 자동으로 추론되며, 수동으로 정의하거나 덮어쓸 수 없어요.
  • 문서화된 각 서브컴포넌트의 테이블에는 프롭스 값을 변경할 수 있는 컨트롤(controls)이 포함되지 않아요. 왜냐하면 컨트롤은 항상 메인 컴포넌트의 args에만 적용되기 때문이죠.

이런 제약 사항들을 해결할 수 있는 몇 가지 기술들을 알아볼까요? 특히 상황이 복잡해질 때 아주 유용할 거예요.


스토리 정의 재사용하기 (Reusing story definitions)

스토리 정의를 재사용하면 코드 반복을 줄일 수 있어요. 여기서는 ListItem 스토리의 args를 List 스토리에서 재사용하는 방법을 보여드릴게요.

CSF 3 | CSF Next 🧪

List.stories.ts|tsx

import * as React from 'react';
 
// 여러분이 사용 중인 프레임워크로 교체하세요.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 ListItem에서 필요한 스토리들을 임포트합니다.
import { Selected, Unselected } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const ManyItems: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem {...Selected.args} />
      <ListItem {...Unselected.args} />
      <ListItem {...Unselected.args} />
    </List>
  ),
};

Unchecked 스토리를 그 args와 함께 렌더링함으로써, ListItem 스토리에 있던 입력 데이터를 List에서도 그대로 재사용할 수 있게 되었죠?

하지만 이 방식도 아직 args를 사용해서 ListItem 스토리를 직접 제어하는 건 아니에요. 즉, 컨트롤 패널에서 값을 바꿀 수 없고, 더 복잡한 다른 컴포넌트 스토리에서 재사용하기도 어렵다는 뜻이죠.


children을 인자(arg)로 사용하기 (Using children as an arg)

상황을 좀 더 개선할 수 있는 한 가지 방법은 렌더링된 서브컴포넌트를 children 인자(arg)로 끌어내는 거예요.

CSF 3 | CSF Next 🧪

List.stories.ts|tsx

// 여러분이 사용 중인 프레임워크로 교체하세요.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
 
//👇 ListItem을 임포트하는 대신, 스토리 자체를 임포트합니다.
import { Unchecked } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const OneItem: Story = {
  args: {
    children: <Unchecked {...Unchecked.args} />,
  },
};

이제 children이 하나의 arg가 되었으니, 다른 스토리에서도 재사용할 수 있는 가능성이 열렸어요!

다만 이 접근 방식을 쓸 때 주의해야 할 주의사항이 몇 가지 있어요.
children 인자도 다른 모든 인자와 마찬가지로 JSON 직렬화(JSON serializable)가 가능해야 해요. 스토리북에서 에러를 피하려면 다음 수칙을 지켜주세요:

  • 빈 값을 사용하지 마세요.
  • 매핑(mapping)을 사용해서 컨트롤로 값을 조정하고 싶을 때 활용하세요.
  • 서드파티 라이브러리가 포함된 컴포넌트를 다룰 때는 주의가 필요해요.

스토리북 팀에서도 현재 children 인자의 전반적인 사용 경험을 개선하기 위해 노력 중이에요. 조만간 컨트롤 패널에서 직접 children을 수정하거나 다른 타입의 컴포넌트들도 자유롭게 사용할 수 있게 될 거예요. 하지만 지금은 이런 제약 사항들을 고려해서 구현해야 합니다!


템플릿 컴포넌트 만들기 (Creating a Template Component)

좀 더 "데이터 중심적인" 또 다른 옵션은 특별한 "스토리 생성용" 템플릿 컴포넌트를 만드는 거예요.

CSF 3 | CSF Next 🧪

List.stories.ts|tsx

// 여러분이 사용 중인 프레임워크로 교체하세요.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 ListItem 스토리에서 특정 스토리를 임포트합니다.
import { Unchecked } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
//👇 ListTemplate 구조가 기존 스토리들에 스프레드(spread)될 거예요.
const ListTemplate: Story = {
  render: ({ items, ...args }) => {
    return (
      <List>
        {items.map((item) => (
          <ListItem {...item} />
        ))}
      </List>
    );
  },
};
 
export const Empty = {
  ...ListTemplate,
  args: {
    items: [],
  },
};
 
export const OneItem = {
  ...ListTemplate,
  args: {
    items: [{ ...Unchecked.args }],
  },
};

이 방식은 설정하기가 조금 복잡할 수 있지만, 복합 컴포넌트 안에 있는 각 스토리의 args를 훨씬 쉽게 재사용할 수 있다는 장점이 있어요. 무엇보다 컨트롤(Controls) 패널을 통해 컴포넌트의 인자들을 실시간으로 변경해볼 수 있다는 게 큰 매력이죠!

profile
프론트에_가까운_풀스택_개발자

0개의 댓글