본문으로 건너뛰기

© 2026 Molayo

Smashing헤드라인2026. 05. 20. 01:33

Smashing Animations Part 8: CSS Relative Colour를 이용한 애니메이션 테마 설정

요약

CSS의 상대 색상 값(relative colour values)과 OKLCH 색 공간을 활용하여 애니메이션 테마를 효율적으로 설정하는 방법을 다룹니다. 사용자의 상호작용이나 시간대에 따라 색상이 변하는 그래픽을 구현할 때, 수동으로 팔레트를 업데이트하는 번거로움을 줄이고 제어권을 높이는 기술적 접근을 제안합니다.

핵심 포인트

  • CSS 상대 색상 값을 사용하여 기존 색상으로부터 명암(shades)과 색조(tints)를 동적으로 생성할 수 있음
  • OKLCH 색 공간을 활용하여 보다 정교하고 일관된 색상 제어 가능
  • 애니메이션 테마(낮/밤, 계절 등) 변경 시 팔레트 전체를 수동으로 수정해야 하는 번거로움 해결
  • Hanna-Barbera 애니메이션의 재사용 원리를 웹 그래픽 구현에 적용

저는 최근 이 시리즈에서 공유했던 많은 기술들을 실제로 적용하여, 새로운 테마와 선구적인 캐릭터 그룹으로 제 웹사이트의 애니메이션 그래픽을 새롭게 단장했습니다. 제 애니메이션 중 일부는 사용자가 상호작용하거나 하루 중 시간대에 따라 모습이 변합니다.

제 블로그 페이지 상단 그래픽의 색상은 매일 아침부터 밤까지 변합니다. 또한, 오버레이 레이어 (overlay layer)와 블렌딩 모드 (blending mode) 덕분에 차가운 색상과 겨울 테마를 더해주는 눈 모드 (snow mode)도 있습니다.

이 작업을 하면서, CSS 상대 색상 값 (relative colour values)을 사용하면 프로세스를 단순화하면서도 더 많은 제어권을 가질 수 있지 않을까 하는 궁금증이 생겼습니다.

참고: 이 튜토리얼에서는 그래픽과 애니메이션의 테마 설정을 위해 상대 색상 값 (relative colour values)과 이를 위한 OKLCH 색 공간 (colour space)에 집중할 것입니다. 만약 상대 색상 (relative colour)에 대해 깊이 있게 파고들고 싶다면, Ahmad Shadeed가 제작한 훌륭한 인터랙티브 가이드를 참고하세요. 색 공간 (colour spaces), 색역 (gamuts), 그리고 OKLCH에 대해서는 저희의 Geoff Graham이 작성한 글이 있습니다.

Smashing Animations Part 1: 클래식 만화가 현대 CSS에 영감을 주는 방식
Smashing Animations Part 2: CSS 마스킹 (Masking)이 어떻게 추가적인 차원을 더할 수 있는가
Smashing Animations Part 3: SMIL은 죽지 않았다, SMIL은 죽지 않았다
Smashing Animations Part 4: SVG 최적화하기
Smashing Animations Part 5: <symbol>, <use>, 그리고 CSS 미디어 쿼리 (Media Queries)를 사용하여 적응형 SVG 구축하기
Smashing Animations Part 6: <use>와 CSS 사용자 정의 속성 (Custom Properties)을 이용한 멋진 SVG
Smashing Animations Part 7: CSS와 SVG로 만화 텍스트 재현하기

만화 애니메이션이 모든 것을 재사용하는 법을 가르쳐준 방식

제가 성장하며 보았던 Hanna-Barbera 애니메이션 시리즈는 William Hanna와 Joseph Barbera가 MGM Cartoons에서 Tom and Jerry 단편들을 제작할 때 사용했던 예산보다 훨씬 적었습니다. 이는 애니메이터들이 비용 제한을 극복하기 위한 기술들을 개발해야 했음을 의미합니다.

요소의 반복적인 사용이 핵심이었습니다. 배경은 가능한 한 재사용되었으며, 줌(zoom)과 오버레이(overlay)를 활용하여 동일한 아트워크로부터 새로운 장면을 구성했습니다. 이는 필요에 의해 탄생한 방식이었지만, 개별 장면보다는 시리즈 관점에서 생각하도록 독려하기도 했습니다.

컬러 팔레트를 수동으로 업데이트할 때 발생하는 문제

제 도전 과제에 대해 바로 말씀드리겠습니다. 1959년 Yogi Bear Show의 에피소드인 “Lullabye-Bye Bear”를 기반으로 한 이 Toon Titles와 같은 작업물, 그리고 저의 전반적인 작업에서 컬러 팔레트는 엄선된 몇 가지 색상으로 제한됩니다.

저는 더 많은 색조(hue)를 추가하지 않고도 팔레트를 확장하기 위해, 제가 "기초(foundation)" 색상이라고 부르는 색으로부터 명암(shades)과 색조(tints)를 만들어냅니다.

Sketch에서 작업할 때 저는 HSL 색 공간(colour space)을 사용하므로, 이 과정은 기초 색상의 밝기(lightness) 값을 높이거나 낮추는 작업을 포함합니다. 솔직히 말해서 아주 힘든 작업은 아닙니다. 하지만 다른 기초 색상을 선택하려면 완전히 새로운 명암과 색조 세트를 만들어야 합니다. 이를 수동으로 반복해서 수행하는 것은 금방 고된 일이 됩니다.

앞서 HSL — H (hue, 색상), S (saturation, 채도), 그리고 L (lightness, 밝기) — 색 공간을 언급했지만, 이는 색을 설명하는 여러 방법 중 하나일 뿐입니다.

RGB — R (red, 빨강), G (green, 초록), B (blue, 파랑) — 는 아마도 Hex 형태를 포함하여 가장 친숙한 방식일 것입니다.

또한 LAB — L (lightness, 밝기), A (green–red, 초록-빨강), B (blue–yellow, 파랑-노랑) — 모델과, 더 최신이면서 현재 널리 지원되는 LCH — L (lightness, 밝기), C (chroma, 채도), H (hue, 색상) — 모델의 OKLCH 형태도 있습니다. LCH, 특히 CSS에서의 OKLCH를 사용하면 기초 색상의 밝기 값을 조절할 수 있습니다.

또는 *채도(chroma)*를 변경할 수도 있습니다. LCH의 chroma와 HSL의 saturation은 모두 색상의 강도나 풍부함을 설명하지만, 그 방식은 서로 다릅니다. LCH는 저에게 더 넓은 범위와 색상 간의 더 예측 가능한 혼합(blending)을 제공합니다.

또한 색상(hue)을 변경하여 동일한 밝기와 채도 값을 공유하는 컬러 팔레트를 만들 수도 있습니다. HSL과 LCH 모두 색상 스펙트럼은 빨강에서 시작하여 초록과 파랑을 거쳐 다시 빨강으로 돌아옵니다.

왜 OKLCH가 색상에 대한 나의 생각을 바꾸었는가

Sketch를 포함한 디자인 도구들이 아직 따라오지 못했을지라도, OKLCH 색 공간 (colour space)에 대한 브라우저 지원은 이제 광범위하게 이루어지고 있습니다. 다행히 그것이 여러분의 OKLCH 사용을 막지는 못할 것입니다. 브라우저는 Hex, HSL, LAB, RGB 값을 여러분을 위해 기꺼이 OKLCH로 변환해 줄 것입니다. 여러분은 Hex를 포함한 어떤 색 공간에서든 기초 색상 (foundation colour)을 사용하여 CSS 사용자 정의 속성 (custom property)을 정의할 수 있습니다:

/* 기초 색상 (Foundation colour) */
--foundation: #5accd6;

그로부터 파생된 모든 색상은 자동으로 OKLCH로 변환됩니다:

--foundation-light: oklch(from var(--foundation) [...]; }
--foundation-mid: oklch(from var(--foundation) [...]; }
--foundation-dark: oklch(from var(--foundation) [...]; }

디자인 시스템으로서의 상대 색상 (Relative Colour)

상대 색상을 다음과 같이 생각해보세요: “이 색상을 가져와서, 약간 조정한 다음, 그 결과를 나에게 줘.” 색상을 조정하는 방법에는 두 가지가 있습니다: 절대적 변화 (absolute changes)와 비례적 변화 (proportional changes)입니다. 코드상으로는 비슷해 보이지만, 기초 색상을 교체하기 시작하면 매우 다르게 동작합니다. 이 차이를 이해하는 것이 상대 색상 사용을 하나의 시스템으로 바꿀 수 있는 핵심입니다.

/* 기초 색상 (Foundation colour) */
--foundation: #5accd6;

예를 들어, 제 기초 색상의 밝기 (lightness) 값은 0.7837인 반면, 더 어두운 버전의 값은 0.5837입니다. 그 차이를 계산하기 위해 높은 값에서 낮은 값을 뺀 후, 그 결과를 calc() 함수를 사용하여 적용합니다:

--foundation-dark:
oklch(from var(--foundation)
calc(l - 0.20) c h);

더 밝은 색상을 얻으려면 대신 그 차이를 더해줍니다:

--foundation-light:
oklch(from var(--foundation)
calc(l + 0.10) c h);

채도 (Chroma) 조정도 동일한 과정을 따릅니다. 기초 색상의 채도를 0.1035에서 0.0035로 줄이려면, 한 값에서 다른 값을 뺍니다:

oklch(from var(--foundation)
l calc(c - 0.10) h);

색조 (hues) 팔레트를 만들기 위해, 기초 색상의 색조 값(200)과 새로운 색조(260) 사이의 차이를 계산합니다:

oklch(from var(--foundation)
l c calc(h + 60));

그러한 계산은 절대적(absolute)입니다. 고정된 값을 뺄 때, 저는 사실상 *"항상 이만큼을 빼라"*라고 말하는 것과 같습니다. 고정된 값을 더할 때도 마찬가지입니다:

calc(c - 0.10)
calc(c + 0.10)

저는 이 방식의 한계를 고통스럽게 배웠습니다. 고정된 채도(chroma) 값을 빼는 방식에 의존했을 때, 파운데이션(foundation) 색상을 변경하자마자 색상들이 회색 쪽으로 무너져 내렸습니다. 한 색상에는 잘 작동하던 팔레트가 다른 색상에서는 완전히 망가져 버린 것입니다.

곱셈은 다르게 동작합니다. 채도를 곱할 때, 저는 브라우저에게 *"이 색상의 강도를 일정 비율로 줄여라"*라고 말하는 것입니다. 파운데이션이 바뀌더라도 색상 간의 관계는 온전하게 유지됩니다:

calc(c * 0.10)

나의 Move It, Scale It, Rotate It 규칙

Move 밝기(lightness)를 이동시키고(더하거나 빼기), Scale 채도(chroma)를 조절하며(곱하기), Rotate 색상(hue)을 회전시킵니다(도(degree) 단위로 더하거나 빼기).

제가 채도를 조절(scale)하는 이유는 강도의 변화가 기본 색상에 비례하여 유지되기를 원하기 때문입니다. 색상(hue) 관계는 회전적(rotational)이므로 색상에 곱셈을 하는 것은 의미가 없습니다. 밝기(lightness)는 지각적(perceptual)이고 절대적이어서, 이를 곱하면 종종 이상한 결과가 발생합니다.

하나의 색상에서 전체 테마로

상대적 색상(Relative colour)을 사용하면 파운데이션 색상을 정의하고, 그로부터 필요한 모든 다른 색상들—채우기(fills), 선(strokes), 그라디언트 정지점(gradient stops), 그림자(shadows)—을 생성할 수 있습니다. 이 시점에서 색상은 더 이상 단순한 팔레트가 아니라 하나의 시스템이 됩니다.

SVG 일러스트레이션은 채우기, 선, 그라디언트에 걸쳐 동일한 몇 가지 색상을 재사용하는 경향이 있습니다. 상대적 색상을 사용하면 이러한 관계를 한 번만 정의하고 어디에서나 재사용할 수 있습니다. 이는 마치 애니메이터들이 새로운 장면을 만들기 위해 배경을 재사용하는 것과 매우 유사합니다.

파운데이션 색상을 한 번만 바꾸면, 수동으로 다시 계산할 필요 없이 파생된 모든 색상이 자동으로 업데이트됩니다. 애니메이션 그래픽 외에도, 버튼이나 링크와 같은 상호작용 요소(interactive elements)의 상태(states)를 정의하는 데에도 이와 동일한 접근 방식을 사용할 수 있습니다.

제가 “Lullabye-Bye Bear” Toon Title에서 사용한 기초 색상(foundation colour)은 청록색 느낌이 나는 파란색입니다. 배경은 이 기초 색상과 더 어두운 버전 사이의 방사형 그라데이션(radial gradient)으로 구성됩니다.

완전히 다른 분위기를 가진 대안 버전을 만들기 위해, 저는 기초 색상만 변경하면 됩니다:

--foundation: #5accd6;
--grad-end: var(--foundation);
--grad-start: oklch(from var(--foundation)
...

색상 값을 중복해서 입력하지 않고 이러한 사용자 정의 속성(custom properties)을 SVG 그라데이션에 연결하기 위해, 하드코딩된 stop-color 값을 인라인 스타일(inline styles)로 교체했습니다:

<defs>
<radialGradient id="bg-grad" […]>
<stop offset="0%" style="stop-color: var(--grad-end);" />
...
<path fill="url(#bg-grad)" fill="#5DCDD8" d="[...]"/>

다음으로, 제가 선택한 어떤 기초 색상과도 Toon Text가 항상 대비(contrast)를 이루도록 보장해야 했습니다. 180deg 색상 회전(hue rotation)은 확실히 눈에 띄는 보색(complementary colour)을 만들어내지만, 불쾌한 진동(vibrate)을 일으킬 수 있습니다:

.text-light {
fill: oklch(from var(--foundation)
l c calc(h + 180));
...

90° 이동은 완전히 보색은 아니면서도 선명한 2차 색상(secondary colour)을 만들어냅니다:

.text-light {
fill: oklch(from var(--foundation)
l c calc(h - 90));
...

Quick Draw McGraw의 1959년 Toon Title인 “El Kabong“을 재현할 때는 동일한 기술을 사용하되 더 다양한 팔레트(palette)를 적용했습니다. 예를 들어, 기초 색상과 더 어두운 색조(shade) 사이에 또 다른 방사형 그라데이션이 존재합니다.

배경의 건물과 나무는 단순히 동일한 기초 색상의 서로 다른 색조들입니다. 해당 경로(paths)들을 위해 두 개의 추가적인 fill 색상이 필요했습니다:

.bg-mid {
fill: oklch(from var(--foundation)
calc(l - 0.04) calc(c * 0.91) h);
...

기초(Foundations)가 움직이기 시작할 때

지금까지 제가 보여드린 모든 것은 정적(static)이었습니다. 누군가가 컬러 피커(colour picker)를 사용하여 기초 색상을 변경하더라도, 그 변화는 즉각적으로 일어납니다. 하지만 애니메이션 그래픽은 좀처럼 가만히 있지 않습니다. 이름 자체에 그 힌트가 들어있죠. 따라서 색상이 시스템의 일부라면, 색상 또한 애니메이션화되지 못할 이유가 없습니다.

기초 색상 (foundation colour)을 애니메이션화하기 위해, 먼저 이를 OKLCH 채널인 밝기 (lightness), 채도 (chroma), 색상 (hue)으로 분리해야 합니다. 하지만 중요한 추가 단계가 있습니다. 바로 이 값들을 타입이 지정된 (typed) 사용자 정의 속성 (custom properties)으로 등록해야 한다는 것입니다. 그런데 이것이 무엇을 의미할까요?

기본적으로 브라우저는 CSS 사용자 정의 속성 값이 색상 (colour), 길이 (length), 숫자 (number) 또는 완전히 다른 무엇을 나타내는지 알지 못합니다. 이는 종종 애니메이션 중에 값들이 부드럽게 보간 (interpolate)되지 못하고, 한 값에서 다음 값으로 갑자기 튀는 현상을 초래합니다.

사용자 정의 속성을 등록하면 브라우저에게 해당 속성이 나타내는 값의 타입과 시간이 지남에 따라 어떻게 동작해야 하는지를 알려주게 됩니다. 이 경우, 저는 브라우저가 제 색상 채널들을 숫자 (numbers)로 취급하여 부드럽게 애니메이션화할 수 있도록 하고 싶습니다.

@property --f-l {
  syntax: "<number>";
  inherits: true;
  ...
}

등록이 완료되면, 이러한 사용자 정의 속성들은 네이티브 CSS처럼 동작합니다. 브라우저는 이를 프레임 단위로 보간할 수 있습니다. 그런 다음 저는 해당 채널들로부터 기초 색상을 다시 구축합니다.

--foundation: oklch(var(--f-l) var(--f-c) var(--f-h));

이를 통해 기초 색상은 다른 모든 숫자 값과 마찬가지로 애니메이션화가 가능해집니다. 다음은 시간이 지남에 따라 밝기를 부드럽게 변화시키는 간단한 "호흡 (breathing)" 애니메이션입니다.

@keyframes breathe {
  0%, 100% { --f-l: 0.36; }
  50% { --f-l: 0.46; }
  ...
}

채우기 (fills), 그라데이션 (gradients), 선 (strokes)에 사용되는 다른 모든 색상이 --foundation으로부터 파생되기 때문에, 이들은 모두 함께 애니메이션화되며 아무것도 수동으로 업데이트할 필요가 없습니다.

하나의 애니메이션 색상, 다양한 효과

이 과정을 시작할 때, 저는 CSS 상대 색상 (relative colour) 값이 구현을 더 단순하게 만들면서도 더 많은 가능성을 제공할 수 있을지 궁금했습니다. 최근 제 웹사이트의 연락처 페이지에 새로운 황금 광산 배경을 추가했는데, 첫 번째 버전에는 빛나며 흔들리는 기름 등불이 포함되어 있었습니다.

저는 CSS 상대 색상 (Relative Colour)을 애니메이션화하여 램프의 색상으로 광산 내부를 착색함으로써, 어떻게 하면 광산 내부를 더 사실적으로 만들 수 있을지 탐구하고 싶었습니다. 실제 빛이 그러하듯, 램프가 주변 환경에 영향을 미치기를 원했습니다. 그래서 여러 색상을 애니메이션화하는 대신, 단 하나의 색상만을 애니메이션화하는 아주 작은 조명 시스템을 구축했습니다.

저의 첫 번째 작업은 배경과 램프 사이에 오버레이 (overlay) 레이어를 끼워 넣는 것이었습니다:

<path
id="overlay"
fill="var(--overlay-tint)"
...

저는 mix-blend-mode: color를 사용했는데, 이는 하단의 휘도 (luminance)를 유지하면서 그 아래에 있는 요소에 색을 입히기 때문입니다. 애니메이션이 켜져 있을 때만 오버레이가 보이도록 하고 싶었기에, 오버레이를 선택 사항 (opt-in)으로 만들었습니다:

.svg-mine #overlay {
display: none;
}
...

오버레이는 배치되었지만, 아직 램프와 연결되지는 않았습니다. 광원 (light source)이 필요했습니다. 제 램프는 단순하며, 각 램프는 필터 (filter)로 블러 (blur) 처리를 한 circle 요소를 포함하고 있습니다. 이 filter는 원 전체에 매우 부드러운 블러를 생성합니다.

<filter id="lamp-glow-1" x="-120%" y="-120%" width="340%" height="340%">
<feGaussianBlur in="SourceGraphic" stdDeviation="56"/>
</filter>

AI 자동 생성 콘텐츠

본 콘텐츠는 Smashing Magazine의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0