본문 바로가기

FrontEnd

디자인 시스템 컴포넌트를 만들 때 고려할 사항들

반응형

원문 : building-a-design-system-from-scratch/

 

Building a Design System from scratch - Maxime Heckel's Blog

A deep dive into my experience building my own design system that documents my process of defining tokens, creating efficient components, and shipping them as a package.

blog.maximeheckel.com

지속 가능한 UI를 구축하기 위한 헥심은 열쇠는

색상과 타이포그래피에 대한 명확한 표준을 설정하고

재사용 가능한 컴포넌트를 구축하는 것입니다.

 

전체 코드 보기 : @maximeheckel/design-system

왜 디자인 시스템을 구축하나요?

  • 브랜딩
    • FE 엔지니어로 자신을 브랜딩
    • 회사의 아이덴티티
  • 일관성
  • 재미/학습
    • 디자인 시스템적 사고 방식
    • 컴포넌트 구축 역량

토큰

 
토큰은 디자인 시스템의 기초를 형성하는 색상 팔레트, 간격 단위, 그림자 또는 타이포그래피와 같은 스타일의 개별 요소입니다.
디자인 시스템 작업을 시작할 때 서로 다른 프로젝트를 가장 기본적인 부분으로 나누는 것이 필수적이었습니다.

컬러 시스템

(two-tier color variable system) 2계층 색상 가변 시스템
  • 첫 번째 레이어는 같은 팔레트 내 다양한 ​​색상의 HSL(Hue, Saturation, Lightness) 값을 나타내는 일련의 변수입니다. 
    • --blue-10: 222, 89%, 90% 또는 --red-60: 0, 95%, 40%.
  • 두 번째 레이어는 디자인 시스템의 구성 요소에서 참조하게 될 색상에 대한 일반적인 별칭입니다.
    • --brand: hsl(var(--blue-50)) 또는 --foreground: hsla(var(- -gray-05), 60%).
    • 이 레이어에서는 첫 번째 레이어에서 정의한 색상을 사용하여 구성하거나 확장합니다.

의존성의 방향은 ->->

2계층 변수 시스템을 보여주는 다이어그램: --brand color는 색상 자체를 참조하면서 버튼의 배경색으로 사용됩니다.
컴포넌트는 실제 "색상"을 참조하는 것으로 끝나지 않습니다.
  • 컴포넌트는 변경에 더 탄력적입니다.
    • Button 구성 요소의 배경색은 --blue-10이 아니라 --brand이며 해당 변수의 값은 시간이 지남에 따라 파란색에서 보라색 또는 다른 것으로 진화할 수 있습니다. 
  • 브랜드 색상을 변경하고 싶으십니까?
    • --brand 변수의 값을 업데이트하기만 하면 이를 참조하는 모든 컴포넌트가 그에 따라 업데이트됩니다. 불투명도를 추가하는 것과 같이 색상 토큰을 구성할 수 있습니다
  • 라이트 모드와 다크 모드와 같은 테마를 쉽게 구축할 수 있습니다.
    • 라이트 모드에서 --brand는 --blue-60을 참조할 수 있고
    • 다크 모드에서는 --blue-20을 참조할 것입니다.

1. 베이스 색상을 고르고

 


2. 밝기를 증가/감소시켜 각 색상에 대한 스케일을 생성합니다.

 


3단계: 라이트 모드와 다크 모드에 대한 색상 하위 집합을 선택하고 이름을 지정합니다.

 

다른 토큰들

스페이싱

--space-0: 0px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 40px;
--space-8: 48px;
--space-9: 56px;
--space-10: 64px;
--space-11: 80px;
--space-12: 96px;

글꼴 관련 토큰

--font-size-1: 0.75rem;
--font-size-2: 0.875rem;
--font-size-3: 1rem;
--font-size-4: 1.125rem;
--font-size-5: 1.25rem;
--font-size-6: 1.5rem;
--font-size-7: 2rem;

반지름 토큰

--border-radius-0: 4px;
--border-radius-1: 8px;
--border-radius-2: 16px;
위 세 종류의 토큰들은 변경될 일이 별로 없어 직접 참조합니다.

 

네이밍

다음은 내가 직접 따랐던 몇 가지 규칙을 요약한 것입니다.

  • "크기 관련" 토큰 세트의 경우 1, 2, ..., 12와 같이 1씩 증가하는 숫자 접미사를 사용하도록 선택합니다.
  • 색상 스케일과 같이 앞으로 좀 더 추가할 세분성이 필요할 수 있는 토큰의 경우 10씩 증가하는 숫자 접미사를 선택했습니다.

Lesson Learned

디자인 토큰도 진화합니다.
 
컴포넌트와 공통 패턴을 번갈아 개발하면서,
종종 드로잉 보드로 돌아가 새 토큰을 정의하고, 다른 토큰을 재정의/정제하거나, 일부를 결합 및 삭제해야 했습니다.
이 과정은 다음과 같이 나에게 특히 지루했습니다.
  • 디자이너가 없었습니다.
    • 디자인을 직감이나 시행 착오에만 의존할 수 있었습니다.
  • 가능한 한 많은 토큰을 포함하려 했습니다.
    • 디자인 시스템의 복잡성과 일관성 수준 사이의 균형을 유지하는 것은 어려웠습니다.
지금까지 만든 토큰들은 컴포넌트의 수를 확장하거나 새로운 색상 또는 변수를 정의하는 새로운 방법을 실험하면서,
미래에 발전할 가능성이 가장 큽니다.
 
 
단단한 기반보다는 디자인 시스템의 변경 가능한 레이어로 보는 것이 좋습니다.


Component patterns

현재 내 디자인 시스템에는 단순한 컴포넌트와 기본 값들만 포함되어 있습니다.
내게 필요한 것은 레고 키트와 같이 창의성을 위한 약간의 공간을 여전히 허용하면서
일관성을 유지하면서 더 빠르게 만들 수 있는 간단한 부품 세트뿐입니다.
따라서 다음과 같은 균형을 유지하기 위해 이 프로젝트를 최적화했습니다.
  • 좋은 개발자 경험(DX)
    • 컴포넌트가 유용하고 더 빠르게 작업하고 실험하고 반복하는 데 도움이 되도록 합니다
  • 응집력 있는 디자인/디자인 언어
    • 컴포넌트를 코드 측면뿐만 아니라 시각적으로도 합성할 수 있습니다.

Variant driven components

@stitches/react를 사용합니다. 높은 수준의 DX를 제공합니다.
  • The variant-driven approach
    • Stitches는 variant의 사용을 강조합니다.
    • 해당 컴포넌트가 지원하는 variant 세트는 미리 정의되어야 합니다.
      • 즉, 스타일링에 동적 props는 허용되지 않습니다.
        • 저는 디자인 시스템에서 작업할 때 이 패턴을 크게 신뢰합니다. 개발자 경험과 컴포넌트의 인터페이스에 대해 정말로 생각하게 만듭니다.
      • 합성, 컴파운드 컴포넌트의 이점을 누리기 위해 variant의 수를 관리합니다.
  • 다형성 지원
    • 다형성(polymorphic as props) as prop을 통해 태그를 재정의 할 수 있습니다.
  • 고급 Typescript 지원
    • variatne는 자동으로 타입과 함께 제공됩니다. 추가 작업이 필요하지 않습니다.

variant를 이용한 예제입니다.

import { styled } from '@stitches/react';

const Block = styled('div', {
    borderRadius: 8px;
    height: '50px';
    width: '100%';
    display: 'flex';
    justifyContent: 'center;
    alignItems: 'center';

    variants: {
        /* the appearance prop will be automatically typed as 'primary' | 'secondary' */
        appearance: {
            'primary': {
                background: 'blue';
                color: 'white';
            },
            'secondary': {
                background: 'hotpink';
                color: 'white';
            }
        }
    }

    /* specifying a default variant will make the appearance prop optional */
    defaultVariant: {
        appearance: 'primary';
    }
});


const App = () => {
    return (
        <Block as="section" appearance="secondary">
            Styled-components
        </Block>
    )
}​

컴포넌트 개발 시 반복하는 하나의 패턴은

transiton 및 hover/focus/active 상태를 처리하기 위해 로컬 CSS 변수에 의존하는 것이었습니다.

아래의 코드로 해결해 봅시다.

import { styled } from '@stitches/react';

const StyledButton = styled('button', {
  /* Initializing local variables first and assigning them default values */
  background: 'var(--background, white)',
  color: 'var(--color, black)',
  boxShadow: 'var(--shadow, none)',
  opacity: 'var(--opacity, 1)',
  transform: 'scale(var(--button-scale, 1)) translateZ(0)',

  /* Main styles of the component */
  padding: 'var(--space-3) var(--space-4)',
  fontSize: 'var(--font-size-2)',
  fontWeight: 'var(--font-weight-3)',
  height: '44px',
  width: 'max-content',
  transition: 'background 0.2s, transform 0.2s, color 0.2s, box-shadow 0.3s',
  borderRadius: 'var(--border-radius-1)',

  /* Update local variables based on state/variant */
  '&:active': {
    '--button-scale': 0.95,
  },

  '&:disabled': {
    '--background': 'var(--form-input-disabled)',
    '--color': 'var(--typeface-tertiary)',
  },

  '&:hover': {
    '&:not(:disabled)': {
      '--shadow': 'var(--shadow-hover)',
    },
  },
  '&:focus-visible': {
    '--shadow': 'var(--shadow-hover)',
  },

  variants: {
    variant: {
      primary: {
        '--background': 'var(--brand)',
        '--color': 'var(--typeface-primary)',
      },
      secondary: {
        '--background': 'var(--brand-transparent)',
        '--color': 'var(--brand)',
      },
    },
  },
});
위의 스니펫에서 다음을 확인할 수 있습니다.
  • 해당 컴포넌트에서 사용되는 로컬 변수는 맨 위에 있습니다. 여기에서 기본값으로 초기화합니다.
  • 다음엔 모든 주요 CSS 속성을 포함하는 CSS의 본문을 작업합니다.
  • 다음엔 모든 중첩 코드, variant, selector가 위치합니다. ::before 또는 ::after 문은 해당 CSS 변수만 재할당합니다.
결과 코드는 훨씬 읽기 쉽고, 유지 관리 가능성의 포기 없이 더 복잡한 CSS 코드를 실험할 수 있습니다.
Stitches의 테마와 변수 시스템을 활용하고 있지 않다는 것을 알 수 있습니다.
내 의도는 내 코드를 어떤 프레임워크/라이브러리와도 독립적으로 만드는 것이었습니다.

Utility components (유틸리티 컴포넌트)

이 디자인 시스템의 목적은 더 빠른 작업/실험을 가능하게 하는 것이기 때문에,

유틸리티 컴포넌트 세트를 생각해 냈습니다.

이러한 컴포넌트의 범위는 다음과 같습니다.

 

BOX

디자인 시스템의 기본 컴포넌트입니다. Stitches css 소품을 지원하는 향상된 div로 주로 사용하는 빈 셸입니다.
여러 파일을 편집할 필요 없이 빠르게 프로토타이핑할 때 유용합니다.
import { styled } from '@stitches/react';

const Box = styled('div', {});

/* Usage with `css` prop on the fly */

const App = () => {
    return (
        <Box
            css={{
                background: 'var(--brand-transparent)';
                color: 'var(--typeface-primary)';
                borderRadius: 'var(--border-radius-1)';
                width: 100,
                height: 100,
            }}
        />
    )
}

Flex & Grid

레이아웃 유틸리티 컴포넌트입니다.
플렉스 및 그리드 CSS 레이아웃을 빠르게 만드는 것을 목표로 합니다.
alignItems, justifyContent, gap 또는 columns와 같은
고유한 속성을 설정하는 데 도움이 되는 사전 정의된 variant/prop이 함께 제공됩니다.
디자인 시스템을 사용하는 코드베이스에서 생명의 은인이 되었습니다.
복잡한 레이아웃 프로토타입을 순식간에 만들 수 있습니다.
const App = () => {
  return (
    <>
      <Flex
        alignItems="center"
        direction="column"
        justifyContent="center"
        gap="2"
      >
        <Box css={...} />
        <Box css={...} />
      </Flex>
      <Grid columns="2" gap="4">
        <Box css={...} />
        <Box css={...} />
        <Box css={...} />
        <Box css={...} />
      </Grid>
    </>
  );
};

 

Text

타이포그래피와 관련된 모든 것을 관리하는 것은 항상 어려운 일이었습니다.
이 문제를 해결하기 위해 이 유틸리티 컴포넌트를 만들었습니다.
크기, 색상, 무게 및 여러 번 생명의 은인이었던 자르기 또는 ✨그라데이션✨과 같은 깔끔한 작은 유틸리티 소품에 대한 전용 변형이 있습니다.
이 컴포넌트를 매일 사용하는 것에 감사하며 그 위에 더 많은 특정 타이포그래피 컴포넌트를 구성하게 되었습니다.
 
const App = () => {
  return (
    <>
      <Text outline size="6">
        Almost before we knew it,
        we had left the ground.
      </Text>
      <Text truncate>
        Almost before we knew it,
        we had left the ground.
      </Text>
      <Text
        gradient
        css={{
          backgroundImage: 
            'linear-gradient(...)',
        }}
        size="6"
        weight="4"
      >
        Almost before we knew it,
        we had left the ground.
      </Text>
    </>
  );

VisibilityHidden

https://www.joshwcomeau.com/snippets/react-components/visually-hidden/

요소를 시각적으로 숨기는 CSS는 기억하기가 매우 어렵습니다.
그래서 자주 구글링할 필요가 없도록 컴포넌트를 만들었습니다 😄.
필요할 때 더 많은 컨텍스트를 가질 수 있는 요소에 보조 기술에 대한 추가 텍스트를 추가하는 데 도움이 됩니다.
원문에 예제가 없어서 다른 곳의 링크로 대체함

Compound components

나는 compound components를 좋아합니다.

좋은 컴파운드 컴포넌트 세트를 생각해내는 것이

컴포넌트의 DX를 크게 향상시킬 수 있다고 믿습니다.

 

컴파운드 컴포넌트를 선택한 두 가지 사용 사례가 있습니다.
  • 더 작은 컴포넌트로 만들지 않으면 props가 오버로드(중복)되는 경우
  • 컴포넌트를 여러 방법으로 합성할 수 있는 경우
 
컴파운드 컴포넌트 패턴을 할용하는 몇가지 예시는 다음과 같습니다.

Radio

<Radio.Group name="options" direction="vertical" onChange={...}>
  <Radio.Item
    id="option-1"
    value="option1"
    aria-label="Option 1"
    label="Option 1"
  />
  <Radio.Item
    id="option-2"
    value="option2"
    aria-label="Option 2"
    label="Option 2"
    checked
  />
</Radio.Group>

Card

<Card>
  <Card.Header>Title of the card</Card.Header>
  <Card.Body>Content of the card</Card.Body>
</Card>

컴파운드 컴포넌트 중 일부는 자식으로 렌더링할 수 있는 컴포넌트 타입과 관련하여 다른 컴포넌트보다 더 제한적입니다.
 
카드의 경우 사용을 "게이트"하고 싶지 않았기 때문에 유연성을 선택했습니다.
 
그러나 Radio의 경우 사용법을 처방할 필요를 느꼈고 이를 위해 다음과 같은 작은 유틸리티를 만들었습니다.
export function isElementOfType(element, ComponentType): element {
  return element?.type?.displayName === ComponentType.displayName;
}​
이 함수를 사용하면 자식의 displayName을 기반으로 Radio에서 렌더링된 컴포넌트를 필터링할 수 있습니다.
import RadioItem from './RadioItem';

const RadioGroup = (props) => {
  const { children, ... } = props;

  const filteredChildren = React.Children.toArray(children).filter((child) =>
    isElementOfType(child, RadioItem);
  );

  return (
    <Flex gap={2} role="radiogroup">
      {filteredChildren}
    </Flex>
  )
}

다형성과 합성 (as를 이용한 다형성과 합성)

composition을 사용하면
기본 컴포넌트보다 더 적은 수의 prop이 필요하고
사용 사례가 더 특별한 추상 컴포넌트가 생성됩니다.
 
잘하면 개발 속도를 높이고 디자인 시스템을 훨씬 더 쉽게 사용할 수 있습니다.
 
이 디자인 시스템이 만들 수 있는 광범위한 애플리케이션과
이 디자인 시스템의 컴포넌트가 얼마나 원시적인지를 감안할 때
처음부터 합성과 확장성(optimize for composition and extensibility)을 최적화하고 싶었습니다.
 
@stiches/react 라이브러리를 선택하는 것은 as prop을 통해 다형성을 지원하기 때문에 훌륭한 선택임이 입증되었습니다.
as prop을 사용하면 컴포넌트가 렌더링하는 태그를 선택할 수 있습니다.
 
예를 들어 Text와 같은 많은 유틸리티 컴포넌트에서 사용합니다.
// Renders a p tag
<Text as="p">Hello</Text>

// Renders an h1 tag
<Text as="h1">Hello</Text>

as prop에서 모든 HTML 태그를 사용할 수 있을 뿐만 아니라 다른 컴포넌트를 지정할 수 있습니다.
<Card>
  {/* Card.Body inherits the style, the props and the type of Flex! */}
  <Card.Body as={Flex} direction="column" gap="2">
    ...
  </Card.Body>
</Card>

위의 코드 조각은 Flex 컴포넌트로 렌더링한 Card.Body 컴파운드 컴포넌트를 보여줍니다.

이 경우 Card.Body는 스타일을 상속할 뿐만 아니라 props와 type도 상속합니다! 🤯

styled-components 자체도 합성을 지원합니다.
const DEFAULT_TAG = 'h1';

const Heading = () => {
  // Remapping the size prop from Text to a new scale for Heading
  const headingSize = {
    1: { '@initial': '4' },
    2: { '@initial': '5' },
    3: { '@initial': '6' },
    4: { '@initial': '7' },
  };

  // Overriding some styles of Text based on the new size prop of Heading
  const headingCSS = {
    1: {
      fontWeight: 'var(--font-weight-4)',
      lineHeight: '1.6818',
      letterSpacing: '0px',
      marginBottom: '1.45rem',
    },
    2: {
      fontWeight: 'var(--font-weight-4)',
      lineHeight: '1.6818',
      letterSpacing: '0px',
      marginBottom: '1.45rem',
    },
    3: {
      fontWeight: 'var(--font-weight-4)',
      lineHeight: '1.6818',
      letterSpacing: '0px',
      marginBottom: '1.45rem',
    },
    4: {
      fontWeight: 'var(--font-weight-4)',
      lineHeight: '1.6818',
      letterSpacing: '0px',
      marginBottom: '1.45rem',
    },
  };

  return (
    <Text
      as={DEFAULT_TAG}
      {...rest}
      ref={ref}
      size={headingSize[size]}
      css={{
        ...merge(headingCSS[size], props.css),
      }}
    />
  );
};

// Creating a more abstracted version of Heading
const H1 = (props) => <Heading {...props} as="h1" size="4" />;
const H2 = (props) => <Heading {...props} as="h2" size="3" />;
const H3 = (props) => <Heading {...props} as="h3" size="2" />;
const H4 = (props) => <Heading {...props} as="h4" size="1" />;
위 코드는 실제로 실행되는지 나중에 확인해봐야 할 듯...
이를 통해 디자인 시스템의 컴포넌트에서 더 추상적이고 좁은 사용 사례에 초점을 맞춘 컴포넌트를 만들 수 있습니다.

Make it shine!

내 눈에는 전체 시스템의 최종 모양과 느낌이 DX만큼 중요합니다.
나는 더 빨리 만들 뿐만 아니라 더 예쁘게 만들기 위해 이 컴포넌트들을 사용합니다.
 

Packaging and shipping

디자인 시스템의 배포 관련한 내용입니다.

  • 패키징 패턴
  • 파일 구조
  • 빌드, 릴리즈

버저닝

하나의 라이브러리를 구축해야 합니까? 아니면 컴포넌트당 하나의 패키지가 있습니까?
프로젝트가 디자인 시스템을 어떻게 소비할지 생각할 때 유효한 질문입니다.
이 프로젝트 전체에서 단순성을 위해 최적화했기 때문에 전체 디자인 시스템에 대해 하나의 패키지를 선택했습니다.
따라서 이 하나의 라이브러리 버전 관리에 대해서만 걱정하면 됩니다.
그러나 여기에는 한 가지 큰 함정이 있었습니다. 이제 패키지 트리를 흔들 수 있게 만들어야 했기 때문에
디자인 시스템의 한 구성 요소를 가져와도 프로젝트의 번들 크기가 크게 증가하지 않았습니다.
각각의 장단점과 함께 다른 버전 관리/패키징 패턴이 궁금하다면
디자인 시스템 버전 관리: 단일 라이브러리 또는 개별 컴포넌트(Design System versioning: single library or individual components?)를 확인하는 것이 좋습니다. 
 
 
  • major : 중요한 디자인 토큰 변경이 발생하거나 코드의 주요 변경 사항이 발생할 때 
  • minor : 디자인 시스템에 새 컴포넌트 또는 새 토큰이 추가될 때 
  • patch : 기존 컴포넌트/토큰이 업데이트되거나 수정 사항(fix)이 제공될 때 패치

File structure

Delightful React File/Directory Structure를 참고하세요


Bundling

번들링을 위해 esbuild(esbuild)를 선택했습니다.
나는 내 경력 내내 상당한 양의 번들러를 가지고 놀았지만 esbuild의 속도에 근접한 것은 없습니다.
나는 내 전체 디자인 시스템(Typescript 타입 생성 제외)을 겨우 1초 만에 묶을 수 있습니다.
esbuilt 자체에 대한 사전 경험이 많지 않아도 비교적 빠르게 작동하는 구성을 생각해낼 수 있었습니다.
const esbuild = require('esbuild');
const packagejson = require('./package.json');
const { globPlugin } = require('esbuild-plugin-glob');

const sharedConfig = {
  loader: {
    '.tsx': 'tsx',
    '.ts': 'tsx',
  },
  outbase: './src',
  bundle: true,
  minify: true,
  jsxFactory: 'createElement',
  jsxFragment: 'Fragment',
  target: ['esnext'],
  logLevel: 'debug',
  external: [...Object.keys(packagejson.peerDependencies || {})],
};

esbuild
  .build({
    ...sharedConfig,
    entryPoints: ['src/index.ts'],
    outdir: 'dist/cjs',
    format: 'cjs',
    banner: {
      js: "const { createElement, Fragment } = require('react');\n",
    },
  })
  .catch(() => process.exit(1));

esbuild
  .build({
    ...sharedConfig,
    entryPoints: [
      'src/index.ts',
      'src/components/**/index.tsx',
      'src/lib/stitches.config.ts',
      'src/lib/globalStyles.ts',
    ],
    outdir: 'dist/esm',
    splitting: true,
    format: 'esm',
    banner: {
      js: "import { createElement, Fragment } from 'react';\n",
    },
    plugins: [globPlugin()],
  })
  .catch(() => process.exit(1));

다음은 이 구성의 주요 내용입니다.

  • esbuild는 Babel과 같은 JSX 변환 기능이나 플러그인을 제공하지 않습니다. 해결 방법으로 jsxFactory(L13-14) 및 jsxFragment 옵션을 정의해야 했습니다.
  • 같은 메모에서 배너 옵션을 통해 react import/require 문도 추가해야 했습니다.
    • 가장 우아한 것은 아니지만 이것이 내가 이 패키지를 작동하게 할 수 있는 유일한 방법입니다.
  • 이 패키지를 ESM 및 CJS 포맷으로 묶었습니다.
  • ESM은 트리 쉐이킹을 지원하므로 여러 entryPoint(L35-40)를 볼 수 있습니다.
Esbuild의 유일한 목적은 코드를 묶는 것입니다.
타입 정의를 생성하기 위해 tsc 자체에 직접 의존하는 것 외에 다른 선택이 없었습니다.
{
    "scripts": {
        "build": "node ./esbuild.build.js",
        ...
        "postbuild": "yarn ts-types",
        ...
        "ts-types": "tsc --emitDeclarationOnly --outDir dist",
    }
}

이 구성 덕분에 몇 초 만에 디자인 시스템을 위한 트리 셰이킹 가능한 패키지를 생성할 수 있었습니다.
이를 통해 단일 패키지 사용의 가장 큰 단점을 수정할 수 있었습니다.
디자인 시스템에서 무엇을 가져오든 상관없이 가져온 것만 결국 소비자 프로젝트에 번들로 포함됩니다.
// This will make the project's bundle *slightly* heavier
import { Button } from '@maximeheckel/design-system';

// This will make the project's bundle *much* heavier
import { Button, Flex, Grid, Icon, Text } from '@maximeheckel/design-system';

 


참고 : 

나무위키 다크모드 : (나무위키 다크모드)

프로그램 가능하며 사실적인 레이어드 섀도우 시스템 : (The programmatic and realistic layered shadow system)

디자인 시스템 버전 관리: 단일 라이브러리 또는 개별 컴포넌트 : (Design System versioning: single library or individual components?)

반응형