-
Notifications
You must be signed in to change notification settings - Fork 2
[feature] 공통 컴포넌트 스토리 파일을 제작한다 #889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop-fe
Are you sure you want to change the base?
[feature] 공통 컴포넌트 스토리 파일을 제작한다 #889
Conversation
- @storybook/react-webpack5를 @storybook/react-vite로 교체 - .storybook/main.ts 설정 업데이트 - 경로 별칭(alias) 해결을 위해 vite-tsconfig-paths 추가 - 컴포넌트에 React를 임포트하여 ReferenceError 수정
- Modal.stories.tsx 파일 생성하여 다양한 케이스(기본, 설명 없음, 긴 내용) 추가
- Spinner.stories.tsx 파일 생성 (기본, 커스텀 높이 예시) - height prop이 스타일 컴포넌트로 전달되지 않던 버그 수정
- InputField.stories.tsx 파일 생성 (기본, 라벨 포함, 비밀번호, 에러 상태 등 다양한 케이스 추가)
- 기본 상태, 초기값 설정, 커스텀 플레이스홀더 등 다양한 사용 예시 추가 - 인터랙티브한 테스트를 위해 useState를 활용한 상태 관리 로직 포함
- Compound Component 패턴을 활용한 드롭다운 스토리 구현 - 기본 드롭다운 및 메뉴 위치 조정(Top) 예시 추가 - 제네릭 타입 호환성을 위한 타입 캐스팅 적용
- CustomTextArea.stories.tsx 파일 생성 (기본, 라벨 포함, 에러 상태, 글자수 제한 등)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
Storybook 마이그레이션 (Webpack → Vite) frontend/.storybook/main.ts, frontend/package.json |
프레임워크를 @storybook/react-webpack5에서 @storybook/react-vite로 변경, webpackFinal 제거, viteFinal 추가, Webpack 관련 의존성 제거 및 react-vite 의존성 추가 |
공통 컴포넌트 스토리 파일 추가 frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx, frontend/src/components/common/CustomTextArea/CustomTextArea.stories.tsx, frontend/src/components/common/InputField/InputField.stories.tsx, frontend/src/components/common/Modal/Modal.stories.tsx, frontend/src/components/common/SearchField/SearchField.stories.tsx, frontend/src/components/common/Spinner/Spinner.stories.tsx |
각 컴포넌트별 기본 및 변형 스토리(Default, WithLabel, ErrorState 등) 추가, 상호작용 가능한 상태 관리 및 argTypes 정의 |
기존 스토리 및 컴포넌트 정리 frontend/src/components/common/Header/Header.stories.tsx, frontend/src/components/common/Modal/Modal.tsx, frontend/src/components/common/Spinner/Spinner.tsx |
Header 스토리에서 React import 제거, Modal 포맷팅 조정, Spinner에 height 속성 전달 |
Sequence Diagram(s)
sequenceDiagram
participant Storybook as Storybook (main.ts)
participant Vite as Vite Config
participant TsConfigPaths as vite-tsconfig-paths
participant Component as Component
Storybook->>Vite: viteFinal(config) 실행
Vite->>TsConfigPaths: tsconfigPaths() 플러그인 로드
TsConfigPaths->>Vite: tsconfig 경로 설정 적용
Vite->>Component: 모듈 해석 및 로드
Component->>Storybook: 스토리 렌더링
sequenceDiagram
participant User as 사용자
participant Story as Story (예: InputField.stories.tsx)
participant Component as InputField 컴포넌트
participant State as 로컬 상태
User->>Story: 값 입력 또는 이벤트 발생
Story->>State: useState 상태 업데이트
State->>Component: 최신 props 전달
Component->>Story: onChange/onClear 콜백 트리거
Story->>State: 상태 동기화
Story->>User: UI 업데이트
Estimated code review effort
🎯 2 (Simple) | ⏱️ ~12 minutes
- Storybook 설정 변경: Vite 마이그레이션 로직 검증 필요
- 스토리 파일 추가: 동일한 패턴이 여러 파일에 반복되므로 일관성 확인 위주
- 의존성 변경: package.json의 버전 호환성 확인
Possibly related issues
- [feature] MOA-390 공통 컴포넌트에 대해 스토리파일을 제작한다 #888: 공통 컴포넌트에 대해 스토리파일을 제작하는 주요 작업으로, 본 PR에서 CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner 스토리 파일을 추가하여 직접 해결
Possibly related PRs
- [chore] 빌드 툴 Webpack to Vite 전환 #844: 동일한 Storybook Webpack → Vite 마이그레이션 작업 수행
- [feature] Common 컴포넌트 스토리 파일 적용 #225: frontend/.storybook/main.ts의 경로 해석 설정 변경 (webpackFinal의 '@' 별칭에서 viteFinal의 tsconfig-paths로 전환)
- [release] FE v1.1.3 배포 #815: CustomDropDown 컴포넌트 리팩터와 본 PR의 CustomDropDown 스토리 추가가 동일 파일 수정
Suggested labels
✨ Feature
Suggested reviewers
- lepitaaar
- suhyun113
- Zepelown
Pre-merge checks and finishing touches
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | PR 제목이 변경 사항의 주요 내용을 명확하게 요약하고 있습니다. 공통 컴포넌트 스토리 파일 제작이라는 주요 변경 사항을 직접적으로 설명합니다. |
| Linked Issues check | ✅ Passed | PR의 모든 코드 변경 사항이 MOA-390의 요구 사항을 충족합니다. common 폴더의 컴포넌트들(CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner)에 대한 스토리북 파일이 제작되었고, Storybook 설정이 Vite로 마이그레이션되었습니다. |
| Out of Scope Changes check | ✅ Passed | 모든 변경 사항이 범위 내에 있습니다. Storybook 설정 변경(Webpack에서 Vite로의 마이그레이션)과 스토리 파일 추가는 공통 컴포넌트 스토리 파일 제작의 필수 작업입니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#888-common-components-storybook-MOA-390
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (3)
frontend/src/components/common/Modal/Modal.stories.tsx (1)
6-134: 모달 스토리 동작은 적절하지만, Controls 연동은 약간 개선 여지가 있습니다.
- 세 스토리 모두
useState(args.isOpen)으로 한 번만 초기화하기 때문에, Storybook Controls에서isOpen값을 변경해도 이후 렌더에서는 로컬 state가 우선되어 arg 변경이 곧바로 반영되지는 않습니다. 큰 문제는 아니지만, args 기반으로 상태를 제어하고 싶다면 로컬 state를 제거하거나, 별도 상호작용 전용 스토리/데코레이터로 분리하는 방법도 있습니다.children의control: 'text'설정은 LongContent처럼 JSX 노드를 args로 넘기는 케이스와 다소 어울리지 않습니다. 이 스토리는 control을 끄거나,children을 string 기반 예제로만 둘지 한 번 고민해 보셔도 좋겠습니다.현재 구현만으로도 기능 확인에는 충분해 보여 선택 사항으로 남겨두셔도 됩니다.
frontend/package.json (1)
47-58: 버전 호환성 정보를 확인하겠습니다.Storybook 8.5~8.6 혼용은 호환성 문제가 없습니다.
Storybook 8.6은 주요 breaking changes가 없으므로,
@storybook/react-vite(^8.6.14)과 핵심 패키지 (^8.5.0)를 혼용해도 안정성 측면에서 기술적 문제는 없습니다. Caret range(^)에 의해 npm은 8.5.0 이상 9.0.0 미만의 모든 버전을 허용하므로 버전 정렬은 필수가 아닙니다.다만 일관성을 위해 모든 Storybook 패키지를 동일 major·minor 버전으로 맞추는 것은 좋은 관행입니다. 현재 상태에서 즉시 수정이 필요하지는 않습니다.
frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (1)
86-86: 타입 단언을 고려해보세요.
optionsprop에 타입 단언(as readonly { label: string; value: string }[])을 사용하고 있습니다. 이는args.options의 타입이CustomDropDown이 기대하는 타입과 정확히 일치하지 않음을 시사합니다.가능하다면 argTypes 정의나 OPTIONS 상수의 타입을 조정하여 타입 단언을 제거하는 것이 타입 안전성 측면에서 더 좋습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
frontend/.storybook/main.ts(1 hunks)frontend/package.json(1 hunks)frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx(1 hunks)frontend/src/components/common/CustomTextArea/CustomTextArea.stories.tsx(1 hunks)frontend/src/components/common/Header/Header.stories.tsx(0 hunks)frontend/src/components/common/InputField/InputField.stories.tsx(1 hunks)frontend/src/components/common/Modal/Modal.stories.tsx(1 hunks)frontend/src/components/common/Modal/Modal.tsx(2 hunks)frontend/src/components/common/SearchField/SearchField.stories.tsx(1 hunks)frontend/src/components/common/Spinner/Spinner.stories.tsx(1 hunks)frontend/src/components/common/Spinner/Spinner.tsx(2 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/components/common/Header/Header.stories.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-09T08:11:51.820Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 388
File: frontend/src/components/common/Spinner/Spinner.tsx:1-31
Timestamp: 2025-05-09T08:11:51.820Z
Learning: 로딩 스피너(Spinner) 컴포넌트에는 웹 접근성을 위해 `role="status"` 및 `aria-label` 같은 ARIA 속성을 추가해야 합니다. 이는 스크린 리더와 같은 보조 기술이 로딩 상태를 사용자에게 적절히 전달할 수 있게 합니다.
Applied to files:
frontend/src/components/common/Spinner/Spinner.tsx
🧬 Code graph analysis (1)
frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (1)
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
CustomDropDown(94-115)
🔇 Additional comments (11)
frontend/src/components/common/Modal/Modal.tsx (1)
15-31: 기능 변화 없음 – 포맷팅 정리만 확인했습니다.
useEffect블록과 헤더 영역 JSX 들여쓰기만 변경되었고, 모달의 열림/닫힘·백드롭 동작에는 영향이 없습니다. 그대로 머지해도 무방해 보입니다.frontend/src/components/common/Spinner/Spinner.tsx (1)
4-40:heightprop 전달 보완 및 접근성 유지 모두 적절합니다.
SpinnerWrapper가height를 받아 스타일에 반영하고,Spinner에서 이를 실제로 전달하도록 수정된 부분이 스토리(CustomHeight)와도 잘 맞습니다. 기존에 유지하던role="status",aria-label="로딩 중"도 그대로라 접근성 요구사항도 충족합니다.Based on learnings, 로딩 스피너에 ARIA 속성을 포함해야 한다는 이전 합의와도 일치합니다.
frontend/src/components/common/Spinner/Spinner.stories.tsx (1)
4-44: Spinner 높이 제어 스토리가 컴포넌트 변경과 잘 맞습니다.기본 스토리와
CustomHeight스토리가heightprop 사용법을 명확히 보여주고 있고, 테두리 박스 안에서 렌더링하는 예시도 실제 사용 시나리오를 잘 설명해 줍니다. 현재 구조로 충분히 직관적이라 추가 수정 없이 사용 가능해 보입니다.frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (5)
1-4: 임포트가 적절합니다.Storybook 스토리 파일에 필요한 모든 임포트가 올바르게 구성되어 있습니다.
6-35: 메타 설정이 잘 구성되어 있습니다.argTypes에 한글 설명이 명확하게 작성되어 있고, autodocs 태그와 layout 설정이 적절합니다.
40-50: 스토리용 스타일 컴포넌트가 적절합니다.스토리 파일 내에서 데모용 스타일 컴포넌트를 정의하는 것은 일반적인 패턴입니다.
52-56: 샘플 데이터가 적절합니다.드롭다운 테스트를 위한 옵션 데이터가 명확하게 정의되어 있습니다.
1-108: React 19와 Storybook 8.5.0의 호환성은 공식적으로 검증되었습니다.검증 결과, Storybook 8.5.0은 2025년 1월 21일 릴리스에서 React 19 지원을 공식 추가했습니다. CustomDropDown 컴포넌트는 React 19의 알려진 호환성 문제(element.ref 제거)와 관련된 패턴(element.ref, forwardRef 등)을 사용하지 않으므로 현재 구성에서 안전합니다.
frontend/.storybook/main.ts (3)
1-1: Vite 프레임워크로의 마이그레이션이 올바릅니다.
@storybook/react-webpack5에서@storybook/react-vite로 임포트를 변경한 것이 적절합니다.
13-13: 프레임워크 설정이 올바릅니다.Vite 기반 Storybook으로 프레임워크를 변경한 것이 적절합니다.
16-23: viteFinal 구성이 올바르게 구현되어 있으며 모든 의존성이 확인되었습니다.
vite-tsconfig-paths(^5.1.4)와vite(^7.2.2)가 모두devDependencies에 정상적으로 설치되어 있습니다. 동적 임포트와mergeConfig를 사용한 플러그인 추가 방식은 Vite 기반 Storybook의 권장 패턴이며, 이를 통해 tsconfig.json에 정의된 경로 별칭(예:@/...)이 Storybook에서 올바르게 작동합니다.
| const meta = { | ||
| title: 'Components/Common/CustomTextArea', | ||
| component: CustomTextArea, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| value: { | ||
| control: 'text', | ||
| description: '텍스트 영역의 값입니다.', | ||
| }, | ||
| onChange: { | ||
| action: 'changed', | ||
| description: '값이 변경될 때 호출되는 함수입니다.', | ||
| }, | ||
| placeholder: { | ||
| control: 'text', | ||
| description: '텍스트 영역의 플레이스홀더입니다.', | ||
| }, | ||
| label: { | ||
| control: 'text', | ||
| description: '텍스트 영역 상단에 표시되는 라벨입니다.', | ||
| }, | ||
| width: { | ||
| control: 'text', | ||
| description: '텍스트 영역의 너비입니다.', | ||
| }, | ||
| disabled: { | ||
| control: 'boolean', | ||
| description: '비활성화 여부입니다.', | ||
| }, | ||
| isError: { | ||
| control: 'boolean', | ||
| description: '에러 상태 여부입니다.', | ||
| }, | ||
| helperText: { | ||
| control: 'text', | ||
| description: '하단에 표시되는 도움말 텍스트입니다 (에러 시 표시).', | ||
| }, | ||
| showMaxChar: { | ||
| control: 'boolean', | ||
| description: '최대 글자수 표시 여부입니다.', | ||
| }, | ||
| maxLength: { | ||
| control: 'number', | ||
| description: '최대 글자수 제한입니다.', | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof CustomTextArea>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| placeholder: '내용을 입력하세요', | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <CustomTextArea | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const WithLabel: Story = { | ||
| args: { | ||
| label: '자기소개', | ||
| placeholder: '자기소개를 입력하세요', | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <CustomTextArea | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const ErrorState: Story = { | ||
| args: { | ||
| label: '지원동기', | ||
| value: '너무 짧습니다.', | ||
| isError: true, | ||
| helperText: '10자 이상 입력해주세요.', | ||
| width: '300px', | ||
| onChange: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <CustomTextArea | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const WithMaxLength: Story = { | ||
| args: { | ||
| label: '문의 내용', | ||
| placeholder: '100자 이내로 입력해주세요', | ||
| maxLength: 100, | ||
| showMaxChar: true, | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <CustomTextArea | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const Disabled: Story = { | ||
| args: { | ||
| label: '피드백', | ||
| value: '이미 제출된 피드백입니다.', | ||
| disabled: true, | ||
| width: '300px', | ||
| onChange: () => { }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onChange Action이 args의 no-op 핸들러로 가려질 수 있습니다.
argTypes.onChange에 action: 'changed'를 설정해 두었지만, 각 스토리의 args에서 onChange: () => { }를 지정해 기본 action 핸들러를 덮어쓰고 있습니다. 이 경우 args.onChange?.(e) 호출이 실제로는 아무 일도 하지 않아서, Storybook Actions 패널에 이벤트가 찍히지 않을 수 있습니다.
각 스토리의 args에서 onChange를 제거하고, render 함수에서만 args.onChange?.(e)를 호출하는 쪽이 더 자연스러워 보입니다.
중복된 상태 관리/렌더링 로직은 공통화 여지가 있습니다.
Default, WithLabel, ErrorState, WithMaxLength 스토리가 모두 useState(args.value || '') + 공통 onChange 구현을 공유하므로, 헬퍼 함수나 데코레이터 추출로 중복을 줄일 수 있습니다. 필수는 아니고 가독성·유지보수 관점에서의 선택 사항입니다.
🤖 Prompt for AI Agents
frontend/src/components/common/CustomTextArea/CustomTextArea.stories.tsx lines
5-161: several stories set onChange: () => {} in their args which overrides the
argTypes action and prevents Storybook Actions from firing; remove onChange from
each story's args (Default, WithLabel, ErrorState, WithMaxLength, Disabled) so
the argTypes action is used, keep the render implementations calling
args.onChange?.(e) to forward the event, and optionally extract the repeated
useState + onChange render pattern into a small helper or decorator to eliminate
duplication.
| const meta = { | ||
| title: 'Components/Common/InputField', | ||
| component: InputField, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| value: { | ||
| control: 'text', | ||
| description: '입력 필드의 값입니다.', | ||
| }, | ||
| onChange: { | ||
| action: 'changed', | ||
| description: '값이 변경될 때 호출되는 함수입니다.', | ||
| }, | ||
| onClear: { | ||
| action: 'cleared', | ||
| description: '삭제 버튼 클릭 시 호출되는 함수입니다.', | ||
| }, | ||
| placeholder: { | ||
| control: 'text', | ||
| description: '입력 필드의 플레이스홀더입니다.', | ||
| }, | ||
| label: { | ||
| control: 'text', | ||
| description: '입력 필드 상단에 표시되는 라벨입니다.', | ||
| }, | ||
| type: { | ||
| control: 'radio', | ||
| options: ['text', 'password'], | ||
| description: '입력 필드의 타입입니다.', | ||
| }, | ||
| width: { | ||
| control: 'text', | ||
| description: '입력 필드의 너비입니다.', | ||
| }, | ||
| disabled: { | ||
| control: 'boolean', | ||
| description: '비활성화 여부입니다.', | ||
| }, | ||
| isError: { | ||
| control: 'boolean', | ||
| description: '에러 상태 여부입니다.', | ||
| }, | ||
| isSuccess: { | ||
| control: 'boolean', | ||
| description: '성공 상태 여부입니다.', | ||
| }, | ||
| helperText: { | ||
| control: 'text', | ||
| description: '하단에 표시되는 도움말 텍스트입니다 (에러 시 표시).', | ||
| }, | ||
| showClearButton: { | ||
| control: 'boolean', | ||
| description: '삭제 버튼 표시 여부입니다.', | ||
| }, | ||
| showMaxChar: { | ||
| control: 'boolean', | ||
| description: '최대 글자수 표시 여부입니다.', | ||
| }, | ||
| maxLength: { | ||
| control: 'number', | ||
| description: '최대 글자수 제한입니다.', | ||
| }, | ||
| readOnly: { | ||
| control: 'boolean', | ||
| description: '읽기 전용 여부입니다.', | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof InputField>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| placeholder: '텍스트를 입력하세요', | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| onClear: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <InputField | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| onClear={() => { | ||
| setValue(''); | ||
| args.onClear?.(); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const WithLabel: Story = { | ||
| args: { | ||
| label: '이메일', | ||
| placeholder: 'example@email.com', | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| onClear: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <InputField | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| onClear={() => { | ||
| setValue(''); | ||
| args.onClear?.(); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const Password: Story = { | ||
| args: { | ||
| type: 'password', | ||
| label: '비밀번호', | ||
| placeholder: '비밀번호를 입력하세요', | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| onClear: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <InputField | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| onClear={() => { | ||
| setValue(''); | ||
| args.onClear?.(); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const ErrorState: Story = { | ||
| args: { | ||
| label: '닉네임', | ||
| value: '이미 사용중인 닉네임입니다', | ||
| isError: true, | ||
| helperText: '이미 사용중인 닉네임입니다.', | ||
| width: '300px', | ||
| onChange: () => { }, | ||
| onClear: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <InputField | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| onClear={() => { | ||
| setValue(''); | ||
| args.onClear?.(); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
| export const WithMaxLength: Story = { | ||
| args: { | ||
| label: '한줄 소개', | ||
| placeholder: '20자 이내로 입력해주세요', | ||
| maxLength: 20, | ||
| showMaxChar: true, | ||
| width: '300px', | ||
| value: '', | ||
| onChange: () => { }, | ||
| onClear: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value || ''); | ||
| return ( | ||
| <InputField | ||
| {...args} | ||
| value={value} | ||
| onChange={(e) => { | ||
| setValue(e.target.value); | ||
| args.onChange?.(e); | ||
| }} | ||
| onClear={() => { | ||
| setValue(''); | ||
| args.onClear?.(); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const Disabled: Story = { | ||
| args: { | ||
| label: '아이디', | ||
| value: 'disabled_user', | ||
| disabled: true, | ||
| width: '300px', | ||
| onChange: () => { }, | ||
| onClear: () => { }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onChange/onClear Actions가 args에 의해 덮어쓰일 수 있습니다.
argTypes에서 onChange, onClear에 action을 선언해 두셨는데, 각 스토리 args에서 onChange: () => { }, onClear: () => { }를 지정하면서 실제로는 no-op 함수가 들어가 Action 패널에 이벤트가 찍히지 않을 수 있습니다.
각 스토리의 args에서 onChange/onClear를 제거하고, render 안에서 그대로 args.onChange?.(e), args.onClear?.()를 호출하면 Storybook이 기본 action 핸들러를 주입해 줘서 더 편하게 이벤트를 확인할 수 있습니다.
중복된 render 로직은 공통 헬퍼로 정리할 수 있습니다.
여러 스토리에서 useState(args.value || '')와 onChange/onClear 구현이 거의 동일하니, 공통 renderInputField 함수나 데코레이터로 추출하면 코드가 조금 더 읽기 쉬워질 것 같습니다. 현재 상태도 동작에는 문제 없어서 선택 사항입니다.
🤖 Prompt for AI Agents
frontend/src/components/common/InputField/InputField.stories.tsx lines 5-231:
The stories currently set no-op onChange/onClear functions in each story's args
which override the argTypes actions; remove onChange and onClear from every
story's args so Storybook can inject the action handlers, keep the render
implementations calling args.onChange?.(e) and args.onClear?.() as-is;
optionally refactor the repeated render logic into a shared renderInputField
helper (or decorator) to avoid duplication.
| const meta = { | ||
| title: 'Components/Common/SearchField', | ||
| component: SearchField, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| value: { | ||
| control: 'text', | ||
| description: '검색어 입력값입니다.', | ||
| }, | ||
| onChange: { | ||
| action: 'changed', | ||
| description: '입력값이 변경될 때 호출되는 함수입니다.', | ||
| }, | ||
| onSubmit: { | ||
| action: 'submitted', | ||
| description: '검색 제출 시 호출되는 함수입니다.', | ||
| }, | ||
| placeholder: { | ||
| control: 'text', | ||
| description: '입력창의 플레이스홀더 텍스트입니다.', | ||
| }, | ||
| ariaLabel: { | ||
| control: 'text', | ||
| description: '접근성을 위한 aria-label 속성입니다.', | ||
| }, | ||
| autoBlur: { | ||
| control: 'boolean', | ||
| description: '제출 후 자동으로 포커스를 해제할지 여부입니다.', | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof SearchField>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| // 기본 검색창 스토리 | ||
| export const Default: Story = { | ||
| args: { | ||
| value: '', | ||
| placeholder: '동아리 이름을 입력하세요', | ||
| autoBlur: true, | ||
| onChange: () => { }, | ||
| onSubmit: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value); | ||
|
|
||
| return ( | ||
| <SearchField | ||
| {...args} | ||
| value={value} | ||
| onChange={(newValue) => { | ||
| setValue(newValue); | ||
| args.onChange(newValue); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| // 값이 미리 채워진 상태 | ||
| export const WithValue: Story = { | ||
| args: { | ||
| value: '밴드 동아리', | ||
| placeholder: '검색어를 입력하세요', | ||
| autoBlur: true, | ||
| onChange: () => { }, | ||
| onSubmit: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value); | ||
|
|
||
| return ( | ||
| <SearchField | ||
| {...args} | ||
| value={value} | ||
| onChange={(newValue) => { | ||
| setValue(newValue); | ||
| args.onChange(newValue); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| // 커스텀 플레이스홀더 | ||
| export const CustomPlaceholder: Story = { | ||
| args: { | ||
| value: '', | ||
| placeholder: '원하는 태그를 검색해보세요 (#음악, #운동)', | ||
| autoBlur: true, | ||
| onChange: () => { }, | ||
| onSubmit: () => { }, | ||
| }, | ||
| render: (args) => { | ||
| const [value, setValue] = useState(args.value); | ||
|
|
||
| return ( | ||
| <SearchField | ||
| {...args} | ||
| value={value} | ||
| onChange={(newValue) => { | ||
| setValue(newValue); | ||
| args.onChange(newValue); | ||
| }} | ||
| /> | ||
| ); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onSubmit prop이 스토리에서 실제로 전달되지 않습니다.
argTypes와 설명에서는 onSubmit을 지원한다고 되어 있지만, 세 스토리 모두 SearchField에 onSubmit={args.onSubmit}를 넘기지 않아 submit 동작·Action 로깅이 확인되지 않습니다. render 내 JSX에 onSubmit={args.onSubmit}를 추가하는 편이 좋습니다.
Actions 설정과 중복 로직은 간단히 정리할 수 있습니다.
argTypes.onChange/onSubmit에action을 이미 선언했기 때문에, 각 스토리args에서onChange: () => {},onSubmit: () => {}를 굳이 지정하지 않으면 기본 action 핸들러가 연결됩니다.- 세 스토리의
render구현이 거의 동일하므로, 공통renderSearchField헬퍼를 만들거나 Story-level decorator로 중복을 줄일 수 있습니다.
🤖 Prompt for AI Agents
frontend/src/components/common/SearchField/SearchField.stories.tsx lines 5-116:
The stories define argTypes.onSubmit but do not pass args.onSubmit into the
rendered SearchField, and each story redundantly supplies empty
onChange/onSubmit handlers and duplicated render logic; fix by (1) in each
story's render, add onSubmit={args.onSubmit} to the SearchField props (keep
onChange handling as-is so local state updates and action logging both occur),
(2) remove the explicit onChange: () => {} and onSubmit: () => {} from each
story's args so Storybook's action from argTypes is used, and (3) optionally DRY
the repeated render by extracting a shared renderSearchField helper or using a
story-level decorator that manages state and passes args through.
- Storybook ^8.6이 아직 Vite 7을 지원하지 않아 Vite를 ^6.0.0으로 다운그레이드 - 버전 일관성을 위해 모든 Storybook 관련 패키지를 ^8.6.14로 업그레이드 - 호환되지 않는 피어 의존성으로 인한 npm install 에러 해결
#️⃣연관된 이슈
📝작업 내용
Storybook 빌더 변경 (Webpack → Vite)
Vite 설정 오버라이딩
vite-tsconfig-paths플러그인을 적용하기 위해 viteFinal 설정을 추가했습니다.공통 컴포넌트 스토리 추가
스토리북 배포링크
추가된 컴포넌트: CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner
🚀 트러블 슈팅
vercel preview에서 배포가 실패했습니다. (커밋 전
npm run build:dev습관 중요..)원인은 현재 vite는 7.2.2 이고 storybook은 8.6인데, 8.6 버전에서는 vite 7버전을 지원하지 않습니다.
그래서 vite 6.0.0으로 다운 그레이드하여 해결했습니다.
중점적으로 리뷰받고 싶은 부분(선택)
vite 6버전으로 내렸을 때 문제가 생길만한 부분이 있을지 찾아봐야 할 것 같네요.
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
Chores
✏️ Tip: You can customize this high-level summary in your review settings.