← DEVLOG
인터랙티브2025.09.045 min read

Cosmos — 우주 감성을 코드로 담기

우주의 고요함을 브라우저에서 경험하게 하는 힐링 페이지 — CSS 별 필드, Canvas 성운, 그리고 Web Audio 합성 BGM

canvasambientcss-animationbgmhealing

시작 — 아무것도 하지 않아도 되는 공간

대부분의 인터랙티브 콘텐츠는 뭔가를 하도록 요구합니다. 클릭하거나, 점수를 올리거나, 레벨을 클리어하거나.

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으로 줄바꿈을 제어합니다. captionTextwhite-space: pre-wrap이 적용되어 있어 그대로 렌더링됩니다.


마치며

우주는 침묵합니다. 소리를 전달할 매질이 없으니까요. 그런데 브라우저에서 우주를 표현할 때 오히려 소리를 넣었습니다. 드론 BGM과 별 깜빡임, 유성의 흔적.

그것이 실제가 아님을 알면서도, 우주를 보며 음악을 들을 때의 감각은 뭔가 진짜처럼 느껴집니다. 감성은 정확성보다 밀도에서 나오는 것 같습니다.

아무것도 하지 않아도 되는 공간을 만드는 것 — 그것이 이 페이지의 전부였습니다.

이 포스트와 연결된 콘텐츠

직접 체험하기