티스토리 뷰
React 다형성 컴포넌트 만들기 (범용성 높은 컴포넌트, Polymorphic, Typescript,Styled-Components), (ReactElement, ReactNode 에러)
YG - 96년생 , 강아지 있음, 개발자 희망 2023. 11. 1. 16:50개발자가 사용할 때 자유도가 높은 컴포넌트를 npm에 배포하고 싶었습니다.
원하는 동작은 아래와 같았습니다.
1. styled-component의 as 기능을 지원하고 싶었습니다.
2. forwardRef를 통해 ref를 받도록 지원하고 싶었습니다.
3. 추가적인 스타일을 사용하고 싶을 때, 인라인 스타일을 지원하고 싶었습니다.
1. as를 지원하기
styled-component에서 as 기능을 그대로 사용하여 구현하였습니다.
styled-components: API Reference
API Reference of styled-components
styled-components.com
예시) as를 사용하는 예시, div를 button으로 사용하고 싶을 때
import styled from 'styled-components';
export default function Test() {
return <Container as="button">Test</Container>;
}
const Container = styled.div``;
1-1. as 속성에서 html 태그가 자동완성 기능
간단하게 구현하자면 이렇게 할 수 있습니다.
import { PropsWithChildren } from 'react';
import styled from 'styled-components';
interface TestProps extends PropsWithChildren {
/**
* HTML 태그를 문자열로 입력해 원하는 HTML 태그로 사용할 수 있습니다.
*/
as?: string;
}
export default function Test({ as, children }: TestProps) {
return <Container as={as}>{children}</Container>;
}
const Container = styled.div``;
하지만 아래의 사진과 같이 html 태그 이름을 자동 완성을 하고 싶은데요.

ElementType으로 as로 html 태그 자동 완성을 지원할 수 있습니다.
import { ElementType, PropsWithChildren } from 'react';
import styled from 'styled-components';
interface TestProps extends PropsWithChildren {
/**
* HTML 태그를 문자열로 입력해 원하는 HTML 태그로 사용할 수 있습니다.
*/
as?: ElementType;
}
export default function Test({ as, children }: TestProps) {
return <Container as={as}>{children}</Container>;
}
const Container = styled.div``;
1-2. as를 사용하여 태그를 변경했을 때 해당 html 태그의 attribute 속성들이 자동완성
ComponentPropsWidthoutRef <T> 타입을 이용하면 T에 해당하는 엘리먼트의 props에서 ref만 제외한 속성들을 타입으로 이용할 수 있습니다.
import {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
} from 'react';
import styled from 'styled-components';
interface TestProps<T extends ElementType> extends PropsWithChildren {
/**
* HTML 태그를 문자열로 입력해 원하는 HTML 태그로 사용할 수 있습니다.
*/
as?: T;
}
export type PolymorphicComponentProps<T extends ElementType> =
ComponentPropsWithoutRef<T> & TestProps<T>;
export default function Test<T extends ElementType = 'div'>({
as,
children,
...rest
}: PolymorphicComponentProps<T>) {
return (
<Container as={as} {...rest}>
{children}
</Container>
);
}
const Container = styled.div``;


2. forwardRef를 통해 ref를 받도록 지원하고 싶었습니다.
ref를 받을 때 forwardRef를 이용했는데요. ref가 필수가 아니고, 부모 컴포넌트에서 ref를 제어할 수 있다는 점에서 사용했습니다.
forwardRef – React
The library for web and native user interfaces
react-ko.dev
2.1 코드를 forwardRef를 사용하는 코드로 변경
ref를 해당 html 태그에 맞는 것을 사용했을 때만 사용될 수 있게 반환 타입을 작성했습니다.
export type PolymorphicComponentProps<T extends ElementType> =
ComponentPropsWithoutRef<T> &
TestProps<T> & {
ref?: PolymorphicRef<T>;
};
type TestComponent = <T extends ElementType>(
props: PolymorphicComponentProps<T>
) => ReactElement | null;
import {
ComponentPropsWithRef,
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
ReactElement,
forwardRef,
} from 'react';
import styled from 'styled-components';
interface TestProps<T extends ElementType> extends PropsWithChildren {
/**
* HTML 태그를 문자열로 입력해 원하는 HTML 태그로 사용할 수 있습니다.
*/
as?: T;
}
export type PolymorphicRef<T extends ElementType> =
ComponentPropsWithRef<T>['ref'];
export type PolymorphicComponentProps<T extends ElementType> =
ComponentPropsWithoutRef<T> &
TestProps<T> & {
ref?: PolymorphicRef<T>;
};
type TestComponent = <T extends ElementType>(
props: PolymorphicComponentProps<T>
) => ReactElement | null;
const Test: TestComponent = forwardRef(function Test<
T extends ElementType = 'div'
>(
{ as, children, ...rest }: PolymorphicComponentProps<T>,
ref: ComponentPropsWithRef<T>['ref']
) {
return (
<Container ref={ref} as={as} {...rest}>
{children}
</Container>
);
});
export default Test;
const Container = styled.div``;


3. 추가적인 스타일을 사용하고 싶을 때, 인라인 스타일을 지원
CSSProperties 타입을 이용해서 인라인 CSS 속성을 자동 완성 되도록 할 수 있습니다.
interface TestProps<T extends ElementType> extends PropsWithChildren {
/**
* HTML 태그를 문자열로 입력해 원하는 HTML 태그로 사용할 수 있습니다.
*/
as?: T;
/**
* 디테일 한 CSS 속성을 지정해야 할 경우 직접 CSS를 입력할 수 있습니다.
*/
css?: CSSProperties;
}
const Test: TestComponent = forwardRef(function Test<
T extends ElementType = 'div'
>(
{ as, children, css, ...rest }: PolymorphicComponentProps<T>,
ref: ComponentPropsWithRef<T>['ref']
) {
return (
<Container ref={ref} as={as} style={css} {...rest}>
{children}
</Container>
);
});


공통 타입과 공통 스타일 정의하기
컴포넌트를 한 개 만들 때마다 매번 타입을 정의하기엔 비효율적입니다.
공통 타입과 공통 스타일을 지정해 두고 사용한다면 편하게 새로운 컴포넌트를 만들 수 있습니다.
공통으로 사용할 스타일 props들과 기본이 될 스타일드 컴포넌트 코드를 작성했습니다.
style/common.ts
m과 mx, px 등 컴포넌트에서 사용할 props들을 정의해 주었습니다.
import { CSSProperties } from 'react';
import styled from 'styled-components';
export interface CommonStyleProps {
/**
* 컴포넌트의 너비를 조정할 수 있습니다.
*/
width?: string;
/**
* true로 지정하면 화면의 전체의 너비를 차지합니다. (100vw)
*/
fullScreen?: boolean;
/**
* margin 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
m?: string;
/**
* margin에서 가로 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
mx?: string;
/**
* margin에서 세로 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
my?: string;
/**
* margin-left 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
ml?: string;
/**
* margin-right 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
mr?: string;
/**
* margin-bottom 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
mb?: string;
/**
* margin-top 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
mt?: string;
/**
* margin에서 가로 옵션을 auto로 조정할 수 있습니다.
*/
mxAuto?: boolean;
/**
* margin에서 세로 옵션을 auto로 조정할 수 있습니다.
*/
myAuto?: boolean;
/**
* padding 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
p?: string;
/**
* padding에서 가로 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
px?: string;
/**
* padding에서 세로 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
py?: string;
/**
* padding-left 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
pl?: string;
/**
* padding-right 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
pr?: string;
/**
* padding-bottom 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
pb?: string;
/**
* padding-top 옵션을 조정할 수 있습니다. EX) 10px, 10%
*/
pt?: string;
fontSize?: string;
fontWeight?: CSSProperties['fontWeight'];
textAlign?: CSSProperties['textAlign'] | string;
border?: string;
borderRadius?: string;
color?: string;
bgColor?: string;
}
const getMarginProperty = ({
m,
mx,
my,
mxAuto,
myAuto,
}: {
m?: string;
mx?: string;
my?: string;
mxAuto?: boolean;
myAuto?: boolean;
}) => {
if (m) {
return m;
}
if (mxAuto && myAuto) {
return 'auto auto';
}
if (mxAuto && my) {
return `${my} auto`;
}
if (myAuto && mx) {
return `auto ${mx}`;
}
if (mx && my) {
return `${my} ${mx}`;
}
if (mx) {
return `0 ${mx}`;
}
return `${my} 0`;
};
const getPaddingProperty = ({
p,
px,
py,
}: {
p?: string;
px?: string;
py?: string;
}) => {
if (p) {
return p;
}
if (px && py) {
return `${py} ${px}`;
}
if (px) {
return `0 ${px}`;
}
return `${py} 0`;
};
const getWidthProperty = ({
width,
fullScreen,
}: {
width?: string;
fullScreen?: boolean;
}) => {
if (fullScreen) {
return '100vw';
}
if (width) {
return width;
}
return '100%';
};
export const CommonTag = styled.div<CommonStyleProps>`
width: ${({ width, fullScreen }) => getWidthProperty({ width, fullScreen })};
padding: ${({ p, px, py }) => getPaddingProperty({ p, px, py })};
padding-left: ${({ pl }) => pl};
padding-right: ${({ pr }) => pr};
padding-top: ${({ pt }) => pt};
padding-bottom: ${({ pb }) => pb};
margin: ${({ m, mx, my, mxAuto, myAuto }) =>
getMarginProperty({ m, mx, mxAuto, my, myAuto })};
margin-top: ${({ mt }) => mt};
margin-bottom: ${({ mb }) => mb};
margin-right: ${({ mr }) => mr};
margin-left: ${({ ml }) => ml};
font-size: ${({ fontSize }) => fontSize};
font-weight: ${({ fontWeight }) => fontWeight};
text-align: ${({ textAlign }) => textAlign};
border: ${({ border }) => border};
border-radius: ${({ borderRadius }) => borderRadius};
color: ${({ color }) => color};
background-color: ${({ bgColor }) => bgColor};
`;
types/common.ts
Common이라는 타입에서 PropsWidthChildren과 공통으로 사용할 CSS Props 타입인 CommonStyleProps을 추가해 주고, 위에서 사용한 타입을 기반으로 작성해 줍니다.
import {
CSSProperties,
ComponentPropsWithRef,
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
} from 'react';
import { CommonStyleProps } from '../style/common';
interface Common<T extends ElementType>
extends CommonStyleProps,
PropsWithChildren {
/**
* HTML 태그를 문자열로 입력해 원하는 HTML 태그로 사용할 수 있습니다.
*/
as?: T;
/**
* 디테일 한 CSS 속성을 지정해야 할 경우 직접 CSS를 입력할 수 있습니다.
*/
css?: CSSProperties;
}
export type PolymorphicRef<T extends ElementType> =
ComponentPropsWithRef<T>['ref'];
export type PolymorphicComponentProps<
T extends ElementType,
Props
> = Common<T> &
ComponentPropsWithoutRef<T> &
Props & {
ref?: PolymorphicRef<T>;
};
사용 예시
공통으로 사용하는 Props가 있지만, 각 컴포넌트마다 사용되는 Props가 다른 경우가 대부분입니다. 그래서 해당 컴포넌트에서 사용할 Props를 정의해 주고, PolymorphicComponentProps 타입을 통해 T와 해당 컴포넌트 Props를 인자로 줘서 사용했습니다.
Container.tsx
import { ElementType, ReactElement, forwardRef } from 'react';
import { PolymorphicComponentProps, PolymorphicRef } from '../types/common';
import * as S from './style';
export interface _ContainerProps {
/**
* 컴포넌트의 최소 너비를 지정하는 옵션입니다. 500px, 50%와 같이 문자열로 사용할 수 있습니다.
*/
minWidth?: string;
/**
* 컴포넌트의 최대 너비를 지정하는 옵션입니다. 500px, 50%와 같이 문자열로 사용할 수 있습니다.
*/
maxWidth?: string;
}
export type ContainerProps<T extends ElementType> = PolymorphicComponentProps<
T,
_ContainerProps
>;
type ContainerComponent = <T extends ElementType>(
props: ContainerProps<T>
) => ReactElement | null;
const Container: ContainerComponent = forwardRef(function Container<
T extends ElementType = 'div'
>({ css, as, children, ...rest }: ContainerProps<T>, ref: PolymorphicRef<T>) {
return (
<>
<S.Component ref={ref} as={as} style={{ ...css }} {...rest}>
{children}
</S.Component>
</>
);
});
export default Container;
Container.style.ts
컨테이너 컴포넌트에서 사용한 props와 공통으로 사용하는 props를 유니온 타입으로 만들어 사용했습니다.
그리고 공통으로 사용되는 CommonTag를 이용하여 컴포넌트를 만들었습니다.
import { styled } from 'styled-components';
import { CommonStyleProps, CommonTag } from '../style/common';
import { _ContainerProps } from '.';
type ComponentProps = _ContainerProps & CommonStyleProps;
export const Component = styled(CommonTag)<ComponentProps>`
min-width: ${({ minWidth }) => minWidth};
max-width: ${({ maxWidth }) => maxWidth};
`;
반환값 ReactElement와 ReactNode에 대한 오류 (ExoticComponent 타입), @types/react 에러
ReactElement로 컴포넌트의 반환값을 설정 후 다른 프로젝트에서 npm 패키지를 설치해서 사용했습니다. 하지만 오류가 났었고 이유를 알 수 없어 답답했었는데요. 이를 해결하기 위해 @types/react의 파일에서 해당 타입의 반환값 타입을 직접 변경해서 사용하기도 했었는데 이유를 알게 되어서 적게 되었습니다.
오류 예시)
const Grid << 이 부분에서 타입 에러가 납니다. 에러는 ForwardRefExoticComponent의 반환 값이 ReactNode에 해당하지 않아서 에러가 나는 것이였습니다.
```tsx
type GridComponent = <T extends ElementType>(
props: GridProps<T>
) => ReactElement | null;
const Grid : GridComponent = forwardRef(function Grid<
```
```
Type 'ForwardRefExoticComponent<Omit<GridProps<ElementType>, "ref"> & RefAttributes<unknown>>' is not assignable to type 'GridComponent'.
Type 'ReactNode' is not assignable to type 'ReactElement<any, string | JSXElementConstructor<any>> | null'.ts(2322)
```
문제가 되는 타입리엑트 17 버전의 ExoticComponent의 타입은 ReactElement | null로 되어있습니다.

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/v17/index.d.ts
그리고 23년 5~6월쯤 해당 타입 관련된 PR이 머지가 되면서 타입이 변경되었습니다.

🤖 Merge PR #65135 [react] Allow returning ReactNode from function com… · DefinitelyTyped/DefinitelyTyped@443451c
…ponents by @eps1lon * [react] Allow returning ReactNode from function components * [react] Ignore statics from element type checking would require a lot of work to fix the issues in the consumi...
github.com
해당 타입을 변경하게 된 이유는 아래와 같은데요. 이해하진 못했습니다.

[react] Allow returning ReactNode from function components by eps1lon · Pull Request #65135 · DefinitelyTyped/DefinitelyTyped
Note: This change only applies to TypeScript 5.1 and later Adds a new ElementType under the JSX namespace that is used by TypeScript 5.1 to determine if an element type is valid. This will allow f...
github.com
결론
ReactElement를 반환값으로 하여 npm을 배포했을 때 @types/react 버전이 낮은 경우 타입 에러가 납니다. 반면에 ReactNode로 할 경우 최신 버전에서 에러가 나게 되어서 이러한 사실을 npm 패키지에 설명해 주시면 좋을 것 같아요
해당 코드가 사용된 레포지토리입니다
GitHub - Gilpop8663/layout-component
Contribute to Gilpop8663/layout-component development by creating an account on GitHub.
github.com
참고자료
Polymorphic한 React 컴포넌트 만들기
들어가기에 앞서 Polymorphism 은 한국어로 다형성이라고 부르는데, 여러 개의 형태를 가진다 라는 의미를 가진 그리스어에서 유래된 단어다. 그럼 이 글의 제목에 포함된 Polymorphic 은 다형의 혹은
kciter.so
TypeScript로 React 다형성 컴포넌트 만들기
해당 글은 Iskander Samatov님의 React polymorphic components with TypeScript를 번역한 글입니다. 오역은 댓글로 남겨주시면 바로 반영하도록 하겠습니다.다형성 컴포넌트는 널리 사용되는 React 패턴입니다. 들
velog.io
React polymorphic components with TypeScript
Polymorphic component is a powerful React pattern for controlling how your components render in DOM.
itnext.io
Build strongly typed polymorphic components with React and TypeScript - LogRocket Blog
Learn how to build strongly typed polymorphic React components with TypeScript, using familiar Chakra UI and MUI component props as guides.
blog.logrocket.com
타입스크립트와 함께 컴포넌트를 단계 별로 추상화해보자
최근 필자가 활동하고 있는 루비콘 팀에서는 멘토링 프로젝트를 함께 진행했던 멘티 분들과 함께 lubycon-ui-kit이라는 작은 프로젝트를 시작했다. 뭐 시작한지 얼마 안 되어서 아직 아무 것도 없지
evan-moon.github.io
'react' 카테고리의 다른 글
React에서 react-toastify로 효과적인 알림 구현하기 (0) | 2025.03.11 |
---|---|
무한스크롤 웹 접근성 챙기기 (1) | 2023.11.21 |
Eslint Import/Order (import 순서) 설정하기 (2) | 2023.05.12 |
CRA(create-react-app)로 만든 프로젝트 / Storybook 7 버전에서 절대 경로를 설정하는 방법 (with Typescript) / jest 절대 경로 설정하기 (0) | 2023.05.04 |
React에서 ApolloGraphQL을 사용한 graphQL 사용 방법 (5) | 2023.04.17 |
- Total
- Today
- Yesterday
- NextRequest
- 위코드
- 북클럽
- 원티드
- 노개북
- NextApiRequest
- createPortal
- nextjs
- 초보
- 윤성우 열혈C프로그래밍
- React
- javascript
- WSL2
- import/order
- 스토리 북
- env
- TopLayer
- C언어
- error
- 노마드코더
- Storybook
- 프론트앤드
- 아차산
- jest
- nodejs
- CLASS
- electron
- 프리온보딩
- 우아한테크코스
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |