anti-07. Storybook을 그려놓고 보지 않는다 — 페이지 단위 스토리, args 미동기화
빈도: 25건 중 12+건 — 학습 목표 2번이 “Storybook으로 컴포넌트의 다양한 상태를 시각적으로 테스트한다” 임에도, Storybook은 거의 모든 PR에 세팅되어 있지만 시각 테스트가 작동하지 않는 형태로 들어온다.
패턴 한 줄
Storybook이 없는 게 아니다. 있다 — 하지만 (1) Story 단위가 페이지·App 전체이거나, (2) 컴포넌트가 자체 useState로 args를 무시하거나, (3) Default 한두 개만 만들고 상태별 분기 를 쓰지 않는다. 결과는 시각 테스트가 작동하지 않는 Storybook.
AS-IS 코드 (8기 PR 발췌)
🔍 읽기 전에
“내
.stories.tsx파일을 열어보자. Story 이름이Default하나뿐인가? Controls에서 값을 바꿨을 때 실제로 컴포넌트가 그 값으로 다시 그려지는가?”
사례 A — args가 컴포넌트에 안 닿는다 (자체 useState)
PR #498 — ExpiryField.stories.tsx
코드래빗 리뷰가 한 줄로 짚는다.
“Interactive 스토리가 Controls 변경값을 반영하지 않습니다.
useState(args.x)초기값은 최초 1회만 적용되어, Controls에서 expiryMonth/expiryYear를 바꿔도 상태가 동기화되지 않습니다.”
페어 본인은 PR body에 “Storybook은 단순 테스트 도구로만 보면 상위 컴포넌트 중심으로 작성해도 충분할 수 있다고 생각했습니다” 라고 적었다 — Storybook을 시각 테스트로 쓴다는 학습 목표가 “세팅 산출물을 만들면 끝” 으로 후퇴한 흔적이다.
PR #514 도 같은 결의 신호.
// CardNetworkBrand.stories.tsx (발췌)
const [, setNetworkBrand] = useState("");코드래빗: “이 스토리로는 Visa/Mastercard 분기와 연동된 렌더링을 실제로 검증할 수 없습니다.” — getter를 누락한 채 setter만 디스트럭처링했다. Story가 상태 분기를 표현할 의도였다면 처음부터 막혀 있다.
사례 B — Story 단위가 페이지·App 전체
PR #510 PR #522 — *.stories.tsx 단위가 CardPreview/CardInfoInput 2개뿐. 필드 단위(Input 1개)가 아니라 섹션 단위(여러 Input + 라벨 + 에러)로 묶여 있어, 한 Story로 그릴 수 있는 상태 분기가 너무 많다.
PR #500 PR #523 에는 Storybook init 템플릿(Button.stories.tsx, Header.stories.tsx, Page.stories.tsx)이 그대로 잔존한다. 도메인 컴포넌트보다 init 더미 스토리가 많은 형태.
사례 C — Story 이름이 의도를 드러내지 않는다
PR #499 — 리뷰어 JUDONGHYEOK가 명시적으로 짚었다.
“스토리 이름이 First라 의도가 드러나지 않아요. Default, Error, Focused처럼 상태를 설명하는 이름으로 바꾸면 더 의도가 잘 드러나게 될 것 같습니다!”
이름이 Default / Error / Focused / Filled 가 아니라 First / Second / Third 라는 건 — 상태별 분기를 의도적으로 만들지 않았다 는 신호다.
사례 D — 페어가 직접 단위 결정 미해소를 자백
PR #517 — PR body에서 페어가 직접 질문한다.
“Q2 — 스토리북은 어떤 컴포넌트에 붙이는 게 효율적일까? Q3 — 현업에서 스토리북 어떻게 쓰나요?”
학습 목표가 명시되어 있는데도 어디에 붙일지를 페어가 정하지 못했다. 결국 세팅하고 일단 default 스토리를 둔다는 전략으로 흘러간다.
PR #524 는 더 노골적이다.
“처음에는 Storybook 없이 곧바로 컴포넌트를 만들기 시작했습니다. 그래서 페어와 함께 레포지토리를 한 번 엎고 Storybook부터 공부하는 시간을 가졌습니다.”
Storybook이 컴포넌트 설계의 출발점이 아니라 사후 도구로 도입됐다 — 그 결과 컴포넌트의 props가 비대해지는 신호(anti-06)까지 함께 따라온다.
리뷰어 피드백 (실제 인용)
- PR #498
ExpiryField.stories.tsx(코드래빗) — “useState(args.x)의 초기값은 최초 1회만 적용되어, Controls에서 … 상태가 동기화되지 않습니다.” - PR #514
CardNetworkBrand.stories.tsx(코드래빗) — “이 스토리로는 Visa/Mastercard 분기와 연동된 렌더링을 실제로 검증할 수 없습니다.” - PR #499 (JUDONGHYEOK) — “스토리 이름이 First라 의도가 드러나지 않아요. Default, Error, Focused처럼 상태를 설명하는 이름으로 바꾸면 더 의도가 잘 드러나게 될 것 같습니다!”
- PR #501 PR #512 (코드래빗) — “
storybook/theming은 devDependency 모듈이라 런타임 코드에서 import하면 빌드가 깨집니다.” → 도구 모듈을 앱 번들에 끌어쓴 사례 - PR #505 PR #507 (코드래빗) — “Storybook 전역 스타일이 앱과 동기화되지 않아 렌더 결과가 다릅니다.”
안티패턴이 모이는 한 가지 단어
위 사례들의 공통점은 “Storybook을 세팅 산출물로 다루고 시각 테스트 도구로 쓰지 않았다” 이다.
- 사례 A — args 동기화 가 안 되니 Controls가 사실상 죽어 있다
- 사례 B — 단위가 너무 커서 한 Story가 너무 많은 분기를 떠안는다
- 사례 C — 이름이 상태 분기를 표현하지 않는다 (First / Second / Third)
- 사례 D — 언제 붙일지를 정하지 않은 채 사후 도입한다
학습 목표가 명시한 것은 “Storybook 파일이 존재한다” 가 아니라 “컴포넌트의 다양한 상태를 시각적으로 테스트한다” 이다. 상태별로 Story가 분리되어 있고, args가 그 상태를 그릴 수 있어야 그 학습 목표가 충족된다.
토론 질문
코드를 보면서 페어와 함께 이야기해보자.
- 내 컴포넌트는 args만으로 상태를 그릴 수 있는가? 자체
useState로 초기값만 받고 그 다음은 Controls를 무시하는 형태인가? - 내 Story 이름은 Default / Error / Focused / Filled 같은 상태 단어인가, 아니면 First / Second / Third 처럼 순번인가? 순번이라면 — 그 Story가 표현하려는 상태는 무엇이었나?
- 컴포넌트의 다양한 상태를 시각적으로 테스트한다 라는 학습 목표를 충족했는지, 어떤 외부 신호로 검증할 수 있는가? “Storybook을 띄우고 5초 안에 default·error·focused가 보이는가” 같은 한 줄 기준을 만들 수 있는가?
연관 카드
- anti-06 — Input 컴포넌트가 입력 종류마다 따로 — Storybook의 단위 결정과 컴포넌트 분리는 같은 결정의 두 면
- Q1 — state location — args만으로 상태를 그릴 수 있는 컴포넌트는 controlled 여야 한다는 결정과 동전의 양면




