개발/프로젝트

cva, clsx를 활용하여 레이어팝업 컴포넌트 만들기

xuwon 2024. 12. 27. 00:15
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')

conditiontrue일 때 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를 사용해 조건부로 클래스를 적용했다.

visibletrue일 땐 'opacity-100 pointer-events-auto' 추가
visiblefalse일 땐 '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 속성에 전달한다.

visiblesize에는 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

 

cva, tailwindmerge, clsx를 조합하여 재사용 가능한 UI 만들기

😎세가지를 조합하면 아주 멋진 것을 만들 수 있다. https://xionwcfm.tistory.com/322 https://xionwcfm.tistory.com/323 https://xionwcfm.tistory.com/325 지금까지 진행한 세가지 라이브러리에 대한 포스트입니다. 이 세

xionwcfm.tistory.com