원문 : The Better Way to Type React Components
저는 TypeScript 느리게 받아들였는데, TypeScript가 나쁘다고 생각해서가 아니라, JavaScript의 역동성에 대한 제 사랑 때문입니다. 하지만 놀랍게도 과거 프로젝트를 되돌아보니 모두 TypeScript로 작성되어 있었고, "나는 주로 TypeScript로 코딩한다"는 진실을 마주할 때라고 생각했습니다. 하지만 React 프로젝트에서 저를 항상 괴롭히는 한가지가 있었는데, 바로 React.FC
입니다.
어떠한 이유로, 제가 TypeScript와 React를 배우던 시기에 거의 모든 블로그, 튜토리얼 예제에서 React.FC
를 사용해서 함수 컴포넌트를 작성했습니다. 그리고 이건 어떤 면에서 의미가 있습니다. React.FunctionComponent
라는, DefinitlyTyped React에 지정된 built-in 타입이 있습니다. 완벽하게 들어 맞는 것 같습니다! 하지만, 몇 가지 단점이 있습니다.. 저는 React.FC
같은 헬퍼 타입 대신, 함수의 매개 변수로 속성을 직접 입력하는걸 추천합니다.
type MyComponentProps = {title: string};
// 👍 추천
function MyComponentFc(props: MyComponentProps) {}
// 👎 비추천
const MyComponent: React.FC<MyComponentProps> = props => {}
왜 그런지 알아봅시다!
React.FC
를 타입으로 사용하려면, 함수 자체에 해당 타입을 지정해야합니다. 매개변수나 반환 값이 아니라, 함수 값입니다. 이것은 화살표 함수나 정규 함수 표현식을 사용해서 함수 표현식을 사용해야함을 의미합니다. 저는 함수 표현식을 좋아하지만, top-level 함수는 함수 선언문을 사용하길 선호합니다. 그 이유는 두가지입니다.
JavaScript/TypeScript 모듈을 만들 때 가장 중요한 내용이 무엇인지 강조하려합니다. 가능한 컨텍스트와 모듈의 주요 목표를 이해하기 위해 필요한 부분을 "스크롤 없이 볼 수 있는 부분"에 표시하려고 노력합니다. 즉, 해당 모듈을 에디터/코드 탐색기 등을 통해 열면 가장 중요한 내용을 먼저 볼 수 있습니다. 따라서 helper 컴포넌트나 유틸들이 더 아래로 이동합니다.
// 1. 변수 설정. 예상대로 동작합니다.
const BoundFooItem = partial(FooItem, { title: "Foo" });
// 2. export할 메인 내용
export default function Foo() {}
// 3. 추가적으로 exports할 서브 내용. 종종 main export를 통해
// 의미상으로 그룹지을 수 있는 하위 유형입니다.
// 많은 사람들이 default와 named export를 혼용하지 않고, 나중에 논의하길 선호합니다.
export function FooItem() {}
// 4. Helper components
function MyInternalComponent() {}
// 5. Utils
function add() {}
function partial() {}
function identity() {}
이 코드가 동작하려면 FooItem
과 MyInternalComponent
가 호이스팅이 가능해야 합니다. 함수 표현식을 한다면, 올바른 상황에서는 이러한 유형의 호이스팅이 가능하지만 TDZ(Temporal Dead Zone)을 야기할 수도 있습니다. 함수 선언문이 더 안전합니다!
const BoundFooItem = partial(FooItem, { title: "Foo" });
// OOOPS: ReferenceError: Cannot access 'FooItem' before initialisation
export default function Foo() {}
export const FooItem = () => {};
React.FC
를 사용한 코드베이스로 작업한 적이 있다면, 아마 이런 패턴을 본 적 있을겁니다.
const MyComponent: React.FC<{}> = () => {
// Potentially lots of content here...
// ...
};
export default MyComponent;
이는 TypeScript와 모듈 문법의 조합이 함수 표현식의 default export를 지원하지 않기 때문입니다. 이것은 더 주관적이지만, 저는 export와 생성을 그룹화하길 좋아합니다. 이 방식이 모듈의 public API가 무엇인지 쉽게 추적할 수 있기 때문입니다. 결론적으로 선언과 동시에 export default를 사용하는 것은 React.FC
사용을 지원하지 않습니다
// MyComponent가 main API임이 더 명확하게 보입니다.
export default function MyComponent() {}
함수 표현식에서 TDZ 없는 호이스팅, 컴포넌트를 직접 내보내는 것. 이 두 가지는 우리가 해결할 수 없는 한계입니다. 하지만 React + TypeScript를 공부하면서 고생했던 기술적 한계가 하나 있고, 이것 때문에 React.FC를 Generics에 대한 일반적인 타입으로 지정하는걸 권하기 힘듭니다.
드롭 다운이나 리스트처럼 전달 받는 아이템의 타입을 정확히 알 수 없는 재사용성 컴포넌트를 만드는 경우가 종종 있습니다. 이 경우에 Generics
가 필요합니다. 하지만 React, TypeScript가 Generics를 지원할지라도 React.FC
를 사용할 때 TypeScript는 Generics를 허용하지 않습니다
type MyDropDownProps<T> = {
items: T[];
itemToString(item: T): string;
onSelected(item: T): void;
};
// Neither of these examples are valid
const MyDropDown: React.FC<MyDropDownProps> = (props) => {};
const MyDropDown: React.FC<MyDropDownProps<T>> = <T>(props) => {};
const MyDropDown: React.FC<MyDropDownProps<T>> = (props) => {};
const MyDropDown<T>: React.FC<MyDropDownProps<T>> = (props) => {};
// How?? No direct way when using React.FC
myDropDown과 함께 Generics를 제대로 사용하려면
타입을 "타입이 지정된 파라미터"로 직접 사용해야합니다.
type MyDropDownProps<T> = {
items: T[];
itemToString(item: T): string;
onSelected(item: T): void;
};
// Valid code
function MyDropDown<T>(props: MyDropDownProps<T>) {}
파라미터의 타입을 직접 지정함으로써, 우리는 타입 스크립트를 다른 함수와 같은 방식으로 사용합니다. React 컴포넌트에 대해서도 특별한 경우는 없습니다. 이것은 우리가 기존에 갖고 있던 지식을 TypeScript로 사용할 수 있음을 의미합니다. 여기서 부족한건 컴포넌트가 반환하는 값의 타입을 지정하는 것입니다. 걱정하지마세요! TypeScript는 자체적으로 반환 값의 타입을 추론할 수 있습니다. 물론, 명시해야하는 경우 직접 반환 값의 타입을 지정할 수 있습니다.
function MyDropDown<T>(props: MyDropDownProps<T>): JSX.Element {}
JSX.Elements에 대해 이야기 해보겠습니다. 위에서 제안한 방법을 사용한다면, children은 어떻게 처리해야할까요? 아래와 같은 방식은 작동하지 않습니다.
type MyComponentProps = { className: string };
// Does not work.
function MyComponent({ className, children }: MyComponentProps) {
// children is not typed and TypeScript complains
return <div className={className}>{children}</div>;
}
하지만 React.FC
를 사용하면 children과 함께 잘 동작합니다.
// Works as expected:
const MyComponent: React.FC<MyComponentProps> = function ({
className,
children,
}) {
// children is typed
return <div className={className}>{children}</div>;
};
이것은 React.FC
가 children
속성을 props type에 추가하기 때문입니다. 이것은 편리해 보이지만, 실제로는 나쁜 것입니다! React.FC
를 사용하면 항상 children을 속성으로 취하는 것 처럼 보입니다.
type MyComponentProps = { title: string };
const MyComponent: React.FC<MyComponentProps> = function ({ title }) {
// children이 지정되지 않았기 때문에 TypeScript가 이를 지적합니다.
return <div>{title}</div>;
};
// TypeScript는 이를 허용하지만, 이것은 어떤 의미에서는 '오탐'입니다.
const myValue = <MyComponent title="Hello">{ children}</MyComponent>;
// This is more correct
function MyComponentCorrect({ title }: MyComponentProps) {
return <div>{title}</div>;
}
const myValueCorrect = (
<MyComponentCorrect title="Hello">{ children}</MyComponentCorrect>
);
// Error:
// Type '{ children: string; title: string; }' is not assignable to type 'IntrinsicAttributes & MyComponentProps'.
// Property 'children' does not exist on type 'IntrinsicAttributes & MyComponentProps'.
파라미터에 직접 타입을 지정하는 것은 오탐을 허용하지 않고 더 많은 유연성, 정확성을 제공합니다. 그렇다면 어떻게 children의 타입을 지정할까요? 두 가지 방법이 있습니다.
// Manually
type MyComponentProps = {
title: string;
// ReactNode 는 배열, fragments, scalar 값 등을 포함하는 모든 자식 타입입니다.
children: React.ReactNode;
};
function MyComponentCorrect({ title, children }: MyComponentProps) {}
만약 children에 어떤 유형을 사용할지 기억하기 어렵다면, React types로부터 편리한 helper를 사용할 수 있습니다.
type MyComponentProps = React.PropsWithChildren<{
title: string;
}>;
function MyComponentCorrect({ title, children }: MyComponentProps) {}
같은 일을 합니다. PropsWithChildren
은 React.FC
처럼 intersection type으로 정의됩니다.
type PropsWithChildren<P> = P & { children?: ReactNode };
// (note: optional type is redundant as ReactNode can be undefined)
파라미터에 직접 props를 사용하면 컴포넌트 타입을 더 정확하게 지정하고, 오탐 방지을 방지하며 유연성을 높일 수 있습니다.
TypeScript와 React를 공부하기 시작하면 React.FC
가 혼란스러웠습니다. 그리고 나는 왜 React.FC
가 널리 퍼졌는지 잘 모르겠습니다. 저는 TypeScript + React를 처음 공부하는 사람들이 특정 라이브러리와 관련된 이상한 관행보다 기존 TypeScript 관용 관례를 보아야한다고 생각합니다. 기본 관행은 generics와 같은 공통 기능을 혼동 없이 허용해야합니다.
이러한 방식으로 여러분은 TypeScript를 다른 함수와 동일하게 사용할 수 있으며, React.FC
를 사용할 때 보다 더 정확합니다. 추가적인 이점으로, 여러분은 preact
와 같은 컴포넌트 기반 라이브러리간에 쉽게 전환할 수 있습니다.
React.FC
대신 타입을 지정한 파라미터를 사용할 경우, JSX.Element
를 반환 값의 타입으로 명시해야 할수 있습니다. 하지만 대부분의 경우에 이러한 경우는 TypeScript가 자체적으로 추론합니다. 누군가는 파라미터의 타입을 명시적으로 입력해야하는 단점때문에 함수 시그니처를 읽기 더 어렵게 한다고 말할 수도 있습니다. 저는 동의하지 않지만, 몇몇 사람들이 왜 그런 생각을 하는지는 알 수 있습니다.
다시 한 번 말하지만 React.FC
는 사실상 표준인 코드베이스와 튜토리얼에 많이 등장합니다. 그래서 반복되는 이야기이기도 합니다. 그렇다고 해서 React.FC
를 사용한 전체 코드를 다시 수정해야할까요? 아마 아닐겁니다.
하지만 새로운 컴포넌트를 작성함에 있어 파라미터에 직접 타입을 지정하고 코드베이스에서 점차 React.FC
를 제거해가는 건 확실히 가치가 있다고 생각합니다.