export interface LoginButtonProps {
loginType: keyof typeof LOGIN_TYPE;
onClick: (e: React.MouseEvent) => void;
children: React.ReactNode;
}
const LoginButton = ({ loginType, children, onClick }: LoginButtonProps) => (
<S.Container onClick={onClick} loginType={loginType}>
<S.IconBox>{LOGIN_TYPE[loginType].icon}</S.IconBox>
<S.ContentBox loginType={loginType}>{children}</S.ContentBox>
</S.Container>
);
위의 코드는 공식에서 사용되고 있던 컴포넌트 중 하나이다. 해당 컴포넌트를 보면, prop을 통해 children과 함께 다른 여려 값들도 넘어오게 된다.
이처럼 children을 넘겨받아 사용하는 컴포넌트가 여러 개 존재했기 때문에 children에 대한 타입을 반복해서 재정의 해줘야 하는 것에 불편함을 느꼈다.
그래서 해당 반복을 줄이고자 타입을 만들고자 하였다.
이미, @types/react에는 PropsWithChildren이라는 타입이 존재한다.
type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };
제너릭 문법과 인터섹션 문법으로 구현된 타입이다.
제너릭 P에는 children 이외의 prop에 대한 타입들을 넣어준다. 그리고 children에 대한 타입은 내부에 옵셔널로 존재하고 있기 때문에 따로 선언하여 넣어줄 필요가 없다.
참고로 children의 type으로 지정된 ReactNode의 타입은 아래와 같다.
type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;
그럼 이렇듯 이미 활용한 가능한 타입이 존재함에도 따로 children에 대한 타입을 만든 이유는 무엇일까?
이는 해당 타입이 범용적으로 사용되도록 해준다.
상위 컴포넌트에서 분기에 따라 children을 넘길 수도 안 넘길 수도 있는 상황은 충분히 존재하기 떄문이다.
실제로, 우리의 공식 서비스에도 이러한 케이스가 존재한다.
하지만, 그럼에도 아쉬움을 느꼈다.
하위 컴포넌트에서 PropsWithChildren을 활용하여 children에 타입을 지정한다고 가정해보자. 다른 개발자가 해당 타입만을 보았을 때 해당 컴포넌트가 children에 대해서 strict로 받는지, optional로 받는지에 대한 정보가 나와있지 않다.
개발자는 직접 children을 넘겨주는 상위의 컴포넌트로 이동하여 흐름을 파악하여 children이 optional인지 strict인지 판단하는 과정이 필요할 것이다.
이는 개발자에게 불필요한 시간 낭비를 가져온다고 생각했다.
그래서 공식에서는 Optional로 children으로 넘어오는지 Strict으로 넘어오는지 명시적으로 타입으로 표기하여 다른 개발자가 컴포넌트를 파악할 때의 시간을 줄이고자 하였다.
children의 타입으로 정의된 ReactNode에 대한 타입을 다시 살펴보자
type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;
React에서 children으로 넘어올 수 있는 모든 타입이 정의되어 있다.
하지만, 여기서도 아쉬움을 느꼈다. 타입의 범위가 넓다는 것이다.
앞서 설명된 이유와 맥락을 같이 하여, 다른 개발자가 PropsWithChildren으로 선언된 prop의 타입을 보고 children의 타입이 어떨지 파악이 가능하냐는 것이다. 해당 타입이 선언되어 있는 컴포넌트 단에서는 children으로 어떤 타입의 값이 넘어오는지 파악하기 힘들다.
경우에 따라서, children에 string만 값으로 넘어와야 할 때도 있는데 말이다!
결국, 상위의 컴포넌트로 이동하여 prop을 넘겨주는 컴포넌트와 로직을 파악하여 children으로 string 값만 넘어와야 하는 구나를 알 수 있을 것이다.
공식 팀에서는 이러한 문제점들을 방지하기 위해, children의 타입을 명시할 수 있는 공식만의 타입을 만들게 되었다.
type PropsWithStrictChildren<
P,
T extends React.ReactNode = React.ReactNode
> = P & { children: T };
이름에서 strict
를 활용하여 children이 무조건 넘어와야 한다는 것을 명시해주었다.
또한 제너릭으로 P에서는 children으로 제외한 props들의 타입들 T에서는 children 관련된 타입을 받아 children의 타입을 좁힐 수 있도록 하였다.
children의 타입이 ReactNode인 경우, 따로 타입을 기재하지 않아도 되도록 T extends ReactNode로 처리해주었다.
공식에서는 children 타입을 좁혀줘야 하는 경우 아래의 예시처럼 처리 해주었다.
export interface LoginButtonProps {
loginType: keyof typeof LOGIN_TYPE;
onClick: (e: React.MouseEvent) => void;
}
const LoginButton = ({
loginType,
children,
onClick,
}: PropsWithStrictChildren<LoginButtonProps, string>) => (
<S.Container onClick={onClick} loginType={loginType}>
<S.IconBox>{LOGIN_TYPE[loginType].icon}</S.IconBox>
<S.ContentBox loginType={loginType}>{children}</S.ContentBox>
</S.Container>
);
로그인 버튼의 경우 children으로 string만이 넘어오도록 규제가 필요하였고 string을 넣어주어 타입을 좁혔다.
type PropsWithOptionalChildren<
P,
T extends React.ReactNode = React.ReactNode
> = P & { children?: T };
위의 Strict의 타입과 동일하다. 하지만 children이 optional로 넘어오는 경우에 대해서만 해당 타입을 사용하고 이를 알려주기 위해 이름에 Optional를 붙여 사용자아게 이를 알렸다.
위의 타입들의 경우, 다른 프로젝트에서도 자주 사용 될 것으로 예상되었기 때문에 npm으로 출시하는 과정도 거치게 되었다.
gongseek-types의 npm 주소이다.