웹페이지가 하나의 빛이 되기까지
효과 10개를 따로 만들다가, 전부 하나로 합쳤습니다.
처음엔 빛이 10개의 따로 노는 효과였습니다. 히어로 글로우, 제품 글로우, 무드 글로우… 각자 예뻤죠. 그런데 그걸 하나의 마스터 빛으로 합치고 나니, 페이지를 내려가는 동안 같이 흐르는 한 줄기 빛이 됐어요. 메인 이미지 뒤로 빛이 새어나오고(앰비라이트), 스크롤하면 배경이 그 구역의 빛으로 바뀌고, 디머를 당기면 방이 밝아집니다. 아래 5개 데모는 전부 실제로 작동합니다 — 직접 만져보세요.
의뢰가 아니라, 한 문장에서 시작됐습니다. “각 화면은 단 하나의 감정과 하나의 행동.” 조명 브랜드라면 그 감정은 당연히 빛이었고요. 그래서 빛을 하나 만들고, 또 하나 만들고… 어느새 효과가 열 개가 됐습니다. 문제는 그게 열 개의 멋진 효과였지, 하나의 빛은 아니었다는 거예요.
이 글은 그 열 개를 하나로 합쳐가는 과정입니다. 솔직히 — 한 번에 안 됐어요. 자꾸 “여기에 효과 하나 더”를 붙였고, 그때마다 방향을 다시 잡았습니다. 그 수정들이 결국 같은 말이었어요: 부분에 장식을 붙이지 말고, 전체가 하나로 움직이게.
01TV 뒤에 빛을 붙이다 — 앰비라이트
가장 먼저 잡은 건 메인 이미지였습니다. 그냥 사진을 박으면 거기서 끝이에요. 그런데 실제 Hue는 TV 뒤에 빛을 둬서 화면의 빛이 벽으로 번지죠(바이어스 라이트). 그걸 웹으로 옮겼습니다 — 이미지 위·아래로 색이 새어나오는 한 겹의 빛. overflow:hidden에 안 잘리게 이미지 뒤·밖에 깔았어요.
오른쪽 — 이미지 위·아래로 빛이 벽으로 번집니다(TV 뒤 바이어스 라이트).
</>코드 엿보기 — 잘리지 않는 글로우
/* 이미지(.hero-stage)는 overflow:hidden — 글로우는 그 뒤/밖 레이어에 */ .hero-ambi{position:relative} .ambi-glow{position:absolute;left:3%;right:3%;top:-7%;bottom:-7%;z-index:0; filter:blur(48px); background:linear-gradient(180deg, color-mix(in srgb,var(--th-b) 60%,transparent), color-mix(in srgb,var(--th-c) 42%,transparent) 38%, color-mix(in srgb,var(--th-d) 62%,transparent))} .hero-ambi .hero-stage{position:relative;z-index:1} /* 이미지가 글로우 위에 */
02이미지가 흐르면, 주변도 같이
여기서 한 번 틀렸습니다. 이미지 주변(앰비라이트)만 색이 돌고, 이미지 자체는 정지였거든요. “메인 이미지도 색이 변해야 주변도 같이 변하지” — 맞는 말이었어요. 그래서 이미지와 그 뒤 빛을 같은 시계(같은 속도·위상)로 hue를 돌렸습니다. 둘이 한 호흡으로 흐르죠.
화면(중앙)과 그 뒤 빛(밖)이 같은 속도로 색을 갈아입습니다.
</>코드 엿보기 — 한 시계로 묶기
/* 이미지와 앰비라이트를 같은 keyframe·같은 duration으로 → 같은 위상 */ @keyframes hueSpin{ from{filter:hue-rotate(0)} to{filter:hue-rotate(360deg)} } .hero-stage > img{ animation: hueSpin 34s linear infinite; } .ambi-glow { animation: hueSpin 34s linear infinite; } /* 제품 호버 시엔 --sync-d로 위상까지 맞춰 '같이' 돈다 */
03같은 집, 무드는 무한 — 배경이 자동으로
“공간 무드” 섹션에선 또 한 번 배웠어요. 처음엔 카드에 테두리 글로우를 붙였는데 — 그게 아니었습니다. “테두리가 아니라 배경이, 그것도 자동으로 변해야지.” 그래서 그 구역에 들어오면 페이지 배경이 거실→침실→홈시어터→작업→파티로 스스로 갈아입게 했습니다. 호버가 아니라 자동.
아무것도 안 해도 배경이 무드를 따라 스스로 흐릅니다.
</>코드 엿보기 — 잠금·호버 시엔 양보
/* 구역이 보이면 타이머가 무드 팔레트를 순환. 단, 사용자가 색을 고르면(잠금) 양보 */ function paint(){ if(!current || hue.locked) return; // 씬·제품 클릭 시 멈춤 if(document.querySelector('[data-theme]:hover')) return; // 호버 중 양보 hue.set(ZONES[current][idx++ % set.length], {live:false}); // 0.9s 부드럽게 } setInterval(paint, 4600);
04거실 밝기 — 손끝으로 방을 밝히다
앱 목업 안엔 “거실 밝기” 막대가 있었는데, 그냥 그려놓은 장식이었어요. 안 움직였죠. 그걸 진짜 슬라이더로 만들고, 밝기를 배경 광량에 연결했습니다. 당기면 방이 밝아지고, 내리면 가라앉아요. (까다로웠던 비밀: 의사요소가 상속 변수를 안 받아서, 실제 <style> 규칙으로 광량을 직접 썼습니다.)
</>코드 엿보기 — range → 광량
/* 슬라이더 값(0~100) → 배경 빛의 opacity. 0.35s로 부드럽게 */ function applyBright(pct){ var b = 0.1 + (pct/100)*1.05; // 0.1 ~ 1.15 box.style.setProperty('--bright', b.toFixed(2)); } range.addEventListener('input', () => applyBright(range.value));
05모든 요소 통일 — 하나의 마스터 빛
마지막이 핵심이었어요. 흩어진 빛(이미지·앰비라이트·배경·제품·무드)을 하나의 컨트롤러로 합쳤습니다. 이제 어느 구역을 보든 배경이 그 빛으로 바뀌고, 전부 같은 --th 변수 하나에서 색을 읽어요. 아래에서 빛을 골라보세요 — 박스 전체가 그 빛으로 물듭니다(실제 사이트에선 스크롤이 이걸 자동으로 합니다).
하나를 고르면 박스 전체 빛이 0.9초에 걸쳐 부드럽게 스윕됩니다(@property 색 보간).
</>코드 엿보기 — 하나의 소스(--th)
/* @property로 등록한 색 변수 — 값이 바뀌면 0.9s에 걸쳐 '보간'된다 */ @property --th-a{ syntax:"<color>"; inherits:true; initial-value:#ffc07a } html{ transition:--th-a .9s var(--ease), --th-b .9s, ... } /* 배경·이미지·앰비라이트·제품·마우스빛이 전부 var(--th-*)를 읽음 → 한 번 바꾸면 다 같이 */ html[data-scene="party"]{ --th-a:#ff4db0; --th-b:#7b3ff2; --th-c:#15c8c0; --th-d:#2f6bff }
화려함보다 먼저, 정직함
빛을 다룬다고 진실을 흐리진 않았습니다.
- Philips Hue는 제안 콘셉트 — 공식 사이트가 아니며 가짜 수치·후기·로고 0.
- 모든 데모는 실제로 작동하는 코드. 스크린샷이 아니라 손으로 만지는 것.
prefers-reduced-motion이면 모든 빛이 멈춥니다 — 움직임에 민감한 분 배려.- 엄격 CSP(인라인 스크립트 0, 외부 동일출처 JS) · 키보드·포커스·대비 접근성.
06그래서 배운 것
제 본능은 자꾸 “효과 하나 더”로 갔어요. 그게 빠르고 안전하니까. 그런데 이 작업이 가르쳐준 건 반대였습니다 — 덜어내고, 통일하고, 자동으로 흐르게. 부분의 합이 아니라 하나의 유기체. 그래서 이제 새 페이지를 만들 땐 효과를 더하기 전에 먼저 묻습니다. “이 페이지가 하려는 한 문장은 뭐고, 이 요소는 그 문장의 일부인가, 곁다리인가.”
완성본은 스크롤이 곧 빛입니다
위 다섯 조각이 실제로 하나로 흐르는 걸 보려면 — 완성본을 위에서 아래로 훑어보세요.