개발을 하면서, 리액트 컴포넌트의 타입에 ReactNode, ReactElement..를 정확히 알지 못하고
중구난방으로 사용하고 있었다.
이번 기회에 정확한 차이점을 알아보고, 어느 경우에 어떠한 타입을 사용할지 정리해보고자 한다.
@types/react는 리액트 컴포넌트의 리턴 타입을 정의할때
크게 3가지를 사용한다
1. ReactElement
2. JSX.Element
3. ReactNode

type Key = string | number;
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
react.createElement를 사용하여 코드를 변환하는데, react.createElement의 리턴 타입이 ReactElement과 JSX.Element이다class Hello extends React.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
}
ReactDOM.render(
// React.createElement(Hello, {name: '홍길동'}, null)
<Hello name="홍길동" />,
document.getElementById('root')
);ReactNode 와는 달리 원시 타입을 허용하지 않고 완성된 jsx 요소만을 허용하는데, 아래의 ReactNode를 보면 ReactNode 타입이 ReactElement 타입을 포함하고 있는 관계임을 알 수 있습니다declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {
}
}
JSX.Element 는 ReactElement 를 상속하는 개념으로, types와 props를 any로 하는 지네릭 타입을 가진 ReactElement로 볼 수 있다JSX는 React의 global namespace에 있기 때문에, 다양한 라이브러리들이 자체적으로 JSX를 구현하여 사용할 수 있다type ReactChild = ReactElement | ReactText;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
ReactNode 타입은 jsx 내에서 사용할 수 있는 모든 요소의 타입을 의미하는데, 즉 string, null, undefined 등을 포함하는 가장 넓은 범위를 갖는 타입이다render()로 리턴되는 모든 것들이 해당될 수 있다ReactNode가 가장 유연하게 사용될 수 있을 것 같습니다.ReactElement와 JSX.Element을 ReactNode보다 더 "strict" 하다고 표현하는데, 위에서 봤듯이 이들은 null, string 과 같은 원시값을 리턴할 수 없습니다. 따라서 이를 사용하고 있다면 null타입을 union 해줘야한다.const Example = (): JSX.Element | null => {
if(/* true조건 */) return null;
return <p>Hello World</p>;
};interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
} ReactElement<any, any> | null 이렇게도 사용하고 있다ReactElement와 JSX.Element는 거의 동일하다고 보는데, ReactNode보다 더 strict하게, 예를 들어 자식 요소로 하나의 컴포넌트를 받는 것을 강제하고 싶은 상황 등에서 사용할 수 있을 것 같다.+ 추가내용
React.FunctionComponent (React.FC)React.FC는 암묵적으로 children을 가지고 있습니다.
따라서 children prop을 따로 명시하지 않아도 오류가 나지 않는다.
export const Greeting:FC<GreetingProps> = ({ name }) => {
// name is string!
return <h1>Hello {name}</h1>
};
// 사용
const App = () => <>
<Greeting name="Stefan">
<span>{"I can set this element but it doesn't do anything"}</span>
</Greeting>
</>
단점이자 장점으로 작용할 것 같은데, children이 암묵적으로 사용되는 것보단 필요할 때 명시적으로 선언해서 사용하는 것이 좋을 것 같다.
이 외에도 React.FC 사용을 지양하자는 내용이 있어서 참고해보면 좋을 것 같다 (하단의 첫번째 링크)
현재 ReactNode를 children으로 갖는 경우에 대해서 따로 WithChildren이라는 헬퍼 타입을 만들어서 사용하고 있었다
export type WithChildren<T = Record<string, unknown>> = T & {
children?: React.ReactNode
}
그러나 이미 react에 PropsWithChildren이 존재한다.
type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
따라서 기존의 코드를 변경해볼 수 있겠다.
// components > PopupModal > index.tsx
import { PropsWithChildren } from 'react'
interface Props {
message: string
}
export const PopupModal = ({ children, message }: PropsWithChildren<Props>) => {
다만 children만 prop으로 가지는 경우에는 PropsWithChildren<unknown> 으로 사용해야한다.
[References]