개발을 하면서, 리액트 컴포넌트의 타입에 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]