확장성 있는 Text 컴포넌트
1 | <Text typography="h1">나는 머릿말 1이야</Text> |
위와 같은 컴포넌트를 만들기 위한 작업을 시작해보자.
컴포넌트 재사용을 높이기 위해서는 누구나 이해하기 쉽게 작성하는 것이 좋다. 그러기 위해서는 선언형 코드인 HTML 방식을 따르는 것이 이득이다.
HTML 처럼 만든 컴포넌트
1 | type Props = HTMLAttributes<"span">; |
이런식으로만 사용해도 HTML Element를 똑같이 따라할 수 있다.
하지만 HTML 속성은 ref, key 같은 리액트 속성들을 없다.
이 때 사용하는 것이 forwardRef 함수로 ref 값은 전달해준다.
1 | const Text = forwardRef(function Text( |
ComponentPropsWithoutRef<"span">
은 ref만 제외한 나머지 리액트 프로퍼티를 모두 포함한다.
커스텀 프로퍼티 추가하기
지금까지의 컴포넌트는 그저 span element와 동일하다. 다른 커스텀 프로퍼티도 받을 수 있도록 타입스크립트의 & (intersection)을 사용하여 타입을 확장해보자.
1 | type TextProps = { |
하지만, ComponentPropsWithoutRef<"span">
가 이미 갖고 있는 프로퍼티와 커스텀 프로퍼티가 동일한 경우 대응하기 어렵다.
1 | type TextProps = { |
예를 들어 위와 같은 커스텀 프로퍼티는 ComponentPropsWithoutRef<"span">
와 동일한 프로퍼티가 없기 때문에 제대로 타입이 추론된다.
1 | type TextProps = { |
하지만 id 프로퍼티는 ComponentPropsWithoutRef<"span">
가 소유하고 있어 동일한 프로퍼티 2개가 생기게 되므로 타입스크립트가 제대로 추론하지 못하고 undefined
로 추론된다.
이를 해결하기 위해서는 Omit 타입을 사용하여 오버라이딩하려는 프로퍼티를 먼저 제거한 후 병합해야지만 문제가 없다.
1 | type Combine<T, K> = T & Omit<K, keyof T>; |
- Combine 유틸타입은 2개의 타입을 받은 후 K타입에서 T 타입이 가진 프로퍼티와 중복되는 프로퍼티를 제거한 후 & (intersection)으로 병합한다.
이런식으로 타입을 병합하려면 상당히 귀찮기 때문에 유틸 타입을 하나 만들어둔다.
원하는 요소로 렌더링 하기
지금까지는 span 요소로만 작동하기 때문에 확장성을 위해 ComponentPropsWithoutRef<"span">
에서 span 위치에 변수를 할당하도록 구현한다.
1 | // Text 컴포넌트의 커스텀 프로퍼티 선언 |
하지만, 이는 오류를 발생시킨다. ComponentPropsWithoutRef
가 받을 수 있는 제네릭 타입이 ElementType
으로 정해져 있기 때문이다.
그래서 type T
는 ElementType
을 상속한 타입이여야 한다고 명시해야한다.
1 | type TextBaseProps<T extends ElementType> = { |
이제 모든 type T
가 동일하다는 것을 보장할 수 있다.
Text<T>
의 타입 변수 TTextProps<T>
의 타입 변수 TTextBaseProps<T>
의 타입 변수 T- as 프로퍼티에 바인딩 된 타입 변수 T
ComponentPropsWithoutRef<T>
의 타입 변수 T
즉, 이 중 한곳이라도 T에 대해서 명확하게 알 수 있다면 나머지 부분에서도 자연스럽게 추론이 가능하다.
as 프로퍼티로 타이핑 추상화 하기
as
라는 프로퍼티는 Text 컴포넌트 뿐만 아니라 다양한 컴포넌트에서도 사용될 수 있기에 이 부분을 최대한 추상화 해둘 필요가 있다.
1 | // 텍스트 컴포넌트의 프로퍼티 |
OverridableProps
타입은 특정 컴포넌트가as
프로퍼티를 사용하여 HTML 요소 이름을 받고 내부적으로 해당 요소의 속성 타입을 찾아 바인딩해주는 함수이다.
이렇게 필요한 부분을 추상화해두면 필자가 아닌 다른 개발자는
ComponentPropsWithoutRef
을 사용해야한다던가Combine
타입을 사용할 때 타입 변수T
를ElementType
으로 제한해야한다던가 하는 귀찮은 부분을 생각하지 않고도as
프로퍼티를 쉽고 빠르게 추가할 수 있을 것이다.
1 | // 전체 코드 |