본문으로 건너뛰기

8장. JSX에서 TSX로

· 약 13분
고현수

1. 클래스 컴포넌트 타입

interface Component<P = {}, S = {}, SS = any>
extends ComponentLifecycle<P, S, SS> {}
class Component<P, S> {
/* 생략 */
}
class PureComponent<P = {}, S = {}, SS = any> extends Component<P, S, SS> {}
  • P, S는 props와 상태를 의미
  • props와 state를 제네릭으로 받고 있다.
interface WorkerProps {
authrization: string;
}

class Worker extends React.Component<WorkerProps> {}

2. 함수 컴포넌트 타입

  • 리액트 v18에서 React.VFC가 삭제되었다.
  • React.FC에서 children이 사라졌다.
interface Workder {
authrization: string;
}
const Worker = ({ authrization }: Worker) => {};

3. Children props 타입 지정

type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
  • ReactNode는 ReactElement외에도 boolean, number 등 여러 타입을 포함하고 있는 타입이다.
    • 더 구체적으로 타이핑 하는 용도로는 적합하지 않다.
    • 특정 문자열만 허용하고 싶다면 구체적으로 타입을 지정하면 된다.
//example 1
type FrontendWorkerProps = {
children: "엄문주" | "성은지" | "고현수" | "박윤국";
};
//example 2
type FrontendWorkerProps = {
children: string;
};
//example 3
type FrontendWorkerProps = {
children: ReactElement;
};

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

ReactElement

  • 함수 컴포넌트의 반환 타입이다.
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string|JSXElementConstructor<any>{
type: T;
props: P;
key: Key | null;
}
  • React.createElement를 호출하는 형태의 구문으로 변환하면 React.createElement의 반환 타입은 ReactElement이다.
  • 가상 DOM의 엘리먼트는 ReactElement의 형태로 저장된다.
  • ReactElement타입은 리액트 컴포넌트를 객체 형태로 저장하기 위한 포멧이다.
  • JSX는 createElement 메서드를 호출하기 위한 문법이다. JSX는 리액트 엘리먼트를 생성하기 위한 문법이며 트랜스파일러는 JSX 문법을 createElement 메서드 호출문으로 변환하여 리액트 엘리먼트를 생성한다.
const element = React.createElement(
"h1",
{ className: "greeting" },
"Hello, world!"
);
  • 리액트는 리액트 엘리먼트 객체를 읽어서 DOM을 구성한다.
  • 리액트에는 여러 개의 createElement 오버라이딩 메서드가 존재하고, 이 매서드들이 반환하는 타입은 ReactElement 타입을 기반으로 한다.
  • 정리 : ReactElement 타입은 JSX의 createElement 메서드 호출로 생성된 리액트 엘리먼트를 나타내는 타입이다.

사용 예시

  • 추론 관점에서 더 유용하게 활용할 수 있는 방법은 JSX.Element 대신 ReactElement를 사용하는 것이다.
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;
reutnr(<li>{icon}</li>);
};

JSX.Element

  • JSX.Element 타입은 리액트의 ReactElement를 확장하고 있는 타입이다.
  • 글로벌 네임스페이스에 정의되어 있어 외부 라이브러리에서 컴포넌트 타입을 재정의 할 수 있는 유연성을 제공한다.

[!NOTE] 글로벌 네임스페이스 식별자가 정의되는 전역적인 범위를 말한다. 어느곳에서든지 접근할 수 있다. 자바스크립트, 타입스크립트에서는 기본적으로 전역(글로벌) 스코프에서 선언된 변수나 함수 등은 글로벌 네임스페이스에 속한다.

declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {}
}
}
  • props와 타입 필드에 대해 any 타입을 가지도록 확장하고 있다.

사용예시

  • props와 타입 필드가 any 타입인 리액트 엘리먼트를 나타낸다. 그래서 리액트 엘리먼트를 prop으로 전달받아 render props 패턴으로 컴포넌트를 구현할 때 유용하게 활용할 수 있다.
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} />} />;
};
  • JSX 문법만 삽입할 수 있게 된다.
  • icon.props에 접근하여 prop으로 넘겨받은 컴포넌트의 상세한 데이터를 가져올 수 있다.

ReactNode

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
type ReactFragment = {} | Iterable<ReactNode>; // ReactNode의 배열 형태

type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;
  • ReactElement 외에도 boolean, string, number 등의 여러 타입을 포함하고 있다.
  • ReactChild 타입은 ReactElement | string | number로 정의되어 ReactElement보다는 좀 더 넓은 범위를 갖고 있다.
  • ReactNode는 리액트의 render 함수가 반환할 수 있는 모든 형태를 담고 있다.

사용 예시

  • 리액트의 Composition(합성) 모델을 활용하기 위해 prop으로 children을 많이 사용한다.
interface ExampleProps {
children: React.ReactNode;
}
  • 어떤 형태든 children props로 지정하고 싶다면 ReactNode를 사용하면 된다.

포함관계(결론)

  • ReactNode > ReactElement > JSX.Element

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

DetailedHTMLProps와 ComponentWithoutRef

DetailedHTMLProps를 활용

type NativeButtonProps = React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
type ButtonProps = {
onClick?: NativeButtonProps["onClick"];
};

ComponentWithoutRef

type NativeButtonType = React.ComponentPropsWithoutRef<"button">;
type ButtonProps = {
onClick?: NativeButtonType["onClick"];
};
  • 언제 사용할까?
    • 함수 컴포넌트의 경우 클래스 컴포넌트와 다르게 전달받은 ref가 Button 컴포넌트의 button 태그를 바라보지 않는다.
cosnt WrapperButton = () =>{
const buttonRef = useRef();
return (
<div>
<Button ref={buttonRef} />
</div>
)
}
  • 클래스 컴포넌트에서 ref 객체는 마운트된 컴포넌트의 인스턴스를 current 속성값으로 가지지만, 함수 컴포넌트에서는 생성된 인스턴스가 없기 때문에 ref에 기대한 값이 할당되지 않는다.
  • forwardRef는 이런 제약을 극복하게 해준다.
const Button = forwardRef((props, ref)=>{
return <button ref={ref} {...props}>버튼</button>
})

cosnt WrapperButton = () =>{
const buttonRef = useRef();
return (
<div>
<Button ref={buttonRef} />
</div>
)
}
  • forwardRef는 2개의 제네릭 인자를 받을 수 있다. 첫 번째는 ref에 대한 타입 정보, 두번째는 props에 대한 타입 정보다.
type NativeButtonType = React.ComponentPropsWithoutRef<"button">;

const Button = forwardRef<HTMLButtonElement, NativeButtonType>((props, ref) => {
return (
<button ref={ref} {...props}>
버튼
</button>
);
});
  • ComponentPropsWIthoutRef<"button"> 타입은 button 태그에 대한 HTML 속성을 모두 포함하지만, ref 속성은 제외된다.

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

리액트 이벤트

  • 리액트는 가상 DOM을 다루면서 이벤트도 별도로 관리한다.
  • 리액트 이벤트는 브라우저의 고유한 이벤트와 완전히 동일하게 동작하지 않는다. 예를 들어 리액트 이벤트 핸들러는 이벤트 버블링 단계에서 호출된다.
  • Capture 단계에서 이벤트 호출을 하려면 onClickCapture와 같이 Capture를 붙여야한다.
  • 리액트는 브라우저 이벤트를 합성한 합성 이벤트를 제공한다.
type EventHandler<Event extends React.SyntheticEvent> = (
e: Event
) => void | null;
type ChangeEventHandler = EventHandler<ChangeEvent<HTMLSelectElement>>;

const eventHandler1: GlobalEventHandlers["onchange"] = (e) => {
e.target; // 일반 Event는 target이 없음.
};
const eventHandler2: ChangeEventHandler = (e) => {
e.target; // 리액트 이벤트(합성 이벤트)는 target이 있다.
};

currentTarget과 target의 차이

  • ChangeEventHandler -> target
  • ChangeEvent -> currentTarget

제네릭 컴포넌트 만들기

  • Select를 사용하는 입장에서 제한된 키(key)와 값(value)만을 가지도록 하려면 어떻게 해야할까? 함수 컴포넌트도 함수이기 때문에 제네릭을 사용한 컴포넌트를 만들어낼 수 있다.
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>) => {
const handleChange = (e) => {
const selected = Object.entries(options).find(
([_, value]) => value === e.target.value
)?.[0];
onChange?.(selected);
};

return (
<select
onChange={handleChange}
value={selectedOption && options[selectedOption]}
>
{Object.entries(options).map(([key, value]) => (
<option key={key} value={value}>
{value}
</option>
))}
</select>
);
};

const fruits = {
apple: "사과",
banana: "바나나",
blueberry: "블루베리"
};

type Fruit = keyof typeof fruits;

const FruitSelect = () => {
const [, changeFruit] = useState<Fruit | undefined>();
return (
// Type '"orange"' is not assignable to type '"apple" | "banana" | "blueberry" | undefined'.ts(2322)
<Select options={fruits} onChange={changeFruit} selectedOption="orange" />
);
};

공변성 반공변성

interface Props<T extends string> {
onChangeA?: (selected: T) => void;
onChangeB?(selected: T): void;
}

const Component = () => {
const changeToPineApple = (selectedApple: "apple") => {
console.log("this is pine" + selectedApple);
};
return (
<Select
//Error
// onChangeA={changeToPineApple}
//OK
onChangeB={changeToPineApple}
/>
);
};
  • 타입 A가 B의 서브타입일 때, T<A>T<B>의 서브타입이 된다면 공변성을 띄고 있다고 말한다.
  • 일반적으로 타입들은 공변성을 가지고 있어서 좁은 타입에서 넓은 타입으로 할당이 가능하다.
// 베이스 타입
interface User {
id: string;
}

// Member는 User의 서브 타입이다. User를 상속하기 떄문에
interface Member extends User {
// id를 가지고 있다.
nickName: string;
}

let users: Array<User> = [];
let members: Array<Member> = [];
// Member는 User에 대해 공변성을 띄고 있다.
users = members;
members = users; // Error nickName이 없다.
  • 제네릭 타입을 지닌 함수는 반공변성을 가진다. 즉 T<B>T<A>의 서브타입이 되어, 좁은 타입 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;
printUser = printMember;
  • User가 Member보다 넓은 타입이다.
  • 좁은 타입 Member를 넓은 타입 User에 할당할 수 없다.

참고자료

번역 타입스크리트의 공변성과 반공변성