ReactNextCentral

defaultProps 타입

Published on
이 문서는 타입스크립트를 사용하여 리액트 컴포넌트에서 `defaultProps`를 타이핑하고 사용하는 방법과 `defaultProps`가 필요하지 않을 수 있는 상황을 다룹니다.
Table of Contents

defaultProps가 필요하지 않을 수 있습니다.

이 트윗에 따르면, defaultProps는 결국 사용되지 않게 될 것입니다. 여기서 토론을 확인할 수 있습니다.

일반적인 의견은 객체 기본값을 사용하는 것입니다.

함수 컴포넌트:

type GreetProps = { age?: number };

const Greet = ({ age = 21 }: GreetProps) => // 등등

클래스 컴포넌트:

type GreetProps = {
  age?: number;
};

class Greet extends React.Component<GreetProps> {
  render() {
    const { age = 21 } = this.props;
    /*...*/
  }
}

let el = <Greet age={3} />;

defaultProps 타이핑하기

타입스크립트 3.0+부터 defaultProps에 대한 타입 추론이 크게 향상되었으나, 일부 경계 상황에서는 여전히 문제가 발생합니다.

함수 컴포넌트

// typeof를 단축어로 사용; 주목할 점은 호이스팅이 됩니다!
// DefaultProps의 타입을 선언할 수도 있습니다
// 예: https://github.com/typescript-cheatsheets/
// react/issues/415#issuecomment-841223219
type GreetProps = { age: number } & typeof defaultProps;

const defaultProps = {
  age: 21,
};

const Greet = (props: GreetProps) => {
  // 등등
};
Greet.defaultProps = defaultProps;

TS 플레이그라운드에서 보기

클래스 컴포넌트의 경우 Pick 유틸리티 타입을 포함하여 여러 방법이 있지만, props 정의를 "역전"하는 것이 권장됩니다.

type GreetProps = typeof Greet.defaultProps & {
  age: number;
};

class Greet extends React.Component<GreetProps> {
  static defaultProps = {
    age: 21,
  };
  /*...*/
}

// 타입 검사됨! 타입 단언이 필요 없습니다!
let el = <Greet age={3} />;
React.JSX.LibraryManagedAttributes 라이브러리 제작자를 위한 뉘앙스

위의 구현 방식은 앱 제작자에게는 잘 작동하지만 때때로 다른 이들이 GreetProps를 사용할 수 있도록 내보내고 싶을 수 있습니다. 여기서 문제는 GreetProps가 정의된 방식으로 agedefaultProps 때문에 필수 prop이 아니면서도 필수 prop으로 되어 있다는 것입니다.

여기서 깨달아야 할 인사이트는 GreetProps가 컴포넌트의 내부 계약이며, 소비자가 마주하는 외부 계약이 아니라는 점입니다. 별도의 타입을 특별히 내보내기 위해 만들거나, React.JSX.LibraryManagedAttributes 유틸리티를 사용할 수 있습니다.

// 내부 계약, 외부로 내보내지 않아야 함
type GreetProps = {
  age: number;
};

class Greet extends Component<GreetProps> {
  static defaultProps = { age: 21 };
}

// 외부 계약
export type ApparentGreetProps = React.JSX.LibraryManagedAttributes<
  typeof Greet,
  GreetProps
>;

이 방법은 제대로 작동하지만, ApparentGreetProps를 마우스로 가리킬 때 약간 위협적일 수 있습니다. 아래에 자세히 설명된 ComponentProps 유틸리티로 이런 보일러플레이트를 줄일 수 있습니다.

defaultProps를 가진 컴포넌트의 Props 사용하기

defaultProps가 있는 컴포넌트는 실제로 필수가 아닌 몇몇 props를 필수로 보이게 할 수 있습니다.

문제 상황

다음은 하고자 하는 작업입니다.

interface IProps {
  name: string;
}
const defaultProps = {
  age: 25,
};
const GreetComponent = ({ name, age }
: IProps & typeof defaultProps) => (
  <div>{`안녕, 내 이름은 ${name}, 나이는 ${age}`}</div>
);
GreetComponent.defaultProps = defaultProps;

const TestComponent = 
  (props: React.ComponentProps<typeof GreetComponent>) => {
  return <h1 />;
};

// '{ name: string; }' 타입에는 'age' 속성이 없지만 
// '{ age: number; }' 타입에서는 필요합니다
const el = <TestComponent name="foo" />;

해결 방법

React.JSX.LibraryManagedAttributes를 적용하는 유틸리티를 정의하세요:

type ComponentProps<T> = T extends
  | React.ComponentType<infer P>
  | React.Component<infer P>
  ? React.JSX.LibraryManagedAttributes<T, P>
  : never;

const TestComponent = (props: ComponentProps<typeof GreetComponent>) => {
  return <h1 />;
};

// 오류 없음
const el = <TestComponent name="foo" />;

TS 플레이그라운드에서 보기

기타 토론 및 지식

React.FCdefaultProps를 깨는 이유는?

여기서 토론을 확인할 수 있습니다.

이는 현재의 상태이며 미래에 수정될 수 있습니다.

타입스크립트 2.9 이전 버전

타입스크립트 2.9 이전 버전의 경우 여러 방법이 있지만, 지금까지 본 최고의 조언은 다음과 같습니다.

type Props = Required<typeof MyComponent.defaultProps> & {
  /* 추가적인 props 여기에 */
};

export class MyComponent extends React.Component<Props> {
  static defaultProps = {
    foo: "foo",
  };
}

이전 추천사항은 타입스크립트의 Partial type 기능을 사용했으며, 이는 현재 인터페이스가 감싸진 인터페이스의 부분적인 버전을 충족시킨다는 의미입니다. 이 방법을 통해 타입 변경 없이 defaultProps를 확장할 수 있습니다!

interface IMyComponentProps {
  firstProp?: string;
  secondProp: IPerson[];
}

export class MyComponent extends React.Component<IMyComponentProps> {
  public static defaultProps: Partial<IMyComponentProps> = {
    firstProp: "default",
  };
}

이 접근 방식의 문제는 React.JSX.LibraryManagedAttributes와 작동하는 타입 추론에 복잡한 문제를 일으킨다는 것입니다. 기본적으로 컴파일러가 해당 컴포넌트로 JSX 표현을 만들 때 모든 prop이 선택 사항이라고 생각하게 만듭니다.

@ferdaber의 코멘터리 보기여기.

추가할 내용이 있나요? 이슈 등록하기.