디자인 트렌드

동적 색상 생성: 디자인 시스템을 위한 알고리즘

9분 읽기

디자인 시스템은 모든 견본을 직접 선택하는 대신 프로그래밍 방식으로 색상 팔레트를 점점 더 많이 생성하고 있습니다. 잘 선택된 하나의 브랜드 색상은 완전한 색조, 접근 가능한 텍스트 쌍, 다크 모드 변형, 조화로운 동반 색상 등 전체 시스템의 씨앗이 될 수 있습니다. 기본 알고리즘은 결과가 전문적으로 제작된 것인지 또는 계산적으로 딱딱한 것인지 여부를 결정합니다.

이 튜토리얼에서는 동적 색상 생성(음영 비율, 대비 인식 선택, 테마 파생, 자동 다크 모드 및 이러한 기술을 구현하는 라이브러리)의 핵심 알고리즘을 자신의 디자인 시스템에 적용할 수 있는 실제 사례와 함께 설명합니다.

음영 생성 알고리즘

셰이드 스케일은 단일 색상에 대한 색조 변형의 체계적인 순서입니다. 즉, 밝은 쪽의 흰색에 가까운 것부터 어두운 쪽의 검은색에 가까운 것까지입니다. Tailwind CSS의 50-950 스케일이 가장 친숙한 예입니다. 쉐이드 스케일의 품질은 전적으로 이를 생성하는 데 사용된 알고리즘에 따라 달라집니다.

HSL 기반 셰이드 생성의 문제점

HSL 색상에서 시작하여 동일한 단계로 밝기를 증가시키는 순진한 접근 방식은 고르지 않게 보이는 스케일을 생성합니다. HSL의 밝기 축은 지각적으로 균일하지 않기 때문에 L=40에서 L=50까지의 단계는 색상에 따라 다르게 보입니다. 노란색은 극적으로 변화합니다. blue barely moves. 결과 스케일은 손으로 제작된 것처럼 보이고 일관성이 없습니다.

// Naive approach — do NOT do this
function hslShades(hue, saturation, steps = 10) {
  return Array.from({ length: steps }, (_, i) => {
    const lightness = 5 + (i / (steps - 1)) * 90;
    return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
  });
}

출력은 중간 범위의 노란색과 녹색에 적합해 보이지만 파란색, 보라색 및 채도가 낮은 색상에 대해서는 품질이 크게 저하됩니다.

OKLCH 기반 셰이드 생성

올바른 접근 방식은 L축이 지각적으로 보정되는 OKLCH를 사용합니다. 동일한 L 증분은 색조에 관계없이 동일한 모양의 단계를 생성합니다. 알고리즘은 다음과 같습니다.

  1. 기본 브랜드 컬러를 OKLCH로 변환
  2. 밝기 범위를 정의합니다(예: 쉐이드 50에서 L = 0.97, 쉐이드 950에서 L = 0.15까지).
  3. 밝기와 함께 채도를 보간합니다. 매우 밝은 색조와 매우 어두운 색조는 본질적으로 중간 색조 색조보다 채도가 낮습니다.
  4. 색상을 일정하게 유지합니다(또는 미적 따뜻함/차가움 변화를 위해 작은 색상 회전 적용).
  5. 각 OKLCH 포인트를 다시 HEX 또는 sRGB로 변환합니다.
// OKLCH-based shade generation
function oklchShades(baseHex) {
  const { L, C, H } = hexToOklch(baseHex);

  // Lightness stops for a 11-step scale (50–950)
  const lightnessStops = [0.97, 0.93, 0.86, 0.78, 0.68, 0.58, 0.48, 0.39, 0.30, 0.22, 0.15];

  // Chroma curve: peaks at the base color's chroma, tapers toward extremes
  const chromaStops = lightnessStops.map(l => {
    const distance = Math.abs(l - L);
    const falloff = Math.max(0, 1 - distance * 1.5);
    return C * falloff;
  });

  return lightnessStops.map((l, i) => ({
    oklch: `oklch(${l} ${chromaStops[i].toFixed(3)} ${H})`,
    hex: oklchToHex(l, chromaStops[i], H),
  }));
}

크로마 폴오프 곡선은 가장 중요한 튜닝 매개변수입니다. 어두운 끝 부분에 전체 채도를 허용하면 부자연스럽게 채도가 높고 어둡게 보이는 음영이 생성됩니다. 밝은 끝 부분에 가까워질수록 채도가 점점 가늘어지면서 색상 특성이 너무 약하고 바랜 파스텔이 방지됩니다. 정확한 폴오프 형태는 설계 결정입니다. 일부 시스템은 2차 곡선을 사용하고 다른 시스템은 조각별 선형 함수를 사용합니다.

Shade Generator를 사용하여 모든 브랜드 색상에 적용된 이 알고리즘을 확인하세요. 접근 가능한 기본값으로 Tailwind 호환 스케일을 생성합니다.

온도 변화에 따른 색조 회전

많은 디자인 시스템은 밝기와 함께 작은 색상 회전을 적용합니다. 밝은 색상은 약간 더 따뜻하게 이동하고(낮은 H 값은 노란색 쪽으로), 어두운 색상은 약간 더 시원하게 이동합니다(높은 H 값은 파란색/보라색 방향으로). 이는 물리적 재질이 실제로 어떻게 동작하는지를 모방합니다. 진한 남색은 밝은 색조에서 약간 더 따뜻해 보입니다.

// Add subtle hue rotation
function shadeWithHueShift(L_target, C_target, H_base, L_base) {
  const lightnessDelta = L_target - L_base;
  // Shift up to ±10 degrees: warm (−) for light shades, cool (+) for dark
  const hueShift = -lightnessDelta * 15;
  return { L: L_target, C: C_target, H: H_base + hueShift };
}

색조 변화의 크기는 스타일에 따라 선택됩니다. Tailwind의 자체 팔레트는 미묘한 색조 변화를 적용합니다. 선형(이동 없음) 스케일은 깔끔하게 보이지만 기계적인 느낌을 줄 수 있습니다.

대비 인식 색상 선택

아름다운 셰이드 스케일을 생성하는 것은 문제의 절반에 불과합니다. 각 음영을 텍스트 색상이나 배경으로 사용할 수 있으려면 접근 가능한 대비를 위해 어떤 음영 음영과 짝을 이루는지 알아야 합니다.

알고리즘: 최소 대비 통과 음영 찾기

배경색과 음영 비율이 주어지면 WCAG AA(일반 텍스트의 경우 4.5:1)를 통과하는 비율에서 가장 어두운 음영을 찾습니다.

function findAccessibleTextShade(backgroundHex, shadeScale) {
  const bgLuminance = relativeLuminance(backgroundHex);

  for (const shade of shadeScale) {
    const shadeLuminance = relativeLuminance(shade.hex);
    const ratio = contrastRatio(bgLuminance, shadeLuminance);
    if (ratio >= 4.5) {
      return shade; // First shade that passes
    }
  }
  return shadeScale[shadeScale.length - 1]; // Darkest available
}

function contrastRatio(L1, L2) {
  const lighter = Math.max(L1, L2);
  const darker = Math.min(L1, L2);
  return (lighter + 0.05) / (darker + 0.05);
}

가장 실용적인 브랜드 색상의 경우, 눈금의 700 또는 800은 흰색에 대해 AA를 통과하고, 50 또는 100은 눈금의 가장 어두운 색조에 대해 AA를 통과합니다.

접근 가능한 텍스트 토큰 생성

완전한 액세스 가능 토큰 시스템은 모든 배경 음영을 WCAG AA를 통과하는 하나 이상의 전경 음영과 쌍을 이룹니다. 일반적인 패턴:

function generateAccessiblePairs(shadeScale) {
  const pairs = {};

  shadeScale.forEach((shade, index) => {
    // Find passing text colors for this background
    const passingShades = shadeScale.filter(textShade => {
      const ratio = contrastRatio(
        relativeLuminance(shade.hex),
        relativeLuminance(textShade.hex)
      );
      return ratio >= 4.5;
    });

    pairs[shade.name] = {
      background: shade.hex,
      textPrimary: passingShades[passingShades.length - 1]?.hex,  // Darkest passing
      textSecondary: passingShades[passingShades.length - 3]?.hex, // Slightly lighter
    };
  });

  return pairs;
}

브랜드 색상이 #2563EB(blue-600)인 파란색 스케일의 경우: - 파란색-50 배경 → 텍스트 기본: 파란색-900, 텍스트 보조: 파란색-700 - 파란색-600 배경 → 텍스트 기본: 흰색(#FFFFFF) 또는 파란색-50 - 파란색-900 배경 → 텍스트 기본: 흰색(#FFFFFF) 또는 파란색-100

Shade Generator는 생성된 각 음영과 함께 명암비를 표시하므로 어떤 쌍이 작동하는지 즉시 식별할 수 있습니다.

브랜드 컬러를 활용한 테마 생성

회사 로고와 마케팅 자료에 나타나는 단일 브랜드 색상이 주어지면 기본, 중립 및 의미론적 색상 역할이라는 완전한 디자인 시스템 테마를 도출할 수 있습니다.

1단계: 기본 저울 고정

위에서 설명한 대로 브랜드 색상의 OKLCH 값을 추출하고 전체 음영 스케일을 생성합니다. 브랜드 색상을 음영 600(또는 인지된 밝기 범위(일반적으로 대부분의 브랜드 색상의 경우 500~700 범위))으로 설정합니다.

2단계: 중립 척도 도출

대부분의 디자인 시스템에는 브랜드 컬러와 조화롭게 느껴지는 뉴트럴 그레이가 필요합니다. 따뜻한 중성은 브랜드 색상의 색상을 취하고 채도를 크게 낮추는 방식으로 만들어집니다.

function deriveNeutralScale(brandOklch) {
  const { H } = brandOklch;
  // Use the brand hue at very low chroma — just enough to feel warm/cool
  const neutralBaseChroma = 0.010;  // Nearly gray, slight hue influence

  return generateOklchShades({
    L: 0.55, C: neutralBaseChroma, H: H
  });
}

파란색 브랜드(#2563EB, 색조 ≒ 260°)의 경우 약간 청회색의 중성 스케일을 생성합니다. 이는 순수 회색보다 더 차갑고 브랜드 색상과 조화를 이룹니다. 오렌지 브랜드의 경우 중성색은 따뜻한 느낌을 줍니다.

3단계: 의미적 역할 할당

의미론적 토큰은 생성된 척도를 기능적 역할에 매핑합니다.

:root {
  /* Generated from brand blue #2563EB */
  --color-primary-50:  /* blue scale 50 */;
  --color-primary-600: /* blue scale 600 = brand color */;
  --color-primary-900: /* blue scale 900 */;

  /* Generated from brand hue + desaturated (warm neutral) */
  --color-neutral-50:  /* neutral scale 50 */;
  --color-neutral-900: /* neutral scale 900 */;

  /* Semantic assignments */
  --color-bg-base:           var(--color-neutral-50);
  --color-text-primary:      var(--color-neutral-900);
  --color-action-primary:    var(--color-primary-600);
  --color-action-primary-hover: var(--color-primary-700);
  --color-action-primary-text:  #FFFFFF;
}

4단계: 동반 강조 색상 생성

완전한 팔레트를 위해서는 브랜드와 조화를 이루는 1~2개의 강조 색상이 필요할 수 있습니다. 팔레트 생성기는 상보(색상환의 반대), 삼극 또는 유사한 동반과 같은 조화 관계를 계산할 수 있습니다.

프로그래밍 방식으로 고정 오프셋으로 OKLCH 색상을 회전하면 밝기와 채도가 일치하는 조화로운 동반자가 제공됩니다.

function generateCompanions(brandOklch, scheme = 'triadic') {
  const { L, C, H } = brandOklch;
  const rotations = {
    complementary: [180],
    triadic: [120, 240],
    analogous: [30, -30],
    split_complementary: [150, 210],
  };

  return rotations[scheme].map(rotation => ({
    L, C,
    H: (H + rotation + 360) % 360
  }));
}

OKLCH의 L과 C는 색상과 무관하기 때문에 이러한 방식으로 생성된 컴패니언 색상은 브랜드 색상과 동일한 밝기와 채도를 갖습니다. 즉, 자동으로 인지 균형이 조정되므로 수동 조정이 필요하지 않습니다.

자동 다크 모드 팔레트

밝은 모드 팔레트에서 어두운 모드 팔레트를 생성하는 것은 단순히 음영 할당을 바꾸는 문제가 아닙니다. 다크 모드 팔레트에는 다음이 필요합니다.

  1. 어두운 배경 (분명히)
  2. 라이트 모드 텍스트와 동일한 명암비를 유지하는 더 밝은 텍스트
  3. 조정된 기본 색상 — 중간 톤의 브랜드 색조(500~600)는 밝은 배경에서는 올바르게 보이지만 어두운 배경에서는 지나치게 밝거나 채도가 높아 보입니다.
  4. 일부 영역의 채도 감소 — 채도가 높은 어두운 표면은 공격적으로 보일 수 있습니다.

알고리즘: 다크 모드 토큰 자동 생성

가장 간단하고 신뢰할 수 있는 알고리즘은 반사 함수를 사용하여 라이트 모드 음영 지수를 다크 모드 등가물에 매핑합니다.

// Light mode: background uses light shades, text uses dark shades
// Dark mode: invert the background/text relationship

const LIGHT_TO_DARK_MAP = {
  // Background shades: reflect
  50:  900, 100: 800, 200: 700,
  // Mid-range shades: adjust slightly
  300: 700, 400: 600, 500: 500,
  600: 400, 700: 300, 800: 200,
  // Text shades: reflect
  900: 100, 950: 50,
};

function generateDarkTokens(lightTokens, shadeScale) {
  return Object.entries(lightTokens).reduce((dark, [token, shade]) => {
    const darkShadeIndex = LIGHT_TO_DARK_MAP[shade.index];
    dark[token] = shadeScale[darkShadeIndex];
    return dark;
  }, {});
}

어두운 표면의 색도 감소

풀 크로마 액센트 색상이 있는 어두운 배경은 부자연스러워 보일 수 있습니다. 실용적인 휴리스틱은 어두운 모드에서 배경 또는 표면 토큰으로 사용되는 색상의 채도를 줄이는 동시에 대화형(버튼, 링크) 색상에 대해서는 채도를 유지합니다.

function darkSurfaceColor(oklch) {
  return {
    ...oklch,
    L: oklch.L * 0.3,        // Much darker
    C: oklch.C * 0.6,        // Reduce chroma to avoid harshness
  };
}

function darkInteractiveColor(oklch) {
  return {
    ...oklch,
    L: Math.min(0.75, oklch.L + 0.15),  // Lighter for visibility
    C: oklch.C,               // Keep full chroma for vibrancy
  };
}

예를 들어 파란색 브랜드 #2563EBoklch(0.55 0.21 264))에서 시작합니다. - 라이트 모드 동작 버튼: oklch(0.55 0.21 264)#2563EB - 다크 모드 동작 버튼: oklch(0.70 0.21 264) → 대략 #60A5FA (파란색-400) — 채도를 잃지 않고 어두운 배경에서 더 밝게 돋보이게 합니다.

라이트 모드와 동일한 알고리즘을 사용하여 다크 모드 명암비를 확인합니다. 다크 모드 표면 색상의 흰색(#FFFFFF) 텍스트는 OLED 화면에서 거칠게 보일 수 있는 순수한 흰색이 아닌 브랜드 중립 스케일의 흰색에 가까운 변형인 --color-neutral-100 또는 --color-neutral-50로 대체되어야 하는 경우가 많습니다.

CSS 구현: 다크 모드 변수

:root {
  /* Light mode */
  --color-bg-base:       oklch(0.97 0.01 264);
  --color-bg-surface:    oklch(1.00 0.00 0);
  --color-text-primary:  oklch(0.18 0.03 264);
  --color-action-primary: oklch(0.55 0.21 264);
}

@media (prefers-color-scheme: dark) {
  :root {
    /* Auto-generated dark mode */
    --color-bg-base:       oklch(0.16 0.02 264);
    --color-bg-surface:    oklch(0.22 0.02 264);
    --color-text-primary:  oklch(0.93 0.01 264);
    --color-action-primary: oklch(0.70 0.21 264);
  }
}

라이브러리 및 도구

몇몇 성숙한 라이브러리는 프로덕션 품질에서 이러한 알고리즘을 구현합니다.

색상 생성 라이브러리

Radix UI 색상 — 자동 다크 모드 및 WCAG 테스트를 거친 페어링을 통해 알고리즘으로 생성된 오픈 소스 색상 스케일입니다. CIELAB 기반 알고리즘을 사용합니다. 가장 많은 테스트를 거친 오픈 소스 시스템입니다.

npm install @radix-ui/colors
import { blue, blueDark } from '@radix-ui/colors';
// blue.blue9 — the most vivid blue, light mode
// blueDark.blue9 — the equivalent in dark mode

Palette.js / Culori — OKLCH를 완벽하게 지원하는 하위 수준 JavaScript 색상 라이브러리로, 자체 알고리즘을 구축하는 데 유용합니다.

import { oklch, formatHex, interpolate, samples } from 'culori';

const brandOklch = oklch('#2563EB');
const scale = samples(11).map(t => ({
  t,
  color: formatHex({
    mode: 'oklch',
    l: 0.15 + t * 0.82,    // L from 0.15 (dark) to 0.97 (light)
    c: brandOklch.c * Math.sin(t * Math.PI),  // Chroma peaks at mid-tone
    h: brandOklch.h,
  })
}));

ColorBox — Lyft의 오픈 소스 도구(원래 Javier Arce 제작). 곡선 기반 밝기 및 채도 편집기를 사용하여 접근 가능한 색상 스케일을 생성합니다. 이 튜토리얼에서 알고리즘적으로 설명하는 내용에 대한 시각적 UI를 제공합니다.

디자인 도구 통합

Figma 토큰 플러그인 — JSON 형식의 디자인 토큰과 함께 작동합니다. OKLCH 기반 토큰을 정의하고 이를 CSS 사용자 정의 속성에 동기화할 수 있습니다.

스타일 사전 — Amazon의 오픈 소스 디자인 토큰 변환기입니다. 토큰 정의 파일을 CSS 사용자 정의 속성, SCSS 변수, iOS/Android 상수 또는 기타 플랫폼 형식으로 변환하는 데 유용합니다.

{
  "color": {
    "primary": {
      "600": { "value": "oklch(0.55 0.21 264)", "comment": "Brand blue" }
    }
  }
}

온라인 도구

ColorFYI의 Shade Generator는 모든 브랜드 색상에 OKLCH 인식 음영 생성을 적용하고 대비율이 있는 Tailwind 호환 스케일을 출력합니다. 팔레트 생성기는 보색, 삼중 및 유사 색상 조화를 계산합니다.

주요 내용

  • HSL 기반 음영 생성은 HSL의 밝기가 지각적으로 균일하지 않기 때문에 고르지 않은 스케일을 생성합니다. 동일한 L 증분은 색상에 따라 매우 다르게 보입니다.
  • OKLCH 기반 쉐이드 생성은 보정된 L축을 활용하여 시각적으로 균일한 단계를 생성합니다. 채도 감소 곡선을 포함합니다. 자연스럽게 보이는 색조와 음영을 위해 채도를 극단으로 갈수록 채도가 가늘어집니다.
  • 대비 인식 토큰 생성은 WCAG 상대 휘도 공식을 사용하여 각 배경 음영을 최소 대비 통과 텍스트 음영과 연결합니다. 비율이 적절하다고 가정하기보다는 항상 비율을 확인하세요.
  • 브랜드 색상에서 테마 생성은 (1) 브랜드 색상에 고정된 기본 스케일, (2) 낮은 채도에서 동일한 색상을 사용하여 조화롭게 따뜻하고 시원한 중립 스케일, (3) 의미 토큰 할당, (4) OKLCH 색상을 회전하여 생성된 선택적 악센트 컴패니언을 생성합니다.
  • 다크 모드 팔레트 생성은 음영 역할 할당을 반전시키고 채도를 조정합니다. 표면 색상은 거친 채도를 피하기 위해 감소된 채도를 사용합니다. 대화형(버튼, 링크) 색상은 증가된 밝기를 사용하여 어두운 배경에서도 생생하게 유지됩니다.
  • Radix UI ColorsCulori와 같은 라이브러리는 이러한 알고리즘을 프로덕션 품질로 구현합니다. Shade GeneratorPalette Generator를 사용하면 동일한 기술에 코드 없이 대화식으로 액세스할 수 있습니다.

관련 색상

관련 브랜드

관련 도구