Cosmos — 우주 감성을 코드로 담기
우주의 고요함을 브라우저에서 경험하게 하는 힐링 페이지 — CSS 별 필드, Canvas 성운, 그리고 Web Audio 합성 BGM
시작 — 아무것도 하지 않아도 되는 공간
대부분의 인터랙티브 콘텐츠는 뭔가를 하도록 요구합니다. 클릭하거나, 점수를 올리거나, 레벨을 클리어하거나.
Cosmos는 그 반대를 목표로 했습니다. 아무것도 하지 않아도, 그냥 열어두기만 해도 우주 안에 있는 느낌을 주는 공간.
SpaceBackground — CSS 기반 별 필드
배경 별들은 Canvas가 아니라 CSS animation으로 구현했습니다. Canvas보다 CPU 부담이 낮고, 탭 비활성 시 브라우저가 자동으로 애니메이션을 조절해줍니다.
StarField 컴포넌트는 수백 개의 <span> 요소를 생성하고, 각각 랜덤한 위치와 깜빡임 주기를 CSS 변수로 주입합니다.
// 별 하나당 인라인 스타일
style={{
'--star-x': `${Math.random() * 100}vw`,
'--star-y': `${Math.random() * 100}vh`,
'--star-delay': `${Math.random() * 8}s`,
'--star-opacity': `${0.3 + Math.random() * 0.7}`,
} as CSSProperties}
MeteorEffect는 별도 레이어로, 이따금 대각선 방향의 유성이 지나가는 효과입니다. @keyframes로 opacity와 transform을 함께 제어합니다.
Canvas 성운 렌더링
성운의 빛은 여러 radialGradient 레이어를 반투명하게 겹치는 방식으로 표현합니다. 단일 그라디언트로는 성운 특유의 불균일한 발광감을 낼 수 없었습니다.
const drawNebula = (ctx: CanvasRenderingContext2D, cx: number, cy: number) => {
// 3~5개 레이어를 서로 다른 위치/크기/색상으로 겹침
NEBULA_LAYERS.forEach(({ dx, dy, rx, ry, color, alpha }) => {
const grad = ctx.createRadialGradient(cx + dx, cy + dy, 0, cx + dx, cy + dy, rx);
grad.addColorStop(0, `${color}${Math.round(alpha * 255).toString(16)}`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(cx + dx, cy + dy, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
});
};
성운 색상은 실제 성운 사진을 참고해 수소(적색), 산소(청록), 황(황금) 계열로 구성했습니다.
Global BGM — 끊기지 않는 우주 BGM
BGM은 GlobalBGMContext를 통해 layout.tsx 레벨에서 관리합니다. 페이지를 이동해도 음악이 끊기지 않고 이어집니다.
// layout.tsx
<GlobalBGMProvider>
<SearchProvider>
{children}
</SearchProvider>
</GlobalBGMProvider>
게임 페이지들은 usePageBGMMode('page')를 호출해 메인 BGM을 억제합니다. Cosmos는 이 호출이 없으므로 BGM이 자연스럽게 유지됩니다.
BGM 자체는 Web Audio API로 순수 합성합니다. 두 개의 낮은 드론 오실레이터 위에 느린 LFO 변조를 걸어, 우주 공간의 끝없는 울림을 표현했습니다.
// 드론 레이어: 두 주파수 사이의 미묘한 간섭
const osc1 = ctx.createOscillator();
osc1.frequency.value = 55;
const osc2 = ctx.createOscillator();
osc2.frequency.value = 55.7; // 0.7Hz 간섭
// LFO: 볼륨을 0.6 ~ 1.0 사이에서 8초 주기로 느리게 진동
const lfo = ctx.createOscillator();
lfo.frequency.value = 0.125; // 8초 주기
0.7Hz의 아주 작은 주파수 차이가 자연스러운 맥놀이(beating)를 만들어냅니다. 이 떨림이 우주의 진동처럼 느껴집니다.
OrbitCaption — 우주 명언 타이핑 효과
화면 하단에는 우주에 관한 문장들이 한 글자씩 타이핑되어 나타납니다. OrbitCaption 컴포넌트가 순차 체인 방식으로 처리합니다.
각 문장은 타이핑 → 표시 → 페이드아웃 → 간격 → 다음 문장 순으로 이어집니다. 한 번 재생하면 멈추고, 페이지를 새로 진입하면 처음부터 다시 시작합니다.
// Timing constants
const REVEAL_SPEED_MS = 55; // 글자 하나당 표시 속도
const DISPLAY_MS = 6000; // 완성 후 표시 유지 시간
const FADE_MS = 1500; // 페이드아웃 시간
const BETWEEN_MS = 1000; // 다음 문장까지 간격
긴 문장은 \n으로 줄바꿈을 제어합니다. captionText에 white-space: pre-wrap이 적용되어 있어 그대로 렌더링됩니다.
마치며
우주는 침묵합니다. 소리를 전달할 매질이 없으니까요. 그런데 브라우저에서 우주를 표현할 때 오히려 소리를 넣었습니다. 드론 BGM과 별 깜빡임, 유성의 흔적.
그것이 실제가 아님을 알면서도, 우주를 보며 음악을 들을 때의 감각은 뭔가 진짜처럼 느껴집니다. 감성은 정확성보다 밀도에서 나오는 것 같습니다.
아무것도 하지 않아도 되는 공간을 만드는 것 — 그것이 이 페이지의 전부였습니다.