
https://www.componentdriven.org/
컴포넌트 중심 개발(CDD)은 빌드 프로세스를 컴포넌트 중심으로 고정하는 개발 방법론 이다. 컴포넌트 수준 즉, 기본 구성 요소에서 시작해서 점진적으로 결합하여 화면을 조립하는 '상향식'으로 구축한다.
컴포넌트 : 전체 시스템을 구성하는 하나의 부품 혹은 모듈
프론트엔드에서 컴포넌트 : UI를 구성하는 UI 요소
최근에 User Interface, User Experience가 복잡해짐에 따라 UI에 더 많은 로직이 섞이고, 프로젝트의 규모가 커짐에 따라 UI의 규모도 함께 커지면서 UI를 다루기가 힘들어졌다. 이에 UI를 구축할때 직면하게 되는 앱 규모의 복잡성을 해결하기 위해 CDD가 등장했다.
복잡한 화면을 모듈방식의 간단한 컴포넌트로 분해한다면 견고하면서도 유연한 UI를 쉽게 구축할 수 있다.


생각없이 개발하면 기준없이 컴포넌트를 만들수 있다. => 고민이 필요함

컴포넌트 모듈은 데이터 관리, 사용자에게 데이터 보여주기(UI), 상호작용 하기의 역할을 한다.
"Unix 철학의 기초"의 규칙 4는 다음과 같다.
분리 규칙: 메커니즘에서 정책을 분리합니다. 엔진과 인터페이스의 분리를 의미합니다.
에릭 S. 레이몬드
이 경우에 정책은 user interface, 메커니즘은 logic & behavior로 생각할 수 있다.
데이터를 다루는 로직, UI로직이 하나의 컴포넌트에 존재하면 재사용을 못하는 문제점이 발생한다. 데이터는 쉽게 바뀌지 않지만, UI는 개발하다보면 쉽게 바뀌기 때문에 데이터와 UI를 분리하는 것이 좋다. 상호작용 하는 부분도 분리하면 같은 동작을 할때 재사용이 가능하기 때문에 이것도 분리하는 것이 좋다.
예시로, 달력 컴포넌트를 생각해보면 달력을 구성하고 있는 데이터 자체는 변하지 않지만, 달력의 UI는 언제든 바뀔 수 있다.

// Calendar 컴포넌트에서는 hooks에서 반환된 값을 어떻게 보여줄지만 정의하면 됨
// 또다른 달력 컴포넌트를 만들어야 할 때, 디자인이 다르더라도, useCalendar hooks를 가져다 사용할 수 있음
export default function Calendar() {
const { headers, body, view } = useCalendar(); // useCalendar라는 hooks로 달력에 대한 데이터를 추상화해서 표현
return (
<Table>
<Thead>
<Tr>
{headers.weekDays.map(({ key, value }) => {
return <Th key={key}>{format(value, 'E', { locale })}</Th>
})}
</Tr>
</Thead>
<Tbody>
{body.value.map(({ key, value: days }) => (
<Tr key={key}>
{days.map(({ key, value }) => (
<Td key={key}>{getDate(value)}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
);
}
예시로, 버튼 컴포넌트에 longPress라는 동작을 정의한다고 했을때.
interface Props extends ComponentProps<typeof Button> {
onLongPress?: (event: LongPressEvent) => void;
}
// 버튼 컴포넌트에서는 hooks로부터 반환되는 값을 UI에 적용하기만 함
export function PressButton(props: Props) {
const longPressProps = useLongPress();
return <Button {...longPressPorops} {...props} />
}
// useLongPress라는 hooks를 만들어서 동작 추상화. 다른 컴포넌트에서도 사용 가능.
function useLongPress() {
return {
onKeyDown={( e ) => {
// ...
}},
onKeyUp={( e ) => {
// ...
}},
onMouseDown={( e ) => {
// ...
}},
onMouseUp={( e ) => {
// ...
}}
}
}
변경가능성에 기준을 두고 변경가능성이 높은 스타일링 담당 코드와 변경가능성이 낮은 데이터를 다루는 코드를 분리하면 오로지 데이터에만 집중해서 모듈화할 수 있다. 이러한 패턴을 Headless라고 한다.

head는 컨텐츠를 보여주는 방법인 UI, 몸통은 컨텐츠 자체인 데이터를 말한다. 따라서 headless란 스타일링을 담당하게 되는 부분을 과감하게 제외하고 상태와 관련된 부분만을 다루는 것을 뜻한다.
한가지 문제에만 집중하기 때문에 더 많은 곳에서 사용할 수 있고, 다른 변경사항으로부터 격리시킬 수 있다.
한가지 역할만 하는 컴포넌트의 조합으로 구성하기
서로의 변경이 서로에게 영향을 끼치지 않도록 구성을 해서 컴포넌트가 변경에 유연하도록 한다.
합성이 가능하도록 컴포넌트를 설계하면 재사용하기도 좋고, 확장하기도 쉽다.
이때, 분리된 컴포넌트를 나중에 다시 찾기 쉽도록 디렉토리 체계(hierarchy)를 구축하는 과정도 함께 이루어져야 한다. atomic 디자인을 참고하여 컴포넌트를 분리하고 배치하면 좋다.
예시로, 드랍다운 컴포넌트가 있을때, 다루고 있는 데이터와 담당하고 있는역할을 기준으로 분리하면 다음과 같이 분리할 수 있다.

이렇게 분리한다면, 만약 Trigger부분을 다른 컴포넌트로 교체해야할때, 해당 컴포넌트만 수정하면 바로 대응할 수 있다.
compound pattern : https://www.patterns.dev/posts/compound-pattern
도메인을 포함하는 컴포넌트와 그렇지 않은 컴포넌트 분리하기
도메인 : 다루고 있는 비즈니스 로직과 관련된 부분
일반적인 인터페이스로 분리하기
일반적인 단어로 표현하면 컴포넌트의 역할을 이해하기가 쉽다. 즉, 컴포넌트의 인터페이스를 구성하는 컴포넌트의 이름과 Props 네이밍을 일반적이게 구성한다면 해당 컴포넌트의 의도를 드러내기가 쉽다.
예시로, 위의 프레임워크를 선택하는 드랍다운 컴포넌트의 Props에서 도메인을 분리할 수 있다.
// 도메인 포함
interface Props {
selectedFrameworks: string;
onFrameworkChange: (selected: string) => void;
frameworks: Array({ label: string }>;
}
// 도메인 분리
interface Props {
value : string;
onChange: (value: string) => void;
options: Array({ label: stirng }>
}
도메인을 분리하면 컴포넌트 인터페이스가 표준에 가까워진다. 즉, 사용하는 입장에서 어떻게 동작할지 예측하기가 쉽기 때문에 표준에 가까울수록 많은 사람들이 쉽게 이해할 수 있다.

UI 컴포넌트를 독립적으로 분리해서 개별 관리, 테스트를 도와주는 도구
React, Vue, Angular와 같은 컴포넌트 중심 도구는 복잡한 UI를 단순한 컴포넌트로 분해하는 데 도움이 되지만, 큰 프로젝트일수록 수백개의 컴포넌트가 존재하게 된다. 또한 UI가 비즈니스 로직, 인터랙티브 상태 및 앱 컨텍스트에 얽혀 있기 때문에 디버깅하기가 어렵다는 것이 문제를 더욱 복잡하게 만든다.
그리고 개발자는 수많은 UI 변형을 고려해야 하지만 이를 모두 개발하거나 정리할 수 있는 리소스가 부족하다. 결국 UI를 구축하기가 더 어렵고, 작업 만족도가 떨어지는 상황에 처할 수 있다.
이에대한 해결책으로 UI를 분리하여 빌드하는 방법이 있다.
스토리북은 앱 비즈니스 로직 및 컨텍스트의 간섭 없이 컴포넌트를 렌더링할 수 있는 격리된 iframe을 제공한다. 이를 통해 컴포넌트의 각 변형, 심지어 도달하기 어려운 엣지 케이스까지 개발에 집중할 수 있다.
스토리는 UI 컴포넌트의 렌더링된 상태를 캡처합니다.
개발자는 컴포넌트가 지원할 수 있는 모든 상태를 설명하는 여러 개의 스토리를 하나의 컴포넌트에 작성할 수 있다. (스토리에 대한 기준은 개인 or 팀 취향)
npx storybook@latest init
npm run storybook
// 따로 스토리 폴더를 분리하거나 해당 컴포넌트와 같은 위치에 생성
Button.js | ts | jsx | tsx | vue | svelte
Button.stories.js | ts | jsx | tsx
🔍 Zooming, 🖼 Background, 📐 Grid, 📏 Measure, 🎚️ Outline,📱 Viewport

컴포넌트에 대해 코드에서 추론되어 자동 생성된 문서가 표시된다. 팀에서 공통적으로 사용하는 컴포넌트를 공유할때 사용 설명서로도 사용할 수 있다.

스토리북의 핵심 기능을 확장하는 플러그인
// Button.jsx
// 버튼 컴포넌트 작성
export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={backgroundColor && { backgroundColor }}
{...props}
>
{label}
</button>
);
};
Button.propTypes = {
primary: PropTypes.bool,
backgroundColor: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
// Button.stories.js
// 버튼 컴포넌트 스토리 작성
import { Button } from './Button';
export default {
title: 'Example/Button', // 스토리북 구조 설정, '/'을 사용해서 그룹핑 가능
component: Button, // 스토리로 나타낼 컴포넌트
tags: ['autodocs'], // 문서 자동 생성 설정
argTypes: { // 특정 argTypes 생성. 정의안하면 자동으로 추론됨. 종료는 boolean, check, radio, select, file, date 등등
backgroundColor: { control: 'color' },
},
};
export const Primary = {
args: { // props 값 설정
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};
이외에 더 다양한 기능이 많고, 공식문서에 잘 정리가 되어있음.
https://storybook.js.org/
Component-Driven Development 와 동일하다.
핵심은... 앱 설계정에 무조건 스토리 정의하는 것. 프로젝트 설계가 더뎌지지만 atomic한 설계의 장점을 가져갈 수 있음.


결과물을 쉽고 빠르게 확인할 수 있다.
컴포넌트를 작성할때 이미 존재하는지 쉽게 확인할 수 있다.
또한 특정 환경일때만 확인할 수 있는 컴포넌트(like 신고, 차단)에 대해서 따로 변수변경이나 처리없이 스토리북을 통해 컴포넌트를 수정할 수 있다.
좀 더 나은 컴포넌트 설계를 고민하게 된다.
코드를 작성할 때는 제대로 설계했다고 생각하지만, 나중에 수정이나 재사용을 하려고 하면 설계의 문제점을 발견해서 리팩토링을 하게된다. 그런데 컴포넌트를 구현하면서 스토리 작성을 병행하면 스토리로 옮기는 게 예상대로 되지 않을 때(초반부) 설계의 결함을 발견할 수 있다. 그래서 스토리 작성을 위해 고민을 하다보면 자연스럽게 설계에 대한 고민과 리팩토링으로 이어진다.
Headless 그림이 귀여워요~!