Component-Driven Development(with Storybook)

BiBi·2023년 10월 18일

찍먹

목록 보기
4/4
post-thumbnail

Component-Driven Development

https://www.componentdriven.org/



CDD란?

컴포넌트 중심 개발(CDD)은 빌드 프로세스를 컴포넌트 중심으로 고정하는 개발 방법론 이다. 컴포넌트 수준 즉, 기본 구성 요소에서 시작해서 점진적으로 결합하여 화면을 조립하는 '상향식'으로 구축한다.

Why Component?

컴포넌트 : 전체 시스템을 구성하는 하나의 부품 혹은 모듈
프론트엔드에서 컴포넌트 : UI를 구성하는 UI 요소

최근에 User Interface, User Experience가 복잡해짐에 따라 UI에 더 많은 로직이 섞이고, 프로젝트의 규모가 커짐에 따라 UI의 규모도 함께 커지면서 UI를 다루기가 힘들어졌다. 이에 UI를 구축할때 직면하게 되는 앱 규모의 복잡성을 해결하기 위해 CDD가 등장했다.

복잡한 화면을 모듈방식의 간단한 컴포넌트로 분해한다면 견고하면서도 유연한 UI를 쉽게 구축할 수 있다.

How to be Component Driven



컴포넌트 잘 만들기

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

목적

  1. 재사용가능한 컴포넌트 만들기
  2. 변경에 따른 부수효과 최소화하기

Headless 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

변경가능성에 기준을 두고 변경가능성이 높은 스타일링 담당 코드와 변경가능성이 낮은 데이터를 다루는 코드를 분리하면 오로지 데이터에만 집중해서 모듈화할 수 있다. 이러한 패턴을 Headless라고 한다.

head는 컨텐츠를 보여주는 방법인 UI, 몸통은 컨텐츠 자체인 데이터를 말한다. 따라서 headless란 스타일링을 담당하게 되는 부분을 과감하게 제외하고 상태와 관련된 부분만을 다루는 것을 뜻한다.
한가지 문제에만 집중하기 때문에 더 많은 곳에서 사용할 수 있고, 다른 변경사항으로부터 격리시킬 수 있다.

headless component를 사용하기에 적절한 경우의 기준(주관적)

  1. 이 컴포넌트의 수명은 얼마나 될까?
  2. 인터페이스를 제외하고 메커니즘을 의도적으로 보존할 가치가 있을까?
  3. 인터페이스가 얼마나 자주 바뀔까?
  4. 동일한 메커니즘을 사용하는 인터페이스가 존재하는가?

Composition

한가지 역할만 하는 컴포넌트의 조합으로 구성하기

서로의 변경이 서로에게 영향을 끼치지 않도록 구성을 해서 컴포넌트가 변경에 유연하도록 한다.
합성이 가능하도록 컴포넌트를 설계하면 재사용하기도 좋고, 확장하기도 쉽다.
이때, 분리된 컴포넌트를 나중에 다시 찾기 쉽도록 디렉토리 체계(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 }>
}

도메인을 분리하면 컴포넌트 인터페이스가 표준에 가까워진다. 즉, 사용하는 입장에서 어떻게 동작할지 예측하기가 쉽기 때문에 표준에 가까울수록 많은 사람들이 쉽게 이해할 수 있다.



Action Item

  1. 인터페이스를 먼저 고민하기 : 만들고자하는 기능이 이미 모듈화 되어있다고 가정하고 그것을 사용하듯이 작성해보기
    a. 의도가 무엇인가?
    b. 이 컴포넌트의 기능은 무엇인가?
    c. 어떻게 표현되어야 하는가?
  2. 컴포넌트를 나누는 이유 고민하기 : 컴포넌트로 분리하면 실제로 복잡도를 낮추는가? 재사용이 가능한 컴포넌트인가?



Storybook


UI 컴포넌트를 독립적으로 분리해서 개별 관리, 테스트를 도와주는 도구

why?

React, Vue, Angular와 같은 컴포넌트 중심 도구는 복잡한 UI를 단순한 컴포넌트로 분해하는 데 도움이 되지만, 큰 프로젝트일수록 수백개의 컴포넌트가 존재하게 된다. 또한 UI가 비즈니스 로직, 인터랙티브 상태 및 앱 컨텍스트에 얽혀 있기 때문에 디버깅하기가 어렵다는 것이 문제를 더욱 복잡하게 만든다.
그리고 개발자는 수많은 UI 변형을 고려해야 하지만 이를 모두 개발하거나 정리할 수 있는 리소스가 부족하다. 결국 UI를 구축하기가 더 어렵고, 작업 만족도가 떨어지는 상황에 처할 수 있다.

이에대한 해결책으로 UI를 분리하여 빌드하는 방법이 있다.
스토리북은 앱 비즈니스 로직 및 컨텍스트의 간섭 없이 컴포넌트를 렌더링할 수 있는 격리된 iframe을 제공한다. 이를 통해 컴포넌트의 각 변형, 심지어 도달하기 어려운 엣지 케이스까지 개발에 집중할 수 있다.

story란

스토리는 UI 컴포넌트의 렌더링된 상태를 캡처합니다.
개발자는 컴포넌트가 지원할 수 있는 모든 상태를 설명하는 여러 개의 스토리를 하나의 컴포넌트에 작성할 수 있다. (스토리에 대한 기준은 개인 or 팀 취향)



스토리북 사용하기

설치

npx storybook@latest init

시작

npm run storybook

스토리 파일 만들기

// 따로 스토리 폴더를 분리하거나 해당 컴포넌트와 같은 위치에 생성
Button.js | ts | jsx | tsx | vue | svelte
Button.stories.js | ts | jsx | tsx


기능

Toolbar

🔍 Zooming, 🖼 Background, 📐 Grid, 📏 Measure, 🎚️ Outline,📱 Viewport

Docs

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

Addon

스토리북의 핵심 기능을 확장하는 플러그인

  • Controls : 입력과 동적으로 상호 작용하여 컴포넌트의 다양한 구성을 실험할 수 있다.
  • Actions : 콜백을 통해 상호작용이 올바른 출력을 생성하는지 확인할 수 있다. 수동으로 테스트할때 유용하다.
  • Interactions : 재생 버튼으로 인터랙션 테스트를 디버깅할 수 있다.
  • Links : 스토리를 함께 연결할 수 있다. 이를 통해 스토리북에서 직접 데모 및 프로토타입을 구축할 수 있다.
  • Console : 스토리북의 로그, 오류 및 경고를 콘솔에 출력한다.


스토리 작성하기

// 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/



Workflow

Component-Driven Development 와 동일하다.

  1. define behavior flow/possible states
  2. Build each component in isolation and write stories for its variations.
  3. Compose small components together to enable more complex functionality.
  4. Assemble pages by combining composite components.
  5. Integrate pages into your project by hooking up data and business logic.

핵심은... 앱 설계정에 무조건 스토리 정의하는 것. 프로젝트 설계가 더뎌지지만 atomic한 설계의 장점을 가져갈 수 있음.



장점

  • 컴포넌트 개별 정돈 편리, 독립된 환경에서 개발
  • 큰 오픈소스 생태계와 다양한 addon들
  • 컴포넌트 별 라이브러리화 해서 브라우징 가능
  • 깔끔화, 모듈화된 view의 강제성으로 클린코드와 재사용성의 증가 => 효율적 업무 가능
  • 재사용성을 고려한 디자인 & 개발 가능
  • 앱과 별개로 배포 가능, 기획과 업무의 진행이 빨라짐

실제로 사용해보니까...

  1. 결과물을 쉽고 빠르게 확인할 수 있다.
    컴포넌트를 작성할때 이미 존재하는지 쉽게 확인할 수 있다.
    또한 특정 환경일때만 확인할 수 있는 컴포넌트(like 신고, 차단)에 대해서 따로 변수변경이나 처리없이 스토리북을 통해 컴포넌트를 수정할 수 있다.

  2. 좀 더 나은 컴포넌트 설계를 고민하게 된다.
    코드를 작성할 때는 제대로 설계했다고 생각하지만, 나중에 수정이나 재사용을 하려고 하면 설계의 문제점을 발견해서 리팩토링을 하게된다. 그런데 컴포넌트를 구현하면서 스토리 작성을 병행하면 스토리로 옮기는 게 예상대로 되지 않을 때(초반부) 설계의 결함을 발견할 수 있다. 그래서 스토리 작성을 위해 고민을 하다보면 자연스럽게 설계에 대한 고민과 리팩토링으로 이어진다.

profile
프론트엔드 개발자

2개의 댓글

comment-user-thumbnail
2023년 10월 18일

Headless 그림이 귀여워요~!

1개의 답글