Part8_JSX_to_TSX
이효석
책 읽기

8장. JSX에서 TSX로

8.1 리액트 컴포넌트의 타입

8.1.1 클래스컴포넌트 타입

  • React.Component 와 React.PureComponent 를 상속 받아서 사용
  • props 의 상태 타입을 제네릭으로 받는다

8.1.2 함수 컴포넌트 타입

  • React.FC 와 React.VFC 로 타입을 지정하여 컴포넌트 사용
  • v16 까지는 React.VFC 의 경우 children props 가 없다
  • v18 에 이르러 React.FC 에서도 children 이 사라져서 별도로 타이핑이 필요

8.1.3 Children props 타입 지정

type PropsWithChildren<P> = P & { children?: ReactNode | undfined };
  • 보편적으로 ReactNode 또는 undfined 사용

8.1.4 render 메서드와 함수 컴포넌트의 반환 타입 - React.ReactElement vs JSX.Element vs React.ReactNode

  • ReactNode ⊃ ReactElement ⊃ Jsx.Element 의 포함 관계를 가진다

8.1.5 ReactElement, ReactNode, JSX.Element 활용하기

ReactElement

  • JSX 는 리액트 엘리먼트를 생성하기 위한 문법이며, JSX 문법을 Babel 이 트랜스파일하여 createElement 메서드 호출문으로 리액트 엘리먼트를 생성
  • 따라서 ReactElement 타입은 JSX의 createElement 로 인해 생성된 리액트 엘리먼트를 나타내는 타입
const element = React.createElement(
  'h1',
  { className: 'greeting' },
  'Hello, world!'
);
 
// 주의: 다음 구조는 단순화되었다
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!',
  },
};
 
declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> {
      // ...
    }
    // ...
  }
}

ReactNode

  • ReactChild 를 포함하여 boolean, null, undefined 등 훨씬 넓은 범주로써 render 함수가 반환 가능한 모든 형태

JSX.Element

  • ReactElement 의 특정 타입으로 props 와 타입 필드를 any 로 가지는 타입

8.1.6 사용 예시

ReactNode

  • prop 을 활용하여 리액트 컴포넌트가 다양한 형태를 가지게 할 때 유용하다
type PropsWithChildren<P = unkown> = P & {
  children?: ReactNode | undefined;
};
 
interface MyProps {
  // ...
}
 
type MyComponentProps = PropsWithChildren<MyProps>;

JSX.Element

  • props 를 전달받아 render props 패턴으로 컴포넌트 구현할 때 유용
  • icon prop 을 JSX.Element 타입으로 선언하여 해당 prop 이 JSX 문법만 사용하도록 강제
interface Props {
  icon: JSX.Element;
}
 
const Item = ({ icon }: Props) => {
  // prop으로 받은 컴포넌트의 props에 접근할 수 있다
  const iconSize = icon.props.size;
 
  return <li>{icon}</li>;
};
 
// icon prop에는 JSX.Element 타입을 가진 요소만 할당할 수 있다
const App = () => {
  return <Item icon={<Icon size={14} />} />;
};

ReactElement

  • JSX.Element 는 암시적으로 props 가 any 타입으로 지정이 되므로, ReactElement 를 사용하여 직접 props 타입을 명시 할 수 있다
  • 실제적으로 JSX.Element 를 사용했을 때와 달리 icon.props 에 접근하면 직접 지정한 타입이 추론 된 것 확인 가능
interface IconProps {
  size: number;
}
 
interface Props {
  // ReactElement의 props 타입으로 IconProps 타입 지정
  icon: React.ReactElement<IconProps>;
}
 
const Item = ({ icon }: Props) => {
  // icon prop으로 받은 컴포넌트의 props에 접근하면, props의 목록이 추론된다
  const iconSize = icon.props.size;
 
  return <li>{icon}</li>;
};

8.1.7 리액트에서 기본 HTML 요소 타입 활용하기

DetailedHTMLProps 와 ComponentWithoutRef

  • DetailedHTMLProps 를 활용하면 아래와 같이 간단하기 HTML 태그의 속성고 호환되는 타입 선언이 가능
type NativeButtonProps = React.DetailedHTMLProps<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  HTMLButtonElement
>;
 
type ButtonProps = {
  onClick?: NativeButtonProps['onClick'];
};
  • ComponentWithoutRef 도 마찬가지로 HTML 의 특정 속성이 호환되는 타입 선언이 가능하다
type NativeButtonType = React.ComponentPropsWithoutRef<'button'>;
type ButtonProps = {
  onClick?: NativeButtonType['onClick'];
};

언제 ComponentWithoutRef 를 사용하면 좋을까

  • 클래스형 컴포넌트와 달리 함수형 컴포넌트의 경우 props 받은 ref 가 button 컴포넌트를 제대로 바라보지 못하는 이슈가 존재
  • React.forwardRef 메서드르 활용하면 이러한 제약 극복이 가능
  • 반면, ComponentWithoutRef 는 ref 를 포함하지 않아 DetailedHTMLProps, HTMLProps, ComponentPropsWithRef 같이 ref 를 포함하여 위와 같은 이유로 예기치 못한 오류를 발생 시키는 것을 예방할 수 있다

8.2 타입스크립트로 리액트 컴포넌트 만들기

8.2.1 JSX 로 구현된 Select 컴포넌트

  • JSX 로 구성된 컴포넌트의 경우는 각 속성에 어떤 타입을 전달해야 할 지 명확하게 알기 어렵다

8.2.2 JSDoc 으로 일부 타입 지정하기

8.2.3 props 인터페이스 적용하기

  • options 의 타입을 정의하여 option 의 유형을 명확히 하고, onChange 에는 리턴 타입이 void 임을 명시
type Option = Record<string, string>; // {[key: string]: string}
 
interface SelectProps {
  options: Option;
  selectedOption?: string;
  onChange?: (selected?: string) => void;
}
 
const Select = ({
  options,
  selectedOption,
  onChange,
}: SelectProps): JSX.Element => {
  //...
};

8.2.4 리액트 이벤트

  • 리액트의 이벤트는 브라우저의 일반적인 이벤트가 DOM 에 등록되어 실행되는 것과 달리 ReactNode 에서 실행이 되는 형태이므로 브라우저와 동일하게 작동하지 않는다

8.2.5 훅에 타입 추가하기

  • 타입을 명확하게 지정하지 않으면 TS 가 타입을 string 등으로 추론하여 입력하면 안되는 값이 입력 될 수 있으므로 명확하게 타입을 지정해야 한다
  • keyof typeof 를 사용하여 특정 타입의 키 값을 유니온 값으로 추출하여 사용
// 타입이 string 으로 추론되어 원치 않는 값이 넘어가는 경우
const [fruit, changeFruit] = useState('apple');
 
// error가 아님
const func = () => {
  changeFruit('orange');
};
 
// keyof typeof 로 타입을 유니온 형태로 강제하는 코드
type Fruit = keyof typeof fruits; // 'apple' | 'banana' | 'blueberry';
const [fruit, changeFruit] = useState<Fruit | undefined>('apple');
 
// 에러 발생
const func = () => {
  changeFruit('orange');
};

** [p.274] 이번에는 제대로 keyof typeof 를 썼군여 ㅋㅋㅋㅋ

8.2.6 제네릭 컴포넌트 만들기

  • Selct 컴포넌트에 전달되는 props 의 타입을 기반으로 타입이 추론되어, 잘못된 옵션을 전달하면 에러가 발생하는 타입 지정
interface SelectProps<OptionType extends Record<string, string>> {
  options: OptionType;
  selectedOption?: keyof OptionType;
  onChange?: (selected?: keyof OptionType) => void;
}
 
const Select = <OptionType extends Record<string, string>>({
  options,
  selectedOption,
  onChange,
}: SelectProps<OptionType>) => {
  // Select component implementation
};

8.2.7 HTMLAttributes, ReactProps 적용하기

  • className, id 와 같은 속성은 리액트에서 제공하는 타입을 사용하면 더 정확하게 타입 설정이 가능
type ReactSelectProps = React.ComponentPropsWithoutRef<'select'>;
 
interface SelectProps<OptionType extends Record<string, string>> {
  id?: ReactSelectProps['id'];
  className?: ReactSelectProps['className'];
  // ...
}

8.2.8 styled-components 를 활용한 스타일 정의

  • CSS-in-JS 라이브러리인 styled-components 를 활용하면 컴포넌트 스타일에 대한 타입을 추가 가능
// 필요한 스타일 정의
const theme = {
  fontSize: {
    default: '16px',
    small: '14px',
    large: '18px',
  },
  color: {
    white: '#FFFFFF',
    black: '#000000',
  },
};
 
type Theme = typeof theme;
type FontSize = keyof Theme['fontSize'];
type Color = keyof Theme['color'];
 
// 부모 컴포넌트에서 원하는 스타일 적용을 위해 StyleSelect 컴포넌트에서 SelectStyleProps 타입을 상속
interface SelectStyleProps {
  color: Color;
  fontSize: FontSize;
}
 
const StyledSelect = styled.select<SelectStyleProps>`
  color: ${({ color }) => theme.color[color]};
  font-size: ${({ fontSize }) => theme.fontSize[fontSize]};
`;
 
// Partial 을 사용하여 전달한 스타일 타입을 선택적으로 설정이 가능
interface SelectProps extends Partial<SelectStyleProps> {}
 
const Select = <OptionType extends Record<string, string>>({
  fontSize = 'default',
  color = 'black',
}: SelectProps<OptionType>) => {
  return <StyledSelect fontSize={fontSize} color={color} />;
};

8.2.9 공변성과 반공변성

  • 타입 A 가 B 의 서브 타입일 때 공변성을 가진다고 말한다. 그로 인해 타입은 보통 좁은 타입에서 넓은 타입으로 할당이 가능
  • 일반적인 타입들은 공변성을 가지고 있으므로 좁은 타입에서 넓은 타입으로 할당이 가능
  • 하지만 제네릭 타입을 가진 함수는 반공변성을 가지게 되어, 역으로 좁은 타입 T<A>T<B> 에 적용이 불가능
type PrintUserInfo<U extends User> = (user: U) => void;
 
let printUser: PrintUserInfo<User> = (user) => console.log(user.id);
 
let printMember: PrintUserInfo<Member> = (user) => {
  console.log(user.id, user.nickName);
};
 
printMember = printUser; // OK.
printUser = printMember; // Error - Property 'nickName' is missing in type 'User' but required in type 'Member'.

** [p.280] 좁은 타입을 가지는 함수를 넓은 타입에 적용하려 하면 매개 변수에서 없는 부분이 발생하므로, 문제가 발생하기 때문으로 추측

** [p.280] 그런데 onChange 의 경우는 화살표 함수와 일반 함수의 차이인데 왜? 화살표 함수는 반공변성을 띠고, 일반 함수는 이변성을 띨까?