API design approach

API design approach (API 설계 방식)

Material UI가 v1 재작성을 통해 어떻게 컴포넌트 API를 재설계했는지, 그 설계 원칙과 규칙을 정리한 문서입니다. 컴포넌트를 조합하고 props를 설계할 때의 기준을 알 수 있어요.

출처: 문서

본문

Material UI가 어떻게 사용되는지에 대해 많은 것을 배웠고, v1 재작성은 컴포넌트 API를 완전히 다시 생각할 기회가 되었습니다.

API design is hard because you can make it seem simple but it's actually deceptively complex, or make it actually simple but seem complex. @sebmarkbage

Sebastian Markbage가 지적한 것처럼, 잘못된 추상화보다는 추상화가 없는 편이 낫습니다. 우리는 조합(composition) 능력을 최대화하기 위해 저수준 컴포넌트를 제공합니다.

조합 (Composition)

컴포넌트를 조합하는 방식에 따른 API의 일관성이 다소 부족하다는 걸 눈치채셨을 거예요. 투명성을 위해, 우리는 API를 설계할 때 다음 규칙을 사용해 왔습니다.

  1. children prop을 사용하는 것은 React에서 조합을 하는 관용적인 방식입니다.
  2. 때로는 자식 순서의 치환을 허용할 필요가 없는 경우처럼, 제한된 자식 조합만 필요한 경우도 있습니다. 이 경우 명시적인 props를 제공하는 것이 구현을 더 단순하고 성능 좋게 만듭니다. 예를 들어 Tab은 icon과 label prop을 받죠.
  3. API 일관성이 중요합니다.

규칙 (Rules)

위의 조합 트레이드오프 외에도, 우리는 다음 규칙을 적용합니다.

Spread

컴포넌트에 제공되는 props 중 명시적으로 문서화되지 않은 것은 루트 요소에 spread됩니다. 예를 들어 className prop은 루트에 적용됩니다.

이제 MenuItem의 ripple 효과를 끄고 싶다고 해 봅시다. spread 동작을 활용하면 됩니다.

<MenuItem disableRipple />

disableRipple prop은 이렇게 흐릅니다: MenuItem > ListItem > ButtonBase.

Native properties

우리는 DOM이 지원하는 className 같은 네이티브 속성을 문서화하지 않습니다.

CSS Classes

모든 컴포넌트는 스타일을 커스터마이즈하기 위해 classes prop을 받습니다. classes 설계는 두 가지 제약을 충족합니다: classes 구조를 가능한 한 단순하게 유지하면서도, Material Design 가이드라인을 구현하기에 충분하게 하는 것이죠.

  • 루트 요소에 적용되는 클래스는 항상 root라고 부릅니다.
  • 모든 기본 스타일은 하나의 클래스로 묶입니다.
  • 루트가 아닌 요소에 적용되는 클래스는 요소의 이름을 접두사로 갖습니다. 예를 들어 Dialog 컴포넌트의 paperWidthXs가 그렇죠.
  • boolean prop으로 적용되는 variant는 접두사가 붙지 않습니다. 예를 들어 rounded prop으로 적용되는 rounded 클래스가 그렇습니다.
  • enum prop으로 적용되는 variant는 접두사가 붙습니다. 예를 들어 color="primary" prop으로 적용되는 colorPrimary 클래스가 그렇죠.
  • variant는 한 단계의 specificity를 가집니다. color와 variant prop은 variant로 간주됩니다. 스타일 specificity가 낮을수록 오버라이드하기 쉬워요.
  • 우리는 variant 수정자를 위해 specificity를 높입니다. :hover, :focus 같은 의사 클래스에 대해서는 이미 그래야만 했죠. 이는 더 많은 보일러플레이트를 대가로 훨씬 많은 제어를 가능하게 합니다. 다행히도 더 직관적이기도 해요.
const styles = {
  root: {
    color: green[600],
    '&$checked': {
      color: green[500],
    },
  },
  checked: {},
};

중첩 컴포넌트 (Nested components)

컴포넌트 내부의 중첩 컴포넌트는 다음을 갖습니다.

  • 최상위 컴포넌트 추상화의 핵심이 되는 경우 자체 평탄화된(flattened) props. 예를 들어 Input 컴포넌트의 id prop이 그렇죠.
  • 사용자가 내부 렌더 메서드의 하위 컴포넌트를 조정해야 할 수 있을 때 자체 xxxProps prop. 예를 들어 내부적으로 Input을 사용하는 컴포넌트에 inputProps와 InputProps를 노출하는 경우가 그렇습니다.
  • 컴포넌트 주입을 수행하기 위한 자체 xxxComponent prop.
  • 명령형 액션을 수행해야 할 때 자체 xxxRef prop. 예를 들어 Input 컴포넌트의 네이티브 input에 접근하기 위해 inputRef prop을 노출하는 경우가 있습니다. 이는 "DOM 요소에 어떻게 접근하나요?"라는 질문에 답하는 데 도움이 됩니다.

Prop 네이밍 (Prop naming)

  • Boolean
    • boolean prop의 기본값은 false여야 합니다. 이렇게 해야 더 좋은 축약 표기법을 쓸 수 있어요. 기본적으로 활성화된 input의 예를 생각해 봅시다. 이 상태를 제어하는 prop의 이름은 어떻게 지어야 할까요? disabled라고 불러야 합니다.

      ❌ <Input enabled={false} />
      ✅ <Input disabled />
      
    • boolean의 이름이 단어 하나라면, 동사보다는 형용사나 명사여야 합니다. props는 _액션_이 아니라 _상태_를 설명하기 때문이죠. 예를 들어 input prop은 상태에 의해 제어될 수 있는데, 이는 동사로는 설명할 수 없습니다.

      const [disabled, setDisabled] = React.useState(false);
      
      ❌ <Input disable={disabled} />
      ✅ <Input disabled={disabled} />
      

제어 컴포넌트 (Controlled components)

대부분의 제어 컴포넌트는 value와 onChange props에 의해 제어됩니다. 또한 open / onClose / onOpen 조합이 관련 상태를 표시하는 데 사용됩니다. 이벤트가 더 많은 경우에는 명사가 먼저 오고 동사가 그 뒤에 옵니다—예를 들어 onPageChange, onRowsChange처럼요.

:::info

  • 컴포넌트가 props를 사용해 부모에 의해 관리되면 제어(controlled) 상태입니다.
  • 컴포넌트가 자체 로컬 state로 관리되면 비제어(uncontrolled) 상태입니다.

제어/비제어 컴포넌트에 대해 더 배우려면 React 문서를 참고하세요. :::

boolean vs. enum

컴포넌트의 변형을 위한 API를 설계하는 방법은 두 가지가 있습니다: _boolean_을 쓰거나 _enum_을 쓰는 것이죠. 예를 들어 서로 다른 타입을 가진 버튼이 있다고 해 봅시다. 각 옵션에는 장단점이 있어요.

  • Option 1 boolean:

    type Props = {
      contained: boolean;
      fab: boolean;
    };
    

    이 API는 축약 표기법을 지원합니다: <Button>, <Button contained />, <Button fab />처럼요.

  • Option 2 enum:

    type Props = {
      variant: 'text' | 'contained' | 'fab';
    };
    

    이 API는 더 장황합니다: <Button>, <Button variant="contained">, <Button variant="fab">.

    하지만 잘못된 조합이 사용되는 것을 막아 주고, 노출되는 props의 수를 제한하며, 미래에 새 값을 쉽게 지원할 수 있게 해 줍니다.

Material UI 컴포넌트는 다음 규칙에 따라 두 접근 방식을 조합해 사용합니다.

  • 2개의 가능한 값이 필요할 때는 _boolean_을 사용합니다.
  • > 2개의 가능한 값이 필요하거나, 미래에 추가 가능한 값이 생길 여지가 있을 때는 _enum_을 사용합니다.

앞선 버튼 예시로 돌아가면, 3개의 가능한 값이 필요하므로 _enum_을 사용합니다.

Ref

ref는 루트 요소로 전달됩니다. 즉 component prop으로 렌더링되는 루트 요소를 바꾸지 않는 한, 컴포넌트가 렌더링하는 가장 바깥쪽 DOM 요소에 전달된다는 뜻이에요. component prop으로 다른 컴포넌트를 넘기면, ref는 그 컴포넌트에 붙습니다.

용어집 (Glossary)

  • host component: react-dom의 맥락에서의 DOM 노드 타입. 예를 들어 'div'가 그렇죠. React Implementation Notes도 참고하세요.
  • host element: react-dom의 맥락에서의 DOM 노드. 예를 들어 window.HTMLDivElement 인스턴스가 그렇습니다.
  • outermost: 컴포넌트 트리를 위에서 아래로 읽을 때 첫 번째 컴포넌트, 즉 너비 우선 탐색(breadth-first search)에서의 첫 컴포넌트를 말해요.
  • root component: host component를 렌더링하는 가장 바깥쪽 컴포넌트.
  • root element: host component를 렌더링하는 가장 바깥쪽 요소.

더 알아보기 (Learn more)