개발자
류준열

UI 시스템 만들게 된 계기

회사에서 랜딩페이지를 자주 업데이트 하고 협업하는 디자이너분은 프로덕트 디자이너가 아닌, 마케팅팀의 브랜드 디자이너였다.

기획의 자유도가 높다보니 컴포넌트의 재사용이 쉽지 않았다.
그렇게 1년을 작업하다가 마케팅팀을 아래 이유로 설득하고 UI 시스템이라 부르는 디자인 시스템을 만들었다.

  • 디자인은 지원버튼 클릭률에 영향을 주지 않음
  • 각 부트캠프마다 디자인이 다르기 때문에 유저로 하여금 브랜드 통일성을 느낄 수 없음(Toss, Naver가 일관된 톤을 유지하는 것을 예시로 듬)

Polymorphic Component 구현

동료분이 Polymorphic 컨셉을 제안해 주셔서 모두 이 글을 읽고, 동료분의 주도하에 Polymorphic 컴포넌트가 탄생하였다.

Polymorphic 컴포넌트를 사용한 이유는 styled-component의 as Props가 Poylnorphic 컴포넌트의 컨셉에 딱 들어맞았기 때문이다.

Polymorphic 컴포넌트는 as Props를 통해 무엇이든 될 수 있는 컴포넌트인데 당시 동료분이 만드셨던 Text.tsx 는 다음과 같다.

    // p태그
    <Text as="p" size={['body5', 'body4', 'body3']} fontWeight="bold" color="white10">
          {title}
    </Text>

반응형

협의한 반응형 스타일만 사용하면서 개발 시간이 크게 단축되었고, UI도 통일시킬 수 있었다.

before

디자인 시스템을 사용하기 이전에는 다음과 같이 아주 자유도 높은 디자인을 미디어 쿼리를 통해 작성했었다.

    export const NewMQ = {
      MOBILE: `@media only screen and (max-width: ${NewBreakPoints.MOBILE.maxPx}px)`,
      TABLET: `@media only screen and (min-width: ${NewBreakPoints.TABLET.minPx}px)`,
      DESKTOP: `@media only screen and (min-width: ${NewBreakPoints.DESKTOP.minPx}px)`
    };
    
    const MainPhraseUnit = styled.span`
      font-style: normal;
      font-weight: bold;
      font-size: 24px;
      line-height: 36px;
      letter-spacing: -0.025em;
      
      ${NEWMQ.desktop} {
        font-size: 28px;
        line-height: 42px;
      }
    `;
after

디자인 시스템에는 정해진 사이즈를 배열[mobile,tablet,desktop]에 할당하면서 개발 시간이 크게 단축할 수 있었다.

다음은 mobile에는 font-size: 14px, tablet에서는 font-size: 16px, desktop에서는 font-size: 18px으로 스타일을 할당한 p 태그이다.

    export const FONT_SIZE = {
      // content typo
      ...
      body3: 'font-size: 18px; line-height: 140%; letter-spacing: 0;',
      body4: 'font-size: 16px; line-height: 140%; letter-spacing: 0;',
      body5: 'font-size: 14px; line-height: 140%; letter-spacing: 0;',
      ...
    }
    
    <Text as="p" size={['body5', 'body4', 'body3']} fontWeight="bold" color="white10">
          {title}
    </Text>

타입

타입스크립트의 목적은 심플하다. 들어오면 안되는 타입을 개발단계에서 차단하여 관련 버그를 예방하는 것.
타입으로 제품의 안정성을 확보할 수 있기 때문에 대충할 수 없는 영역이고 때로는 골치가 아프다.

Font Style 타입

일단 Text 컴포넌트에서 이용할 스타일은 font-size, font-weight, color 이다.

    export type FontSize = keyof typeof FONT_SIZE;
    export type FontWeight = keyof typeof FONT_WEIGHT;
    export type lineHeight = `${number}${'px' | '%'}`;
    
    type _TextProps = {
      size: FontSize | FontSize[];
      fontWeight?: FontWeight;
      color?: Color;
    };
as

as Props를 위한 타입은 간단하다.

    export type AsProp<T extends React.ElementType> = {
      as?: T;
    };
Ref를 할당할 수 있는 PolymorphicComponentProps Type

ref를 props로 주고 받으려면 forwardRef로 컴포넌트를 호출해야한다. 이 과정에서 forwardRef와 타입이 꼬이지 않게 ref 타입을 명시해주어야 한다.

일단 React.ComponentPropsWithRef<T>['ref']를 통해 정확한 ref 타입을 가져온다. (제너릭 T는 ElementType에 해당하는 것만 할당 가능)

    // react component의 ref 를 PolymorphicRef에 할당. 제너릭 T는 ElementType에 해당하는 것만 할당 가능
    export type PolymorphicRef<T extends React.ElementType> = React.ComponentPropsWithRef<T>['ref'];

이렇게 PolymorphicRef{ref?: PolymorphicRef<T>;}에 할당하고 AsProps,ComponentPropsWithoutRef, 제너릭 Props 와 합친다.

    export type PolymorphicComponentProps<T extends React.ElementType, Props = {}> = AsProp<T> &
      React.ComponentPropsWithoutRef<T> &
      Props & {
        ref?: PolymorphicRef<T>;
      };
최종 타입

위에서 만든 모든 타입을 TextComponent Type 으로 묶어준다.

    type _TextProps = {
      size: FontSize | FontSize[];
      fontWeight?: FontWeight;
      color?: Color;
    };
    export type TextProps<T extends React.ElementType> = PolymorphicComponentProps<T, _TextProps>;
    type TextComponent = <T extends React.ElementType = 'span'>(props: TextProps<T>) => React.ReactNode | null;
    
    export const Text: TextComponent = forwardRef(
      <T extends React.ElementType = 'span'>(
        { size, color = 'black10', as, children, className, ...props }: TextProps<T>,
        ref: PolymorphicRef<T>['ref']
      ) => {
        const isFontSize = typeof size === 'string' ? Object.keys(FONT_SIZE).includes(size) : true;
    
        return (
          <StyledText
            className={cn({ [size as string]: !isFontSize }, className)}
            size={size}
            color={color}
            ref={ref}
            as={as as React.ElementType}
            {...props}
          >
            {children}
          </StyledText>
        );
      }
    );

섹션 템플릿화

각 부트캠프 섹션들이 분명 비슷한데도 디테일이 상이하여 결국에는 재사용하지 못하고 매번 새로 만드는 일이 비일비재 했고 이 또한 개선하였다.

ex) 디테일이 상이한 커리큘럼 섹션 curriculums

섹션들을 템플릿화 하고 나서 개발, 디자인 시간이 크게 단축되었고 각 부트캠프의 UI도 통일되어 더욱 일관성있는 브랜딩을 가져갈 수 있었다. be blockchain uxui

정리

UI 시스템의 특징은 크게 두가지이다.

  • Polymorphic 컨셉으로 만든 Text.tsx
  • 자주쓰이는 섹션 템플릿 화

위 두가지를 통해 스타일 작성 시간을 크게 단축할 수 있었고, 디자이너분도 디자인 설계하는데 시간이 크게 단축되었다고 하셨다.

서로가 서로를 배려하다보니 아무 발전도 하지 못한 지난 1년이었다. 갈등은 더 심각한 갈등을 피하기 위한 것이다. 갈등이 없으면 아무 발전도 없다. 라는 것을 몸소 느꼈다.