React와 TypeScript로 구현하는 고급 아이콘 컴포넌트: CSS 필터를 활용한 다양한 비활성화 효과

kiwon kim·2025년 1월 23일

Frontend

목록 보기
19/30
post-thumbnail

웹 애플리케이션을 개발하다 보면 단순히 아이콘을 보여주는 것을 넘어서, 상황에 맞는 다양한 시각적 피드백을 제공해야 할 때가 있습니다. 특히 비활성화 상태의 표현은 사용자 경험에 중요한 영향을 미치는데, 오늘은 React와 TypeScript를 사용하여 CSS 필터를 활용한 고급 아이콘 컴포넌트를 구현하는 방법을 자세히 알아보겠습니다.

왜 커스텀 아이콘 컴포넌트가 필요한가?

일반적으로 아이콘의 비활성화 상태는 단순히 회색조(grayscale)나 투명도(opacity)를 조절하는 것으로 구현합니다. 하지만 이러한 방식은 때로는 너무 단순하여 디자인의 세련됨이나 브랜드의 아이덴티티를 충분히 표현하지 못할 수 있습니다. 우리가 만들 컴포넌트는 두 가지 독특한 스타일링 방식을 제공합니다:

  1. 원본 이미지의 특성을 부분적으로 유지하면서 탁하게 만드는 방식
  2. 완전히 새로운 색상으로 대체하는 방식

타입 시스템 설계하기

먼저 TypeScript를 사용하여 컴포넌트의 타입을 정의해보겠습니다. 명확한 타입 정의는 컴포넌트의 사용성과 유지보수성을 크게 향상시킵니다.

// 비활성화 관련 속성을 위한 인터페이스
interface DisabledProps {
  isDisabled?: boolean;  // 비활성화 상태 여부
  color?: string;        // 비활성화 시 적용할 색상
}

// 아이콘 컴포넌트의 메인 속성을 위한 인터페이스
interface IconProps {
  currentInfo: {
    icon: React.ComponentType<any>;  // 아이콘 컴포넌트
  };
  iconProps?: any;                   // 아이콘에 전달할 추가 속성
  disabledProps?: DisabledProps;     // 비활성화 관련 속성
  styleType?: 'original' | 'replacement';  // 스타일링 방식 선택
}

여기서 styleType은 우리가 구현할 두 가지 스타일링 방식을 구분하는 역할을 합니다. 'original'은 원본 특성을 유지하는 방식을, 'replacement'는 새로운 색상으로 대체하는 방식을 나타냅니다.

핵심 컴포넌트 구현

이제 실제 IconWrapper 컴포넌트를 구현해보겠습니다.

const IconWrapper: React.FC<IconProps> = ({
  currentInfo,
  iconProps,
  disabledProps,
  styleType = 'original'
}) => {
  const Icon = currentInfo.icon;

  // 스타일 1: 원본 색상 부분 유지
  const originalStyle = disabledProps?.isDisabled
    ? {
        filter: `grayscale(1) ${
          disabledProps.color
            ? `opacity(0.3) sepia(1) hue-rotate(-10deg) saturate(1000%) contrast(0.8)`
            : 'brightness(0)'
        }`,
        WebkitFilter: `grayscale(1) ${
          disabledProps.color
            ? `opacity(0.3) sepia(1) hue-rotate(-10deg) saturate(1000%) contrast(0.8)`
            : 'brightness(0)'
        }`,
      }
    : {};

  // 스타일 2: 새로운 색상으로 대체
  const replacementStyle = disabledProps?.isDisabled
    ? {
        filter: `grayscale(1) ${
          disabledProps.color
            ? `opacity(0.3) drop-shadow(0 0 0 ${disabledProps.color})`
            : 'brightness(0)'
        }`,
        WebkitFilter: `grayscale(1) ${
          disabledProps.color
            ? `opacity(0.3) drop-shadow(0 0 0 ${disabledProps.color})`
            : 'brightness(0)'
        }`,
      }
    : {};

  return (
    <div className="relative">
      <div
        className="flex items-center justify-center"
        style={styleType === 'original' ? originalStyle : replacementStyle}
      >
        <Icon
          {...iconProps}
          style={{
            filter: styleType === 'original'
              ? originalStyle.filter
              : replacementStyle.filter,
            WebkitFilter: styleType === 'original'
              ? originalStyle.WebkitFilter
              : replacementStyle.WebkitFilter,
          }}
        />
      </div>
    </div>
  );
};

이 구현에서 주목할 만한 점들을 살펴보겠습니다:

1. CSS 필터의 이해

첫 번째 스타일링 방식에서 사용된 필터들의 역할을 자세히 살펴보면:

  • grayscale(1): 이미지를 흑백으로 변환합니다.
  • opacity(0.3): 30%의 투명도를 적용합니다.
  • sepia(1): 세피아 톤을 적용하여 따뜻한 느낌을 줍니다.
  • hue-rotate(-10deg): 색조를 약간 조정합니다.
  • saturate(1000%): 채도를 높여 색상을 더 선명하게 만듭니다.
  • contrast(0.8): 대비를 낮춰 부드러운 느낌을 줍니다.

두 번째 방식은 더 단순하지만 효과적입니다:

  • grayscale(1): 먼저 이미지의 색상을 제거합니다.
  • opacity(0.3): 투명도를 적용합니다.
  • drop-shadow(0 0 0 ${color}): 지정된 색상으로 그림자 효과를 적용하여 단색 효과를 만듭니다.

2. 브라우저 호환성 처리

CSS 필터는 모든 최신 브라우저에서 지원되지만, Safari와 같은 WebKit 기반 브라우저를 위해 -webkit- 접두사도 함께 사용했습니다:

WebkitFilter: `grayscale(1) ${
  disabledProps.color
    ? `opacity(0.3) sepia(1) hue-rotate(-10deg) saturate(1000%) contrast(0.8)`
    : 'brightness(0)'
}`,

실제 사용 예시

컴포넌트를 실제로 사용하는 방법을 살펴보겠습니다:

import { Camera, Mail, Bell } from 'lucide-react';

// 스타일 1: 원본 색상 부분 유지
<IconWrapper
  currentInfo={{ icon: Camera }}
  iconProps={{ size: 32, color: "#FF5733" }}
  disabledProps={{
    isDisabled: true,
    color: "#FF5733"
  }}
  styleType="original"
/>

// 스타일 2: 새로운 색상으로 대체
<IconWrapper
  currentInfo={{ icon: Mail }}
  iconProps={{ size: 32, color: "#FF5733" }}
  disabledProps={{
    isDisabled: true,
    color: "#FF5733"
  }}
  styleType="replacement"
/>

데모 컴포넌트 구현

실제 사용성을 테스트하기 위한 데모 컴포넌트도 함께 구현해보았습니다. 이 컴포넌트는 두 가지 스타일을 비교할 수 있고, 색상 선택과 활성화/비활성화 상태를 직접 테스트해볼 수 있습니다.

const IconDemo = () => {
  const [isDisabled1, setIsDisabled1] = useState(false);
  const [isDisabled2, setIsDisabled2] = useState(false);
  const [currentColor, setCurrentColor] = useState('#FF5733');

  const icons = [
    { icon: Camera, label: "Camera" },
    { icon: Mail, label: "Mail" },
    { icon: Bell, label: "Bell" }
  ];

  return (
    <div className="p-6 space-y-8 bg-white">
      {/* 스타일 1 섹션 */}
      <div className="space-y-4">
        <h2 className="text-xl font-bold">스타일 1: 원본 색상 부분 유지</h2>
        <div className="flex space-x-8 items-center">
          {icons.map((iconInfo, index) => (
            <div key={index} className="text-center space-y-2">
              <IconWrapper
                currentInfo={{ icon: iconInfo.icon }}
                iconProps={{ size: 32, color: currentColor }}
                disabledProps={{
                  isDisabled: isDisabled1,
                  color: currentColor
                }}
                styleType="original"
              />
              <p className="text-sm">{iconInfo.label}</p>
            </div>
          ))}
          <button
            onClick={() => setIsDisabled1(!isDisabled1)}
            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
          >
            {isDisabled1 ? '활성화' : '비활성화'}
          </button>
        </div>
      </div>

      {/* 색상 선택 섹션 */}
      <div className="space-y-2">
        <h3 className="font-semibold">아이콘 색상 선택</h3>
        <div className="flex items-center space-x-2">
          <input
            type="color"
            value={currentColor}
            onChange={(e) => setCurrentColor(e.target.value)}
            className="h-8 w-32"
          />
          <span className="text-sm">{currentColor}</span>
        </div>
      </div>
    </div>
  );
};

결과화면

활성화

비활성화

성능 고려사항

CSS 필터는 브라우저에서 하드웨어 가속을 사용하여 처리되므로, 성능상 큰 문제는 없습니다. 하지만 많은 수의 아이콘에 복잡한 필터를 적용할 경우, 다음과 같은 최적화를 고려할 수 있습니다:

  1. 필터 효과를 캐싱하여 재사용
  2. 필요한 경우에만 필터를 적용하도록 조건부 렌더링 사용
  3. 복잡한 필터 대신 미리 계산된 색상값 사용

마무리

이렇게 구현한 아이콘 컴포넌트는 단순한 비활성화 표현을 넘어, 디자인 시스템에 풍부한 표현력을 제공합니다. TypeScript를 활용한 타입 안정성과 CSS 필터의 다양한 조합을 통해, 우리는 재사용 가능하고 확장 가능한 컴포넌트를 만들었습니다.

이 컴포넌트는 다음과 같은 상황에서 특히 유용합니다:

  • 브랜드 아이덴티티에 맞는 세련된 비활성화 효과가 필요할 때
  • 다양한 상호작용 상태를 시각적으로 표현해야 할 때
  • 디자인 시스템의 일관성을 유지하면서도 유연한 스타일링이 필요할 때
profile
FOR_THE_BEST_DEVELOPER

0개의 댓글