cva (Class Variance Authority)
Tailwind CSS 클래스의 동적 관리를 하는 라이브러리.
기본 스타일과 다양한 변형을 정의하고 조합하여 효율적으로 관리가 가능하다!
→ 재사용성이 높음
cva('base-class', {
variants: { // 변형에 따라 추가될 클래스 정의 (size)
size: {
sm: 'w-4 h-4',
lg: 'w-8 h-8',
},
},
defaultVariants: { // 기본으로 적용되는 값
size: 'sm',
},
});
clsx
조건부로 클래스를 조합하는 라이브러리
clsx('base-class', condition && 'conditional-class')
condition이 true일 때 conditional-class 추가.
이렇게 여러 클래스를 쉽게 조합할 수 있다.
먼저 cva를 사용해서 레이어팝업의 스타일을 관리하는 layerPopupVariants 함수를 구현해 보자.
// LayerPopupStyles.ts
import { cva } from 'class-variance-authority';
export const layerPopupVariants = cva(
'flex items-center justify-center rounded-2xl bg-background border border-darkerGray transition-all', // base
{
variants: {
size: {
md: 'w-96 h-52', // 기본 크기 설정
},
visible: {
true: 'opacity-100 pointer-events-auto', // 보이는 상태
false: 'opacity-0 pointer-events-none', // 숨겨진 상태
},
},
defaultVariants: {
size: 'md', // 기본 크기
visible: false, // 기본 상태는 숨김
},
},
);
size엔 여러 크기를 넣을 수 있는데, 사이즈가 통일될 것 같아서 일단 w-96 h-52만 넣었다.
이 다음에는 props 타입을 정의해준다.
props 타입을 정해주는 이유는 컴포넌트에서 받을 수 있는 props의 타입을 명확히 정의함으로써, 코드의 오류를 방지할 수 있기 때문이다.
// types.ts
import { VariantProps } from 'class-variance-authority';
import { layerPopupVariants } from './LayerPopupStyles';
import { HTMLAttributes } from 'react';
export type LayerPopupProps = HTMLAttributes<HTMLDivElement> &
VariantProps<typeof layerPopupVariants> & {
label?: React.ReactNode; // 팝업의 제목이나 텍스트
children?: React.ReactElement; // 팝업 안에 렌더링할 JSX 요소
onClose?: () => void; // 팝업 닫기 콜백 함수
};
HTMLAttributes<HTMLDivElement>
<div> 요소에 적용 가능한 모든 HTML 속성(className, style, id 등)을 포함한다.
VariantProps<typeof layerPopupVariants>
layerPopupVariants에서 정의된 변형(size, visible) 옵션을 타입으로 자동 생성하여 포함한다.
이제 LayerPopup 컴포넌트를 구현해 보자.
'use client'; // useEffect 훅을 사용하기 위해
import React, { useEffect } from 'react';
import { clsx } from 'clsx';
import { layerPopupVariants } from './LayerPopupStyles';
import { LayerPopupProps } from './types';
const LayerPopup: React.FC<LayerPopupProps> = ({
visible,
size,
children,
label,
onClose,
...props
}) => {
// ESC 키 핸들러
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && visible) {
onClose?.(); // onClose 콜백 호출
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [visible, onClose]);
return (
<div
className={clsx(
'fixed inset-0 flex items-center justify-center transition-opacity bg-overlay',
visible ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none',
)}
>
<div
className={layerPopupVariants({ visible, size })}
{...props} // 추가 HTML 속성
>
<div className="relative w-full p-6 rounded-2xl">
<div className="flex flex-col gap-6">
<div className="flex justify-start gap-5 text-base">
<img src="/images/dolmung.png" className="w-16"></img>
{label && label}
</div>
<div>{children && children}</div>
</div>
</div>
</div>
</div>
);
};
export default LayerPopup;
하나씩 뜯어보자
const LayerPopup: React.FC<LayerPopupProps> = ({
visible, // 팝업의 보이는 상태를 제어하는 Boolean
size, // 팝업의 크기
children, // 팝업 내부에 렌더링할 React 요소
label, // 팝업의 제목이나 메시지로 표시되는 React 노드
onClose, // 팝업을 닫을 때 호출되는 콜백 함수
...props // 추가적인 HTML 속성 지원
}) => { ... }
컴포넌트 정의
React.FC<LayerPopupProps>
React 함수형 컴포넌트를 정의하고, props의 타입을 LayerPopupProps로 지정.
React.FC를 사용하면, children 타입이 자동으로 포함된다.
그래서 children을 기본적으로 사용할 경우 유용함!
또한 함수형 컴포넌트임을 명확히 나타내기 때문에 코드의 의도를 쉽게 전달할 수 있다.
React.FC를 사용하지 않고 명시적으로 props 타입을 정의할 수도 있다.
const LayerPopup = ({
visible,
size,
children,
label,
onClose,
...props
}: LayerPopupProps & { children: React.ReactNode }) => {...}
이렇게 해주면 됨!
대신 children을 쓸 거라면 정의를 따로 해줘야 한다.
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && visible) {
onClose?.(); // onClose 콜백 호출
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [visible, onClose]); // visible이 변경될 때마다 이벤트 리스너 다시 설정
ESC 키 핸들러
ESC 키를 눌렀을 때 팝업창이 닫히도록 핸들러를 구현했다.
팝업창이 켜져있고, ESC 키를 눌렀을 경우에 onClose 콜백을 호출하도록 작성했다.
<div
className={clsx(
'fixed inset-0 flex items-center justify-center transition-opacity bg-overlay',
visible ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none',
)}
>
배경 레이어 렌더링
clsx를 사용해 조건부로 클래스를 적용했다.
visible이 true일 땐 'opacity-100 pointer-events-auto' 추가
visible이 false일 땐 'opacity-0 pointer-events-none' 추가
<div
className={layerPopupVariants({ visible, size })}
{...props} // 추가 HTML 속성
>
<div className="relative w-full p-6 rounded-2xl">
<div className="flex flex-col gap-6">
<div className="flex justify-start gap-5 text-base">
<img src="/images/dolmung.png" className="w-16"></img>
{label && label}
</div>
<div>{children && children}</div>
</div>
</div>
</div>
팝업 콘텐츠
layerPopupVariants 함수를 호출하여 동적으로 Tailwind CSS 클래스 문자열을 생성 후 className 속성에 전달한다.
visible과 size에는 LayerPopup 컴포넌트에 전달받은 props 값이 들어간다!
<LayerPopup
visible={true}
size={'md'}
label={
<div className="leading-7 text-gray-800">
안녕하세요
<br />
레이어팝업입니다.
</div>
}
onClose={closePopup}
>
<>
<input className="w-full" />
<button>취소</button>
</>
</LayerPopup>
이렇게 사용할 수 있습니다!
참고 자료
cva, tailwindmerge, clsx를 조합하여 재사용 가능한 UI 만들기
https://xionwcfm.tistory.com/328
'개발 > 프로젝트' 카테고리의 다른 글
redux-persist로 Redux 상태 유지하기 (0) | 2025.01.08 |
---|---|
RTK Query로 로그인 API 구현하고 RTK로 유저 정보 관리하기 (0) | 2025.01.07 |
RTK Query를 사용하여 회원가입 API 구현하기 (1) | 2025.01.06 |
react-hook-form, zod를 활용하여 회원정보 수정 페이지 제작하기 (1) | 2025.01.04 |
dayjs를 활용하여 생년월일 선택하는 드롭다운 생성하기 (1) | 2024.12.28 |