React용 컬러 휠을 만들었습니다 (게시됨)

발행: (2026년 1월 3일 오후 07:48 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

(번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.)

📦 패키지

  • npm: react-hsv-ring
  • GitHub:
  • Storybook / Live Demo: (repo에 링크)
npm install react-hsv-ring

유일한 피어 의존성은 React 18 (또는 19)입니다.

빠른 시작

import { useState } from 'react';
import * as ColorWheel from 'react-hsv-ring';

function App() {
  const [color, setColor] = useState('#3b82f6');

  return (
    
      
        
        
        
        
      
    
  );
}

만약 Radix UI를 사용해 본 적이 있다면, import * as ComponentName 스타일과 점 표기법을 사용한 서브 컴포넌트가 익숙하게 느껴질 것입니다.

전체 기능 예제

import { useState } from 'react';
import * as ColorWheel from 'react-hsv-ring';

function ColorPicker() {
  const [color, setColor] = useState('#3b82f6');

  return (
    
      {/* The color wheel */}
      
        
        
        
        
      

      {/* Color display and HEX input */}
      
        
        
        Copy
        Paste
      

      {/* Alpha slider */}
      
    
  );
}

필요한 부분만 골라서 혼합해 사용할 수 있으므로 UI를 디자인할 때 완전한 유연성을 제공합니다.

컴포넌트 구성

1️⃣ 휠만


  
    
    
    
    
  

2️⃣ 슬라이더 기반 레이아웃


  
  
  

3️⃣ HEX 입력만


  

키보드 및 접근성

동작
← ↓ A S1씩 감소
→ ↑ D W1씩 증가
Shift + Arrow10씩 변경
Alt + Arrow최소/최대로 이동
  • 마우스 없이도 컴포넌트를 사용할 수 있습니다.
  • ARIA 역할 slider와 관련 속성이 자동으로 적용됩니다.
  • 스크린리더 안내가 색상 변화를 설명합니다.

스타일링

컴포넌트는 최소한의 스타일링만 제공됩니다. Tailwind CSS, 일반 CSS, 혹은 다른 어떤 방법으로도 스타일을 적용할 수 있습니다.



슬라이더 컴포넌트

ComponentPurpose
HueSlider선형 색조 제어 (바)
SaturationSlider채도 제어
BrightnessSlider밝기 / 값 제어
LightnessSliderHSL 밝기 제어
AlphaSlider불투명도 제어
GammaSlider감마 보정 (독립)

유틸리티 함수

import {
  // Conversions
  hsvToHex, hexToHsv, hexToRgb, rgbToHex,
  hexToHsl, hslToHex, hexToCssRgb, cssRgbToHex,

  // Manipulation
  lighten, darken, saturate, desaturate,
  mix, complement, invert, grayscale,

  // Accessibility
  getContrastRatio, isReadable, suggestTextColor,

  // Palette generation
  generateAnalogous, generateTriadic, generateShades,

  // Validation (Zod schemas)
  hexSchema, rgbSchema, hslSchema,
} from 'react-hsv-ring';

왜 HSV인가?

색 공간장점단점
RGB간단하고 웹 표준인간 인지와 일치하지 않음
HSL직관적이며 CSS‑네이티브50 % 명도에서 가장 선명하지만 어색함
HSV직관적이며 채도와 밝기가 독립적약간 더 많은 변환 작업 필요

HSV는 완벽하게 휠 UI에 매핑됩니다: 색상 링과 채도/밝기 영역이 HSV 차원에 직접 대응합니다. 또한 Photoshop과 Figma에서 사용하는 모델이므로 사용자는 이미 익숙합니다.

구현 하이라이트

루트 컴포넌트 (상태 보유자)

// Root.tsx (simplified)
export const Root = forwardRef(function Root(
  { value, onValueChange, children },
  ref,
) {
  // Radix UI’s useControllableState supports controlled & uncontrolled usage
  const [hexWithAlpha, setHexWithAlpha] = useControllableState({
    prop: value,
    defaultProp: defaultValue,
    onChange: onValueChange,
  });

  const hsv = useMemo(() => hexToHsv(hexWithAlpha), [hexWithAlpha]);

  const contextValue = useMemo(
    () => ({
      hsv,
      hex: hexWithAlpha,
      setHue,
      setSaturation,
      // …
    }),
    [hsv, hexWithAlpha],
  );

  return (
    
      {children}
    
  );
});

자식 컴포넌트는 useContext를 통해 공유된 상태를 간단히 읽습니다.

@radix-ui/react-use-controllable-state 패키지는 제어된(부모가 상태를 관리) 및 제어되지 않은(내부 상태) 패턴을 모두 쉽게 지원하도록 해줍니다:

// Controlled usage
const [color, setColor] = useState('#ff0000');

마무리 생각

  • Compound Components 패턴은 UI 구성을 세밀하게 제어하면서 API를 인체공학적으로 유지합니다.
  • 완전한 키보드 지원, 적절한 ARIA 역할, 그리고 스크린 리더 알림은 라이브러리를 진정으로 접근 가능하게 만듭니다.
  • 최소한의 스타일링은 기본 CSS와 충돌하지 않고 어떤 디자인 시스템에서도 채택할 수 있음을 의미합니다.

GitHub에서 자유롭게 사용해보고, 기여하거나, 이슈를 제기하세요! 🎨🚀

ColorWheel 컴포넌트 – 개발 노트

// Uncontrolled (component manages its own state)

문제: 채도가 0일 때 색조가 움직이지 않음

문제
채도가 0으로 설정되면 색조 슬라이더를 더 이상 움직일 수 없습니다.

근본 원인
HSV → HEX → HSV 변환 시 채도가 0이면 색조 정보가 손실됩니다.

// When saturation is 0 (gray), hue is "undefined"
const hsv = hexToHsv('#808080'); // { h: 0, s: 0, v: 50 }
// Even if h was originally 180, it becomes 0 because gray has no hue

이는 HSV 색 공간에서는 수학적으로 올바른 것이지만, 사용자 경험이 좋지 않습니다 – 사용자는 채도를 낮췄다가 다시 높일 때 색조가 유지되길 기대합니다.

해결책 – 색조를 별도로 저장하여 채도가 0일 때도 유지되도록 합니다.

// Derived HSV (calculated from hex)
const derivedHsv = useMemo(() => hexToHsv(hex), [hex]);

// Hue is stored independently
const [preservedHue, setPreservedHue] = useState(() => derivedHsv.h);

// Use preserved hue when saturation is 0
const hsv = useMemo(
  () =>
    derivedHsv.s === 0
      ? { ...derivedHsv, h: preservedHue }
      : derivedHsv,
  [derivedHsv, preservedHue]
);

공유 슬라이더 키보드 훅

export function useSliderKeyboard({
  value,
  min,
  max,
  disabled,
  onChange,
  wrap = false, // Whether to wrap around (for hue)
}) {
  return useCallback(
    (e: React.KeyboardEvent) => {
      if (disabled) return;

      let step = 1;
      if (e.shiftKey) step = 10; // Shift for 10‑unit steps

      switch (e.key) {
        case 'ArrowRight':
        case 'ArrowUp':
        case 'd':
        case 'w':
          e.preventDefault();
          if (e.altKey) {
            onChange(max); // Alt jumps to max
          } else if (wrap) {
            onChange((value + step) % (max + 1)); // Wrap around
          } else {
            onChange(Math.min(max, value + step));
          }
          break;
        // …
      }
    },
    [value, min, max, disabled, onChange, wrap]
  );
}

WASD 지원은 게이머만을 위한 것이 아닙니다 – 화살표 키가 없는 키보드도 있습니다.

색상 알림을 위한 Live Region


  {/* Text is dynamically inserted here */}

Thumb ARIA 속성

Source:

기술 스택

기술이유
React 18/19최신 버전 지원
TypeScript타입이 중요함
Tailwind CSS v4v4를 직접 사용해 보면서 학습
Radix UIuseControllableState 차용
shadcn/uicn 유틸리티 (clsx + tailwind-merge 래퍼)
Zod검증
Vitest테스트
Storybook문서화 및 시각적 테스트
ESLint v9플랫 설정

Radix UI 컴포넌트를 직접 사용하지는 않았지만, 그들의 디자인 철학을 많이 참고했습니다.
@radix-ui/react-use-controllable-state 훅은 특히 도움이 되었는데, 제어/비제어 지원을 직접 구현하는 것이 생각보다 까다롭고 오류가 발생하기 쉽습니다.

제어 가능/제어 불가능 상태 훅

// This single hook handles both controlled and uncontrolled modes
const [value, setValue] = useControllableState({
  prop: valueProp,           // Value from parent
  defaultProp: defaultValue, // Initial value
  onChange: onValueChange,   // Change callback
});

cn 유틸리티 예시

import { cn } from '@/lib/utils';

이러한 생태계가 갖춰져 있어 개발이 훨씬 수월했습니다.

Drag Behaviour

커서가 요소를 벗어나도 드래그를 계속하려면 setPointerCapture를 사용했습니다:

const handlePointerDown = useCallback((e: React.PointerEvent) => {
  e.preventDefault();
  (e.target as HTMLElement).setPointerCapture(e.pointerId);
  onDragStart?.();
}, [onDragStart]);

const handlePointerMove = useCallback((e: React.PointerEvent) => {
  // Ignore if not captured
  if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
  // Handle drag …
}, []);

const handlePointerUp = useCallback((e: React.PointerEvent) => {
  (e.target as HTMLElement).releasePointerCapture(e.pointerId);
  onDragEnd?.();
}, [onDragEnd]);

포인터 위치에서 색상(Hue) 계산

export function getHueFromPosition(
  x: number,
  y: number,
  centerX: number,
  centerY: number,
  hueOffset: number = -90
): number {
  const dx = x - centerX;
  const dy = y - centerY;
  let angle = Math.atan2(dy, dx) * (180 / Math.PI);
  angle = angle - hueOffset;
  if (angle = 360) angle -= 360;
  return angle;
}

Source:

Hue 링 (도넛 형태) 렌더링

일반 CSS만으로 도넛 모양의 색상 링을 만드는 것은 쉽지 않습니다. border-radius: 50% 요소를 겹쳐 쌓거나 clip-path를 시도해 본 뒤, conic‑gradientradial‑gradient 마스크를 조합하는 방법으로 해결했습니다.

const ringStyle: React.CSSProperties = {
  borderRadius: '50%',

  // 색상 그라디언트
  border: `${ringWidth}px solid transparent`,
  backgroundImage: `conic-gradient(
    from ${hueOffset}deg,
    hsl(0, 100%, 50%),
    hsl(60, 100%, 50%),
    hsl(120, 100%, 50%),
    hsl(180, 100%, 50%),
    hsl(240, 100%, 50%),
    hsl(300, 100%, 50%),
    hsl(360, 100%, 50%)
  )`,
  backgroundOrigin: 'border-box',
  backgroundClip: 'border-box',

  // 핵심: 도넛 형태를 위한 radial‑gradient 마스크
  mask: `radial-gradient(
    farthest-side,
    transparent calc(100% - ${ringWidth}px - 1px),
    black calc(100% - ${ringWidth}px)
  )`,
};
  • radial‑gradient가 가장자리에서 ringWidth 만큼 안쪽까지 중앙을 투명하게 만들어 외부 링만 보이게 합니다.
  • ‑1px 오프셋은 안티앨리어싱을 부드럽게 해 주며, 없을 경우 가장자리가 들쭉날쭉하게 보입니다.
background-origin: border-box;
background-clip: border-box;

위의 모든 요소가 합쳐져 견고하고 접근성이 뛰어나며 사용자 친화적인 컬러 피커 컴포넌트를 구성합니다.

참고 사항

  • background-originbackground-clip은 매우 중요합니다; 이 설정이 없으면 그라디언트가 테두리 영역에 적용되지 않습니다.
  • 저는 주로 개인 용도로 이 코드를 만들었지만, 비슷한 프로젝트를 진행하는 다른 분들에게도 도움이 되길 바랍니다.
  • 아직 개선할 여지는 충분히 있으니, 버그를 발견하거나 기능 요청이 있으면 GitHub에 이슈를 열어 주세요. 최선을 다해 대응하겠습니다.

참조

  • Radix UI – 복합 컴포넌트 모델
  • WAI‑ARIA Slider Pattern – 접근성 사양
  • HSL 및 HSV – Wikipedia
Back to Blog

관련 글

더 보기 »

데이터 시각화를 청각화하기

소개 약 10년 전, 저는 Web Audio API 해커톤에 참석했고 사이드 프로젝트 아이디어를 떠올렸습니다: 시각 장애인 사용자가 데이터 시각화를 “듣는” 방법은 어떨까?