Vite + TS + Tailwind 에서 Storybook 을 사용해서 공통 컴포넌트를 정의하는중 알게된 것들
🔰 Content ->
motion/react 라이브러리 사용시 주의사항
Animation Props는 Tailwind 를 해석할 수 없다. CSS 속성으로 애니메이션화는 가능하다.
태그의 className은 Tailwind로 작성하되, motion props는 CSS로 정의 해야함
Storybook에서 컴포넌트 Props 자동화 방법
첫 시도
InputHTMLAttributes<HTMLInputElement>를 활용해서 등의 표준 HTML 태그의 속성(id, name, onChange)들을 자동으로 포함시켰다.
그런데 motion/react를 함께 사용하니까 Type 충돌이 났다.
알아보니 motion/react에서 애니메이션과 제스처 기능을 제공하기 위해 내부적으로 HTMLMotinProps 라는 자체적인 타입을 사용한다.
이 HTMLMotionProps 는 표준 HTML 속성들을 확장하거나, 때로는 자신들의 로직에 맞게 특정 이벤트 핸들러(onChange, onDrag, onFocus, …)의 타입 시그니처를 재정의 한다고 한다.
두번째 시도
@/types/motion-props.d.ts 파일을 만들고 충돌이 나는 속성을 모두 제거한 Motion태그 Props 타입을 정의했다.
import { HTMLMotionProps } from "motion/react" ;
import {
HTMLAttributes,
ButtonHTMLAttributes,
InputHTMLAttributes,
AnchorHTMLAttributes,
ImgHTMLAttributes,
} from "react" ;
type CommonMotionConfilcts =
| "onDrag"
| "onChange"
| "onFocus"
| "onBlur"
| "onKeyDown"
| "onKeyUp"
| "onKeyPress"
| "onPointerDown"
| "onPointerMove"
| "onPointerUp"
| "onPointerCancel"
| "onPointerEnter"
| "onPointerLeave"
| "onPointerOver"
| "onPointerOut" ;
export type MotionDivProps = HTMLMotionProps < "div" > &
Omit < HTMLAttributes < HTMLDivElement >, CommonMotionConfilcts >;
export type MotionButtonProps = HTMLMotionProps < "button" > &
Omit < ButtonHTMLAttributes < HTMLButtonElement >, CommonMotionConfilcts >;
export type MotionInputProps = HTMLMotionProps < "input" > &
Omit < InputHTMLAttributes < HTMLInputElement >, CommonMotionConfilcts >;
export type MotionAnchorProps = HTMLMotionProps < "a" > &
Omit < AnchorHTMLAttributes < HTMLAnchorElement >, CommonMotionConfilcts >;
export type MotionImgProps = HTMLMotionProps < "img" > &
Omit < ImgHTMLAttributes < HTMLImageElement >, CommonMotionConfilcts >;
이제 이렇게 안전하게 사용이 가능해졌다
import type { MotionInputProps } from "@/type/motion-props" ;
type InputProps = MotionInputProps & {
variant ?: "primary" | "secondary" ;
fontSize ?: "sm" | "md" | "lg" ;
motion ?: "none" | "inView" ;
onChange ?: ( value : string | number ) => void ;
};
const Input = ({
variant = "primary" ,
fontSize = "md" ,
motion = "none" ,
onChange,
... props
} : InputProps ) => {
const basicClasses = "border rounded-md p-2" ;
const variantClasses = {
primary: "border-blue-500" ,
secondary: "border-gray-500" ,
};
const fontSizeClasses = {
sm: "text-sm" ,
md: "text-base" ,
lg: "text-lg" ,
};
const combinedClasses = `
${ basicClasses }
${ variantClasses [ variant ] }
${ fontSizeClasses [ fontSize ] }
${ props . disabled ? "opacity-50" : ""}
${ props . className || ""}
` . trim ();
return (
< Motion.input
initial = {motion === "inView" ? { opacity: 0 , y: 10 } : {}}
animate = {motion === "inView" ? { opacity: 1 , y: 0 } : {}}
transition = {{ duration: 0.3 }}
className = {combinedClasses}
onChange = {( e ) => onChange ?.(e.target.value)}
{ ... props}
/>
);
};
export default Input;