일관된 사용자 경험을 제공하고 개발 및 디자인 프로세스를 효율화하기 위해 디자인 시스템을 구축하는 것이 중요하다고 느꼈다.
특히, 프로젝트 싱크스팟 은 디자이너의 부재로 디자인 시스템이 명확히 존재하지 않았으며, 기획자는 프로젝트의 컴포넌트의 구조와 변동사항 적용을 독립된 환경에서 검증하고 싶어했다.
프론트엔드에서 스토리북 Storybook 을 사용해 디자인 시스템 구축과 협업 효율을 극대화 할 수 있다고 판단해 도입했다.
- 컴포넌트의 파편화 및 재사용성 부족: 유사한 UI를 가진 컴포넌트들이 각기 다르게 개발되거나, 특정 페이지에 종속적으로만 사용되어 재사용성이 떨어지는 경우
- 디자이너/기획자와의 소통 비효율: 완성된 화면이나 개발 서버를 통해서만 UI를 공유하고 피드백을 주고받는 과정에서 시간 소요가 크고, 디자인 시스템의 일관성 유지의 어려움
- 독립적인 컴포넌트 개발 환경의 부재: 전체 애플리케이션을 실행해야만 개별 컴포넌트의 동작을 확인할 수 있어 개발 속도가 느려지고 디버깅이 번거로움
이러한 문제들을 해결하고 `공용 UI 컴포넌트`와 `디자인 토큰`을 체계적으로 관리하며 시각적으로 문서화할 필요성을 느끼게 되었다.
해결책으로 스토리북 `Storybook` 도입 결정!
스토리북 Storybook
UI 컴포넌트 개발을 위한 오픈 소스 도구 https://storybook.js.org/docs
웹 애플리케이션이나 모바일 애플리케이션을 개발할 때, 각각의 UI 컴포넌트를 독립적으로 빌드하고 시각적으로 문서화하여 개발 효율성을 높이는 데 활용
- 컴포넌트 단위 개발: 전체 애플리케이션의 복잡한 로직에 얽매이지 않고, 개별 UI 컴포넌트만을 위한 독립적인 개발 환경을 제공
- 시각적 테스트: 컴포넌트의 다양한 상태(예: 활성화, 비활성화, 로딩, 에러 등)를 시각적으로 확인, 테스트
- 문서화: 컴포넌트의 props와 사용 방법, 예시 등을 명확하게 문서화하여 팀원 간의 공유와 협업을 편리하게 한다.
- 재사용성과 일관성: 디자인 시스템의 핵심인 재사용 가능한 컴포넌트를 구축하고, 가이드라인의 일관된 UI를 유지한다.
단순한 개발 도구를 넘어, 디자이너, 기획자, 개발자 모두가 함께 UI/UX를 이해하고 소통하는 `공유 공간`의 역할을 수행한다고 이해했다.
적용 방법
npx sb init
yarn storybook
파일의 위치에 대한 팀 규칙 논의가 많았다
1. story 폴더 내에 정리하는가
2. 테스트하는 파일과 같은 위치에 두는가
파일의 스토리 작성 여부를 파악하기 용이하게 테스트파일과 같은 위치에 두기로 결정했다.

✅ Introduction 문서 작성
싱크스팟의 UI 컴포넌트에 대한 포괄적인 소개, 가이드라인 제공
- 프로젝트 소개: 이 UI 라이브러리의 목적, 주요 디자인 원칙 등을 설명
- 디자인 토큰: 색상 팔레트, 타이포그래피, 간격 등의 디자인 토큰들을 시각적으로 제시하여, 디자이너와 개발자가 참조할 수 있다.
- 컴포넌트 구분 : 공통, 특정 기능에서 사용되는 작은 단위부터, 페이지 단위까지 디자인 시스템을 구조화 했다.

✅ API 요청 대신 Mock 목 데이터 설정
실제 백엔드 API와의 연동 없이도 컴포넌트의 다양한 상태를 재현할 수 있어야 한다.
프로젝트에서 React Query를 사용하여 데이터를 관리하고 있기 때문에, 실제 API 호출 없이 컴포넌트가 다양한 데이터 상태(로딩, 성공, 에러, 빈 데이터 등)에서 어떻게 동작하는지 테스트하는 것이 중요하다.
여기서 핵심은 MSW(Mock Service Worker)와 같은 네트워크 요청 가로채기 도구를 사용하지 않고,
React Query의 캐시를 직접 조작하여 목 데이터를 제공하는 방법이다. `queryClient.setQueryData`를 활용하여 이 문제를 해결한다.
- 작동 원리: React Query는 데이터를 가져오기 전에 내부 캐시를 먼저 확인한다.
- 만약 해당 쿼리 키에 대한 데이터가 캐시에 이미 존재한다면, 실제 API 호출 없이 캐시된 데이터를 사용한다.
- 이 원리를 활용하여 스토리북 데코레이터에서 미리 queryClient.setQueryData를 통해 캐시에 원하는 목 데이터를 주입한다.
//..
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
const mockRoomDetailInfo = {
'1': {
data: {
name: '테스트 방',
memo: '',
memberCount: 2,
emails: ['test1@example.com', 'test2@example.com'],
},
},
'2': {
///...
};
const meta = {
title: 'Common/Modal/RoomDetailInfoModal',
component: RoomDetailInfoModal,
tags: ['autodocs'],
argTypes: {
room: {
description: '방 정보',
control: 'object',
},
onClose: {
description: '모달을 닫을 때 호출되는 함수',
action: 'closed',
},
},
parameters: {
layout: 'centered',
},
decorators: [
(Story) => {
queryClient.setQueryData(
['roomDetailInfo', '1'],
mockRoomDetailInfo['1'],
);
///...
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
},
],
} satisfies Meta<typeof RoomDetailInfoModal>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
room: {
roomId: '1',
roomName: '테스트 방',
},
onClose: () => {},
},
parameters: {
docs: {
description: {
story: '방 상세 정보 모달의 기본 디자인입니다.',
},
},
},
};
export const LongMemo: Story = {
///...
};
export const ManyMembers: Story = {
///...
};
- 구현 방법:
- 스토리 파일 상단에 테스트에 사용할 `QueryClient 인스턴스`를 생성한다.
- defaultOptions 설정에서 retry: false, refetchOnWindowFocus: false와 같이 실제 네트워크 요청을 방지하는 옵션을 추가한다.
- 테스트 시나리오별로 필요한 목 데이터를 `mockRoomDetailInfo` 에 정의한다.
- 이 데이터는 컴포넌트가 API로부터 받을 응답 형태와 동일하게 구성한다.
- 스토리북의 `decorators 속성`을 활용한다. 데코레이터 함수
- 정의된 QueryClient 인스턴스의 setQueryData 메서드를 사용
- ['roomDetailInfo', '1']와 같은 특정 쿼리 키에 미리 정의해둔 목 데이터를 주입한다.
- 각 스토리(Default, LongMemo, ManyMembers)에서 사용할 roomId에 맞춰 적절한 데이터를 캐시에 넣어준다.
- 테스트할 컴포넌트를 QueryClientProvider로 감싸주고, 앞서 생성한 queryClient 인스턴스를 client prop으로 전달하여 React Query 환경을 제공한다.
- 정의된 QueryClient 인스턴스의 setQueryData 메서드를 사용
- 스토리 파일 상단에 테스트에 사용할 `QueryClient 인스턴스`를 생성한다.
decorators에서 각 쿼리 키에 대한 mockRoomDetailInfo를 setQueryData로 직접 설정함으로써 RoomDetailInfoModal 컴포넌트가 useQuery 훅을 통해 데이터를 요청할 때 실제 서버 대신 이 목 데이터를 받게 된다.
네트워크 호출 없이 컴포넌트의 다양한 데이터 상태를 안정적이고 빠르게 테스트할 수 있다.
✅ 독립된 환경에서의 리액트 쿼리 설정
스토리북은 메인 애플리케이션과는 완전히 독립적인 런타임 환경을 제공한다.
컴포넌트를 개발하고 테스트할 때 메인 애플리케이션의 전역 상태나 라우팅, 복잡한 환경 설정으로부터 자유롭다는 큰 장점을 가진다.
다만, 프로젝트와의 독립성 때문에 React Query와 같은 데이터 관리를 스토리북에서 사용할 때 추가적인 설정이 필요하다.
- 메인 애플리케이션과의 분리: React 애플리케이션에서는 최상위 컴포넌트(App.tsx 또는 index.tsx)에서 QueryClientProvider를 한 번만 설정하여 애플리케이션 전역에서 React Query 기능을 사용할 수 있도록 한다. 하지만 스토리북 환경은 전역 설정과 무관하게 동작한다.
- 스토리 QueryClient 설정: React Query를 사용하는 컴포넌트를 스토리북에서 테스트하려면, 해당 컴포넌트의 스토리 파일 내에서 QueryClient 인스턴스를 생성하고, 이를 QueryClientProvider로 감싸주어야 한다.
- 독립된 환경에서의 쿼리 설정
- 스토리 파일 내부에 const queryClient = new QueryClient(...)를 정의하고, decorators 함수 안에서 <QueryClientProvider client={queryClient}>로 <Story />를 감싸준 것
- 독립적인 캐시 관리: 각 스토리(또는 스토리가 속한 스토리 파일)는 자신만의 독립적인 React Query 캐시를 가지게 된다.
- 즉, 한 스토리에서 setQueryData로 캐시에 데이터를 넣거나 invalidateQueries로 캐시를 무효화해도 다른 스토리에는 영향을 미치지 않아 `테스트의 격리성`을 보장한다.
- 독립된 환경에서의 쿼리 설정
이러한 방식은 각 컴포넌트 스토리가 완벽하게 자체적인 React Query 환경에서 작동하게 하여, 실제 애플리케이션 환경을 모방하면서도 복잡한 설정 없이 컴포넌트의 데이터 의존성을 효과적으로 관리할 수 있게 한다.
🔥 .storybook/preview.ts 전역에서 선언된 QueryCient 인스턴스를 사용하고, 각 스토리에서 `loaders`, `clear` 를 사용해서 중복을 줄이는 방법도 있다. 개선점으로 파악되었고, 리팩토링할 예정이다.
👀 도입 효과, 성과
- 협업 효율 증대:
- 디자이너/기획자와의 소통 비용 절감: 스토리북은 디자이너, 기획자, 개발자 모두가 동일한 시각 언어를 공유하는 공간이 된다. 더 이상 스크린샷이나 추상적인 설명이 아닌, 실제 동작하는 UI 컴포넌트를 보며 소통하므로 오해를 줄이고 피드백 주기를 단축시켰다.
- 병렬 개발 가능: 백엔드 API가 완성되지 않아도 프론트엔드 개발자는 스토리북 환경에서 목 데이터를 활용하여 독립적으로 UI 컴포넌트를 개발하고 테스트할 수 있게 되었다. 이는 전체 개발 일정을 앞당기는 데 기여했다.
- 디자인 시스템 강화 및 일관성 유지:
- 중앙 집중식 컴포넌트 라이브러리: 모든 UI 컴포넌트와 디자인 토큰이 스토리북을 통해 한곳에 모여 관리되므로, 재사용성이 크게 향상되었고, 새로운 기능 개발 시 기존 컴포넌트를 활용하여 일관된 UI를 빠르게 구축할 수 있었다.
- 시각적 문서화 자동화: Docs 기능을 활용하여 컴포넌트의 사용법, props, 예시 코드 등이 자동으로 문서화되어, 새로운 팀원이 프로젝트에 합류하거나 기존 컴포넌트의 사용법을 확인할 때 매우 유용하게 활용되고 있다.
- 개발 생산성 및 품질 향상:
- 빠른 변경사항 테스트: UI 변경사항이 발생했을 때, 전체 애플리케이션을 구동할 필요 없이 스토리북에서 해당 컴포넌트만 빠르게 확인하고 테스트할 수 있어 디버깅 및 수정 시간이 단축되었다.
- 예측 가능한 UI 동작: 컴포넌트의 모든 가능한 상태와 시나리오를 스토리북에서 미리 테스트하고 검증함으로써, 실제 서비스에 배포되었을 때 발생할 수 있는 UI 관련 버그를 최소화할 수 있었다.
스토리북의 도입은 단순히 기술 스택 하나를 추가한 것이 아니라, 프론트엔드 개발 팀의 문화와 워크플로우 전반에 걸쳐 긍정적인 변화를 가져왔다. 이는 앞으로 싱크스팟의 품질과 효율성을 한 단계 더 끌어올리는 중요한 기반이 될 것이다.