🔯 다형성 컴포넌트 시리즈
as 속성을 위한 컴포넌트 문자열 별칭을 정해보자
이번 글은 프로젝트에 적용된 내용도 아니고 그냥 `이런 것도 가능할까?` 라는 생각으로 시도해 본거라
굳이 다형성 컴포넌트를 구성하는데 필요한 내용은 아니다. 아이디어도 심플하고... 가볍게 봐주면 좋겠다.
만약 다형성 컴포넌트를 만들어보고 싶다면 1편을, as 속성을 특정 태그로 제한을 두고 싶다면 2편을 참고하자.
그래서 주제가 뭐냐면, 컴포넌트에 문자열(string) 형태의 별칭을 정해서
as 속성에 이 별칭을 넣으면 지정된 컴포넌트가 렌더링되도록 만드는 것이다.
예를 들어 `next/Link` 컴포넌트가 있다고 하자.
다형성 컴포넌트에서 Link 컴포넌트를 사용하려면 아래와 같이 사용해야한다.
import Link from "next/link";
<PolymorphicComponent as={Link} />
일반 html 태그가 `as="button"`과 같이 string 형태로 사용할 수 있는 것과는 사뭇 다르다.
더욱이 Link 컴포넌트는 문자열이 아니라서 as 속성에 대해 TS에서 자동완성 지원이 되지 않는다.(해보게 된 이유)
그래서 이제 이를
<PolymorphicComponent as="myNextLink" />
이런 형태로 바꾸는..... 뭐 이런 걸 해볼 생각이다.
1️⃣ 매핑 객체 정의하기
// constant/componentAlias.ts
import Link from "next/link";
export const ComponentAlias = {
"next/link": Link,
};
먼저 컴포넌트와 별칭을 매핑하는 매핑 객체를 정의한다.
여기서는 Link 컴포넌트에 "next/link" 라는 별칭을 매핑했다.
2️⃣ RenderableAsType 타입 정의하기
그 다음 as 속성이 extends 할 타입을 정해주자.
import { ComponentAlias } from "@constants/componentAlias";
/** ComponentAlias의 key와 컴포넌트 타입을 매핑한 타입 */
type ComponentAliasType = {
[K in keyof typeof ComponentAlias]: (typeof ComponentAlias)[K];
};
/** renderAs에 실제로 사용될 수 있는 html 태그 + 컴포넌트 별칭 통합 타입 */
export type RenderableAsType = keyof JSX.IntrinsicElements | keyof typeof ComponentAlias
이전까지는 `React.ElementType`을 extends 했는데
이는 `JSX.IntrinsicElements`와 `React.ComponentType`이 합쳐진 타입이다.
그래서 as 속성에 Link를 그대로 넣어도 에러가 나지 않았다.
그런데 이제 컴포넌트의 별칭만 받을 생각이므로 `ComponentType`을 덜어내고 별칭을 포함시킨
`RenderableAsType`을 정의해준다.
/** html 태그 or 컴포넌트 별칭이 들어오면 컴포넌트 타입을 return하는 helper 타입 */
type RenderableElementType<T extends RenderableAsType> = T extends keyof typeof ComponentAlias
? ComponentAliasType[T]
: T;
/** html 태그 or 컴포넌트 별칭이 들어오면 ComponentPropsWithRef를 return하는 helper 타입*/
type RenderableElementPropsWithRef<T extends RenderableAsType> = React.ComponentPropsWithRef<
RenderableElementType<T>
>;
/** html 태그 or 컴포넌트 별칭이 들어오면 ComponentPropsWithoutRef를 return하는 helper 타입*/
type RenderableElementPropsWithoutRef<T extends RenderableAsType> = React.ComponentPropsWithoutRef<
RenderableElementType<T>
>;
추가적으로 별칭타입을 넣으면 실제 타입으로 바꿔주는 `RenderableElementType`과 이를 활용한 helper 타입도 정의한다.
interface IntrinsicElements {
// HTML
a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
area: React.DetailedHTMLProps<React.AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>;
article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
aside: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
audio: React.DetailedHTMLProps<React.AudioHTMLAttributes<HTMLAudioElement>, HTMLAudioElement>;
b: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
base: React.DetailedHTMLProps<React.BaseHTMLAttributes<HTMLBaseElement>, HTMLBaseElement>;
bdi: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
bdo: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
big: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
blockquote: React.DetailedHTMLProps<React.BlockquoteHTMLAttributes<HTMLQuoteElement>, HTMLQuoteElement>;
body: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>;
br: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBRElement>, HTMLBRElement>;
button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
canvas: React.DetailedHTMLProps<React.CanvasHTMLAttributes<HTMLCanvasElement>, HTMLCanvasElement>;
caption: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
center: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
cite: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
code: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
col: React.DetailedHTMLProps<React.ColHTMLAttributes<HTMLTableColElement>, HTMLTableColElement>;
colgroup: React.DetailedHTMLProps<React.ColgroupHTMLAttributes<HTMLTableColElement>, HTMLTableColElement>;
data: React.DetailedHTMLProps<React.DataHTMLAttributes<HTMLDataElement>, HTMLDataElement>;
datalist: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDataListElement>, HTMLDataListElement>;
dd: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
del: React.DetailedHTMLProps<React.DelHTMLAttributes<HTMLModElement>, HTMLModElement>;
details: React.DetailedHTMLProps<React.DetailsHTMLAttributes<HTMLDetailsElement>, HTMLDetailsElement>;
dfn: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
dialog: React.DetailedHTMLProps<React.DialogHTMLAttributes<HTMLDialogElement>, HTMLDialogElement>;
div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
dl: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDListElement>, HTMLDListElement>;
dt: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
em: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
embed: React.DetailedHTMLProps<React.EmbedHTMLAttributes<HTMLEmbedElement>, HTMLEmbedElement>;
fieldset: React.DetailedHTMLProps<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, HTMLFieldSetElement>;
figcaption: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
figure: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
footer: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>;
h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
h2: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
h3: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
h4: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
h5: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
h6: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
head: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>;
header: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
hgroup: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
hr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHRElement>, HTMLHRElement>;
html: React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>;
i: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
iframe: React.DetailedHTMLProps<React.IframeHTMLAttributes<HTMLIFrameElement>, HTMLIFrameElement>;
img: React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>;
input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
ins: React.DetailedHTMLProps<React.InsHTMLAttributes<HTMLModElement>, HTMLModElement>;
kbd: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
keygen: React.DetailedHTMLProps<React.KeygenHTMLAttributes<HTMLElement>, HTMLElement>;
label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>;
legend: React.DetailedHTMLProps<React.HTMLAttributes<HTMLLegendElement>, HTMLLegendElement>;
li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>;
link: React.DetailedHTMLProps<React.LinkHTMLAttributes<HTMLLinkElement>, HTMLLinkElement>;
main: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
map: React.DetailedHTMLProps<React.MapHTMLAttributes<HTMLMapElement>, HTMLMapElement>;
mark: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
menu: React.DetailedHTMLProps<React.MenuHTMLAttributes<HTMLElement>, HTMLElement>;
menuitem: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
meta: React.DetailedHTMLProps<React.MetaHTMLAttributes<HTMLMetaElement>, HTMLMetaElement>;
meter: React.DetailedHTMLProps<React.MeterHTMLAttributes<HTMLMeterElement>, HTMLMeterElement>;
nav: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
noindex: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
noscript: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
object: React.DetailedHTMLProps<React.ObjectHTMLAttributes<HTMLObjectElement>, HTMLObjectElement>;
ol: React.DetailedHTMLProps<React.OlHTMLAttributes<HTMLOListElement>, HTMLOListElement>;
optgroup: React.DetailedHTMLProps<React.OptgroupHTMLAttributes<HTMLOptGroupElement>, HTMLOptGroupElement>;
option: React.DetailedHTMLProps<React.OptionHTMLAttributes<HTMLOptionElement>, HTMLOptionElement>;
output: React.DetailedHTMLProps<React.OutputHTMLAttributes<HTMLOutputElement>, HTMLOutputElement>;
p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>;
param: React.DetailedHTMLProps<React.ParamHTMLAttributes<HTMLParamElement>, HTMLParamElement>;
picture: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
pre: React.DetailedHTMLProps<React.HTMLAttributes<HTMLPreElement>, HTMLPreElement>;
progress: React.DetailedHTMLProps<React.ProgressHTMLAttributes<HTMLProgressElement>, HTMLProgressElement>;
q: React.DetailedHTMLProps<React.QuoteHTMLAttributes<HTMLQuoteElement>, HTMLQuoteElement>;
rp: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
rt: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
ruby: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
s: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
samp: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
search: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
slot: React.DetailedHTMLProps<React.SlotHTMLAttributes<HTMLSlotElement>, HTMLSlotElement>;
script: React.DetailedHTMLProps<React.ScriptHTMLAttributes<HTMLScriptElement>, HTMLScriptElement>;
section: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
select: React.DetailedHTMLProps<React.SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>;
small: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
source: React.DetailedHTMLProps<React.SourceHTMLAttributes<HTMLSourceElement>, HTMLSourceElement>;
span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>;
strong: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
style: React.DetailedHTMLProps<React.StyleHTMLAttributes<HTMLStyleElement>, HTMLStyleElement>;
sub: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
summary: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
sup: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
table: React.DetailedHTMLProps<React.TableHTMLAttributes<HTMLTableElement>, HTMLTableElement>;
template: React.DetailedHTMLProps<React.HTMLAttributes<HTMLTemplateElement>, HTMLTemplateElement>;
tbody: React.DetailedHTMLProps<React.HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>;
td: React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement>;
textarea: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>;
tfoot: React.DetailedHTMLProps<React.HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>;
th: React.DetailedHTMLProps<React.ThHTMLAttributes<HTMLTableHeaderCellElement>, HTMLTableHeaderCellElement>;
thead: React.DetailedHTMLProps<React.HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>;
time: React.DetailedHTMLProps<React.TimeHTMLAttributes<HTMLTimeElement>, HTMLTimeElement>;
title: React.DetailedHTMLProps<React.HTMLAttributes<HTMLTitleElement>, HTMLTitleElement>;
tr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLTableRowElement>, HTMLTableRowElement>;
track: React.DetailedHTMLProps<React.TrackHTMLAttributes<HTMLTrackElement>, HTMLTrackElement>;
u: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>;
"var": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
video: React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>;
wbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
webview: React.DetailedHTMLProps<React.WebViewHTMLAttributes<HTMLWebViewElement>, HTMLWebViewElement>;
// SVG
svg: React.SVGProps<SVGSVGElement>;
animate: React.SVGProps<SVGElement>; // TODO: It is SVGAnimateElement but is not in TypeScript's lib.dom.d.ts for now.
animateMotion: React.SVGProps<SVGElement>;
animateTransform: React.SVGProps<SVGElement>; // TODO: It is SVGAnimateTransformElement but is not in TypeScript's lib.dom.d.ts for now.
circle: React.SVGProps<SVGCircleElement>;
clipPath: React.SVGProps<SVGClipPathElement>;
defs: React.SVGProps<SVGDefsElement>;
desc: React.SVGProps<SVGDescElement>;
ellipse: React.SVGProps<SVGEllipseElement>;
feBlend: React.SVGProps<SVGFEBlendElement>;
feColorMatrix: React.SVGProps<SVGFEColorMatrixElement>;
feComponentTransfer: React.SVGProps<SVGFEComponentTransferElement>;
feComposite: React.SVGProps<SVGFECompositeElement>;
feConvolveMatrix: React.SVGProps<SVGFEConvolveMatrixElement>;
feDiffuseLighting: React.SVGProps<SVGFEDiffuseLightingElement>;
feDisplacementMap: React.SVGProps<SVGFEDisplacementMapElement>;
feDistantLight: React.SVGProps<SVGFEDistantLightElement>;
feDropShadow: React.SVGProps<SVGFEDropShadowElement>;
feFlood: React.SVGProps<SVGFEFloodElement>;
feFuncA: React.SVGProps<SVGFEFuncAElement>;
feFuncB: React.SVGProps<SVGFEFuncBElement>;
feFuncG: React.SVGProps<SVGFEFuncGElement>;
feFuncR: React.SVGProps<SVGFEFuncRElement>;
feGaussianBlur: React.SVGProps<SVGFEGaussianBlurElement>;
feImage: React.SVGProps<SVGFEImageElement>;
feMerge: React.SVGProps<SVGFEMergeElement>;
feMergeNode: React.SVGProps<SVGFEMergeNodeElement>;
feMorphology: React.SVGProps<SVGFEMorphologyElement>;
feOffset: React.SVGProps<SVGFEOffsetElement>;
fePointLight: React.SVGProps<SVGFEPointLightElement>;
feSpecularLighting: React.SVGProps<SVGFESpecularLightingElement>;
feSpotLight: React.SVGProps<SVGFESpotLightElement>;
feTile: React.SVGProps<SVGFETileElement>;
feTurbulence: React.SVGProps<SVGFETurbulenceElement>;
filter: React.SVGProps<SVGFilterElement>;
foreignObject: React.SVGProps<SVGForeignObjectElement>;
g: React.SVGProps<SVGGElement>;
image: React.SVGProps<SVGImageElement>;
line: React.SVGLineElementAttributes<SVGLineElement>;
linearGradient: React.SVGProps<SVGLinearGradientElement>;
marker: React.SVGProps<SVGMarkerElement>;
mask: React.SVGProps<SVGMaskElement>;
metadata: React.SVGProps<SVGMetadataElement>;
mpath: React.SVGProps<SVGElement>;
path: React.SVGProps<SVGPathElement>;
pattern: React.SVGProps<SVGPatternElement>;
polygon: React.SVGProps<SVGPolygonElement>;
polyline: React.SVGProps<SVGPolylineElement>;
radialGradient: React.SVGProps<SVGRadialGradientElement>;
rect: React.SVGProps<SVGRectElement>;
stop: React.SVGProps<SVGStopElement>;
switch: React.SVGProps<SVGSwitchElement>;
symbol: React.SVGProps<SVGSymbolElement>;
text: React.SVGTextElementAttributes<SVGTextElement>;
textPath: React.SVGProps<SVGTextPathElement>;
tspan: React.SVGProps<SVGTSpanElement>;
use: React.SVGProps<SVGUseElement>;
view: React.SVGProps<SVGViewElement>;
}
3️⃣ PolymorphicType 변경하기
이제 위 타입을 활용해서 다형성 타입을 바꿔준다.
type AsPropsType<T extends RenderableAsType> = {
renderAs?: T;
};
type InnerRefType<T extends RenderableAsType> = {
innerRef?: RenderableElementPropsWithRef<T>["ref"];
};
export type PolymorphicPropsType<
T extends RenderableAsType,
Props = object,
AlternateAs extends RenderableAsType = T
> = AsPropsType<T | AlternateAs> & Props & Omit<RenderableElementPropsWithoutRef<T>, keyof Props>;
export type PolymorphicPropsWithInnerRefType<
T extends RenderableAsType,
Props = object,
AlternateAs extends RenderableAsType = T
> = PolymorphicPropsType<T, Props, AlternateAs> & InnerRefType<T>;
기존에 React.ElememtType을 사용하고 있던 부분들과 별칭 처리가 필요한 타입들을
ReaderableAs와 관련된 타입들로 바꿔주었다.
4️⃣ Box 수정하기
마지막으로 RenderableAs를 정상적으로 렌더링할 수 있도록 Box 컴포넌트를 수정한다.
import React from "react";
import { PolymorphicPropsWithInnerRefType, RenderableAsType } from "@customTypes/polymorphicType";
import { ComponentAlias } from "@constants/componentAlias";
function isComponentAliasKey(key: string): key is keyof typeof ComponentAlias {
return key in ComponentAlias;
}
const Box = <T extends RenderableAsType = "div">({
renderAs,
innerRef,
...props
}: PolymorphicPropsWithInnerRefType<T>) => {
const Root = renderAs || "div";
if (isComponentAliasKey(Root)) {
const AliasComponent = ComponentAlias[Root] as any;
return <AliasComponent ref={innerRef} {...props} />;
}
return React.createElement(Root, { ref: innerRef, ...props });
};
export default Box;
TS에게는 꽤 무자비한 코드지만 Box는 다른 곳에서 import해서 사용되는 코드이고
props의 타입은 PolymorphicPropsType 쪽에서 미리 타입 체크를 해주고 받기 때문에 괜찮다.
5️⃣ 사용해보기
실제로 적용을 해보자.
- export type ButtonAlterAs = "a" | typeof Link;
+ export type ButtonAlterAs = "a" | "next/link";
정상적으로 자동완성이 되는 모습이 보인다.
props들에 대한 타입 체크 역시 정상적으로 이루어지는 것을 볼 수 있다.
마무리
만드는 과정은 재미있었지만, 굳이 적용할 필요가 없는 부분이긴하다.
실제로 프로젝트에는 안 들어간 부분이고 게시글도 기록 느낌으로 작성한거라서.....
더군다나 별칭을 정하는 컴포넌트가 많아질수록 동적 import와 같은 처리가 추가되어야하는 등 용도에 비해 단점이 명확하다.
말 그대로 `이런 것도 가능하다 `라는 느낌으로 봐줬으면 좋겠다.
'Frontend > React' 카테고리의 다른 글
재사용할 수 있는 상호작용 상태 만들기 (0) | 2024.03.15 |
---|---|
태그에 따라 바뀌는 React 컴포넌트 만들기 2 - As 속성 제한하기 (0) | 2023.09.06 |
Icon 컴포넌트 만들기(with SVGR, 동적 import) (0) | 2023.08.08 |
Tailwind에서 재사용 가능한 컴포넌트로(with cva, tailwind-merge) (0) | 2023.08.05 |
태그에 따라 바뀌는 React 컴포넌트 만들기(with TypeScript) (0) | 2023.08.03 |