디자인에서 우리는 일관성(consistency), 그리드, 정렬에 집착하며 몇 시간을 보내는 것을 좋아합니다. 하지만 네이티브 CSS random() 함수가 Safari에 탑재되었고 Chrome 156으로 출시되고 있다는 소식을 듣자, 저는 궁금해지기 시작했습니다. 웹에서 **무작위성(randomness)**은 실제로 어디에 의미가 있을까? 이것을 디자인 시스템에 포함해야 할까요? 알아내기 위해, 저는 늘 하던 대로 의심 속에서 여러 가지 재미있는 CodePen 데모를 만들었습니다. 유기적인 타이포그래피, 튀어 오르는 글자들, 퍼진 카드들, 그리고 펑크 팬지인을 탐험했습니다.
디자인 시스템을 구축하고 CSS 아키텍처를 만지는 것을 좋아하는 사람으로서, 제 직업의 큰 부분은 통제(control)에 관한 것입니다. 우리는 간격 스케일(spacing scales)을 설정하고, 타이포그래피를 고정하며, 컬러 토큰(color tokens)을 설정하고, 모든 것이 그리드에 맞춰 정렬되도록 합니다. 일관성은 디자인 엔지니어에게 보통 주식이자, 디자인 시스템을 만들 때 자랑스러운 기술의 증표입니다. 만약 카드가 새로고침될 때 갑자기 왼쪽으로 20픽셀씩 점프한다면, 그것은 창의적인 표현이 아니라 프로젝트 매니저가 기록할 버그일 것입니다.
그래서 네이티브 random() 함수가 CSS Values and Units Level 5 사양에서 나와 Safari에 탑재되었고 현재 Chrome 156으로 출시되고 있으며 (Mozilla가 Firefox에서도 긍정적인 신호를 보내고 있습니다), 저의 첫 반응은 다음과 같았습니다. 꽤 멋지다!
그리고 두 번째 반응은 다음과 같았습니다. 잠깐... 이걸 가지고 난장판을 만들지 않고 실제로 어디에 사용할 수 있을까? 디자이너들에게 무작위성이 디자인 시스템에 좋은 아이디어일 수 있다고 어떻게 설득할 수 있을까?
솔직히 말해서, 지난 10년간 웹을 더 예측 가능하게 만들려고 노력해 왔는데, 왜 우리가 스타일시트에서 무작위성을 원할까요? 플렉스박스(flexbox)나 유동 타이포그래피(fluid typography)를 사용하더라도, 우리는 여전히 모든 것을 가능한 한 정렬되고 균일하게 만들려고 합니다.
완벽하게 곧지 않은 것들의 매력
잠시 브라우저에서 벗어나 주변의 물리적인 사물들을 본다면, 아무것도 진정으로 균일한 것은 없습니다.
오래된 레터프레스 인쇄물을 생각해 보세요. 잉크가 종이 결에 살짝 번지고, 어떤 글자는 미세하게 기울어져 있으며, 기준선(baseline)이 항상 곧지는 않습니다. 아니면 커피 테이블 위에 떨어진 폴라로이드 사진 한 묶음을 생각하거나요. 그 사진들 역시 완벽하지 않고 아마도 똑바로 놓여있지 않을 겁니다. 혹은 밤거리를 비추는 깜빡이는 가로등을 생각해 보세요. 그런 작은 아날로그적인 불완전함이야말로 사물을 살아있게, 유기적으로, 그리고 인간답게 느끼게 만듭니다.
웹에서도 우리는 픽셀 단위의 완벽함을 너무 잘하게 되어서, 때로는 우리의 인터페이스가 다소 무균실처럼 느껴지기도 합니다. 특히 LLM(대규모 언어 모델)에 의해 급조된 디자인을 접할 때는 더욱 그렇습니다. 보통은 10년 전 우리가 봤던 것과 비슷한 종류의 디자인으로 귀결되기 일쑤입니다. 모든 것이 각지고, 정렬되어 있으며, 깔끔합니다. 작동은 하지만 솔직히 말해 다소 지루해지기도 합니다.
CSS에서의 무작위성(Randomness)이 반드시 당신의 디자인 시스템을 통째로 버려야 한다는 의미는 아닙니다. 그것은 작은 불완전함과 장난스러움에 관한 것입니다. 어쩌면 이것을 재미라고 부르는 게 나을지도 모르겠습니다.
실제로 이것이 어떻게 보이는지 확인하기 위해 CodePen에서 몇 시간을 만지작거렸습니다. 데모들이 다소 고립되어 느껴질 수도 있지만, 많은 디자이너들이 이 통제된 혼란(controlled chaos) 속에서 최고의 결과물을 만들어낼 수 있다고 확신합니다. 하지만 그 데모들을 보기 전에, 문법부터 살펴보겠습니다.
CSS random() 함수 작동 방식
The random() 함수는 CSS Values and Units Module Level 5에 명시되어 있습니다. JavaScript나 빌드 타임 Sass 루프가 필요 없이 CSS 내에서 유사 무작위 값(pseudo-random value)을 생성합니다.
공식 사양은 다음과 같습니다:
random( <random-caching-key>?, <min>, <max> [, by <step>]? )
실제 사용과 현재 브라우저 구현 전반에 걸쳐는 쉼표로 구분된 인자만 작성하면 됩니다:
/* -3deg와 3deg 사이의 무작위 각도 */
rotate: random(-3deg, 3deg);
...
단위 일치(Matching Units)
대부분의 사람들에게는 놀랍지 않겠지만, 최소값과 최대값이 정확히 같은 단위 유형을 공유해야 한다는 점을 언급하고 싶었습니다.
CSS 파서는 어떤 차원(dimension)을 다루고 있는지 알아야 합니다. 길이(lengths), 각도(angles), 시간(time), 백분율(percentages), 또는 단위가 없는 일반 숫자(plain unitless numbers)를 사용할 수 있습니다:
/* 좋음: 둘 다 각도임 */
rotate: random(-4deg, 4deg);
...
만약 여러 단위를 결합해야 한다면, 인자 내부에서 calc() 함수를 사용해야 각 측면이 올바르게 해결됩니다.
단계 및 반올림 (Stepping And Rounding)
선택적인 세 번째 인자는 단계 크기(step size)입니다. 이를 추가하지 않으면 브라우저가 연속 범위(continuous range)에서 값을 선택합니다. 하지만 단계를 제공하면, 무작위 값(random value)을 이산 증분(discrete increments)으로 반올림합니다:
/* 0px, 15px, 30px, 45px, 또는 60px만 생성함 */
top: random(0px, 60px, 15px);
이것은 요소들이 약간씩 움직이게 하되, 여전히 미리 정의된 그리드에 맞춰 착지하기를 원하는 경우에 유용할 수 있습니다.
캐싱 키 (Caching Keys): 동기화 유지하기
일반적으로 random() 함수가 속성(property)에서 평가될 때마다 독립적인 무작위 값을 생성합니다. 하지만 inline-size와 block-size를 무작위로 설정하는 박스를 만들면서도, 요소가 완벽한 정사각형으로 유지되도록 두 값이 일치해야 하는 예시가 있을 수 있습니다.
이때 **캐싱 키(caching key)**가 사용됩니다:
.card-thumbnail {
/* 두 속성이 --square-size 키를 공유함 */
inline-size: random(--square-size, 60px, 120px);
...
두 선언(declarations) 모두 해당 요소의 --square-size를 참조하기 때문에, 브라우저 엔진은 이들 사이에 동일한 계산된 값(computed value)을 공유합니다.
실제 작동 방식을 보여주기 위해 작은 예시를 만들었습니다. 상단 행에서는 타일들이 캐싱되지 않은 차원을 가져 예측할 수 없고 불일치하는 직사각형으로 왜곡됩니다. 하단 행에서는 inline-size와 block-size가 --square-size를 공유하여, 각 타일이 완벽한 1:1 정사각형을 유지하도록 보장합니다. CodePen을 몇 번 새로고침하고 아래 카드들이 동일한 무작위성을 유지하는지 확인해 보세요.
See the Pen Uncached vs cached random() [forked] by @utilitybend.
/* Uncached: width and height resolve independently, creating random rectangles */
.tile-uncached {
inline-size: random(4.2rem, 8.5rem);
...
Browser Support And Progressive Enhancement
오늘 어디서 테스트할 수 있나요?
- WebKit / Safari: Safari가 가장 먼저 지원을 구현했습니다.
- Chrome / Edge: Chrome 156부터 배포되었으며 현재 순차적으로 적용 중입니다.
- Firefox: 표준 트랙에서 긍정적인 신호가 있어, 그리 멀지 않을 것으로 기대됩니다.
아직 배포 단계에 있으므로, 실험 내용은 @supports로 감싸는 것이 좋습니다:
/* Baseline for all browsers */
.badge {
rotate: 0deg;
...
미래의 random-item 함수
명세(spec)에는 random-item()이라는 또 다른 함수가 있습니다. random()이 숫자 범위를 처리하는 반면, random-item()은 미리 정의된 토큰이나 키워드 목록에서 선택할 수 있게 해줍니다:
/* Picking a random color from your palette */
background-color: random-item(--seed, oklch(75% 0.18 150), oklch(65% 0.22 350), oklch(85% 0.16 85));
이것은 디자인 토큰 세트에서 무작위로 선택하는 데 유용할 수 있습니다. 현재 이 함수는 브라우저 지원이 없지만, 기대해 볼 만한 기능임에는 틀림없습니다.
이제 실질적인 부분으로 넘어가겠습니다.
Demo 1: 문자를 무작위화하기
제가 이 기능을 가지고 놀기 시작했을 때, 애니메이션을 이용한 타이포그래피 트릭도 함께 하고 있었습니다. 그래서 첫 번째 생각이 떠오른 곳은 바로 '타이포그래피'였습니다! 단어에 아주 작은 불완전함을 추가하면 어떨까?
빈티지 책 표지를 생각해 보세요. 글자들이 몇 마일씩 어긋나지는 않았지만, 작은 불완전함들이 있습니다.
See the Pen Imperfection random() [forked] by @utilitybend.
여기에 글자에 힘을 실어주는 CSS가 있습니다:
또한 작은 호버 효과를 추가했습니다. 읽는 동안 단어가 장난기 넘치는 느낌을 주지만, 마우스를 올리는 순간 모든 문자가 원래 위치로 돌아옵니다.
스크린 리더 사용자를 위해 부탁드립니다
여기에 간단한 접근성 경고가 있습니다. 만약 각 글자를 스타일링하기 위해 단어를 개별 <span> 요소로 분할하면, 스크린 리더는 이를 개별 문자로 읽을 가능성이 높습니다: “i - m - p - e - r - f - e - c - t - i - o - n” 또는 각 문자 사이에 어색하게 멈춥니다. 이는 최악의 사용자 경험입니다.
항상 시각적 span 요소는 보조 기술(assistive tech)에서 숨기고 전체 단어를 제공해야 합니다:
<h1 class="word" aria-label="Imperfection">
<span class="word-inner" aria-hidden="true">
<span class="char">i</span>
...
스크린 리더 사용자는 깔끔한 텍스트를 얻고, 시각 장애가 없는 사용자(sighted users)는 재미있는 시각적 특이점을 얻게 됩니다.
데모 2: 문자를 애니메이션화하기: 순수 절차적 바운스 (Pure Procedural Bounce)
글자들이 작은 기울임과 함께 놓여 있는 것을 보고, 저도 참지 못했습니다. 만약 이것을 애니메이션화하면 어떨까?
바운스 키프레임을 만지작거리기 시작했습니다. 그저 장난치는 것이었지만, 결과물은 저에게 작은 미소를 안겨주었습니다. 모든 글자를 정확히 같은 타이밍으로 애니메이션화하면 우리가 이전에 본 깔끔한 사인파(sine wave)가 나옵니다. 하지만 각 글자가 자체적인 점프 높이, 자체적인 지속 시간, 그리고 자체적인 착지 압축(landing squash)을 갖게 되면, 갑자기 단어는 트램펄린 위에서 뛰어다니는 작은 문자들의 묶음처럼 보이게 됩니다.
@utilitybend의 Pen Bouncy imperfection with CSS random() [forked]을 확인해 보세요.
바운스 매개변수들이 어떻게 연결되었는지 여기를 보세요:
@layer demo {
.char {
display: inline-block;
...
음수 애니메이션 지연 시간(--delay: random(-3s, 0s))에 주목하세요.
양수(positive) 지연 시간을 설정하면, 글자들이 페이지 로드 시 멈춰 서서 타이머가 만료될 때까지 기다린 후 점프합니다. 음수(negative) 지연 시간을 사용하면, 브라우저는 애니메이션이 이미 몇 초 동안 실행된 것처럼 착각합니다. 페이지가 로드되는 순간, 모든 글자는 이미 각자의 리듬에 맞춰 튀어 오릅니다.
데모 3: 기울어진 카드와 동적 그림자
저희는 카드를 많이 만들었습니다. 정말 많은 카드요... 포트폴리오 그리드, 블로그 요약, 기능 목록 등. 그리고 대부분의 경우, 이들은 세 개나 네 개의 깔끔하고 지루한 열에 놓여 있습니다.
대시보드의 경우, 이것이 정확히 원하는 바입니다. 하지만 라이프스타일 포트폴리오, 아동 웹사이트 또는 휴가 사진 모음집을 만들고 있다면, 좀 더 장난스러워질 수 있습니다. 다음 예제에서 저는 바로 그렇게 했습니다:
Random card tilt [forked]을 @utilitybend 님에게서 확인해 보세요.
제가 이것을 구성한 방법은 다음과 같습니다:
@layer demo {
.card {
/* 부드러운 무작위 기울임과 수직 이동 */
...
예제에서 그림자를 가지고 노는 것은 재미있는 실험이었습니다. 앞으로도 이와 관련된 더 많은 효과를 시도해 볼 것 같습니다.
데모 4: sibling-index와 스크롤을 사용한 펼쳐지는 카드 덱
여기서부터 현대 CSS 기능들이 정말 흥미로워지기 시작합니다. 이전의 기울어진 카드 데모에서는 순수한 무작위성을 사용했습니다. 하지만 스크롤함에 따라 화면 전체에 퍼져 나가야 하는 카드의 더미(stack)가 있다면 어떨까요?
이 예제에서 저는 구조를 위해 수학을 그리고 인간적인 불완전함을 위해 무작위성을 사용하기로 결정했습니다.
Scroll drivin animations with random() tilted cards [forked]을 @utilitybend 님에게서 확인해 보세요.
설정은 다음과 같습니다:
@layer demo {
.deck-card {
position: absolute;
...
이것이 얼마나 깔끔하게 작동하는지 보세요:
sibling-index()는 수학적 부채꼴(fan)을 계산하여 카드들이 중앙 카드를 중심으로 균일하게 퍼지게 하고, 스택에서 높은 위치에 있는 카드들은 더 넓은 그림자를 드리우도록 --elevation을 조정합니다.
calc((sibling-index() - (sibling-count() + 1) / 2) * 12deg);는 카드 개수에 관계없이 작동하도록 하여, 모든 카드가 동일한 12deg만큼 회전하게 만듭니다.
random()은 사람이 손으로 카드를 분배했을 때 얻을 수 있는 미묘한 흔들림(wobble)을 추가합니다.
animation-timeline: scroll(root)는 전체 부채꼴과 그림자 전환이 스크롤바에 연결되도록 합니다.
Demo 5: Random Velocity
브루탈리즘(brutalist) 또는 고급 패션 웹사이트를 본 적이 있다면, 페이지를 아래로 스크롤할 때 수평 마키지(marquee) 텍스트가 화면을 가로질러 떠다니는 것을 보았을 것입니다.
보통 개발자들은 .drift-slow, .drift-fast, .drift-reverse와 같은 클래스를 만들거나, 복잡한 JavaScript 스크롤 리스너를 사용하여 변환(transform) 값을 계산합니다.
하지만 random()과 약간의 수학만으로도 몇 줄 만에 절차적 드리프트 속도(procedural drift speeds)를 얻을 수 있습니다: