행성 포지 — 나만의 행성을 만드는 시뮬레이터
크기·색상·링·위성을 직접 조합해 행성을 만들고 저장하는 커스터마이저 — Canvas 렌더링과 localStorage 영속화
시작 — 내 행성을 만들고 싶다
태양계 행성들은 저마다 개성이 있습니다. 토성의 거대한 고리, 목성의 소용돌이 대적반, 해왕성의 짙은 파란빛. 그 개성들을 조합해 완전히 새로운 행성을 만드는 도구가 있다면 어떨까 생각했습니다.
행성 포지는 슬라이더와 색상 피커만으로 나만의 행성을 디자인하고, 그 설정을 저장해두는 커스터마이저입니다.
행성 파라미터 설계
행성을 구성하는 파라미터는 크게 세 그룹입니다.
interface PlanetSettings {
// 기본
size: number; // 6 ~ 100 (반지름 px 기준)
color: string; // 표면 기본색
highlight: string; // 하이라이트 포인트 색상
glowColor: string; // 외곽 발광 색상
// 링
hasRing: boolean;
ringColor: string;
ringOpacity: number; // 0 ~ 1
ringRotateX: number; // -80 ~ -20 (도)
ringRotateZ: number; // -30 ~ 30 (도)
// 위성
moons: MoonSettings[];
}
링의 기울기는 rotateX와 rotateZ 두 축으로 제어합니다. 토성처럼 거의 수평으로 놓인 링, 천왕성처럼 세로로 선 링 모두 표현 가능합니다.
Canvas 구체 렌더링 — radial gradient
행성의 구체감은 createRadialGradient로 만듭니다. 핵심은 하이라이트 포인트를 중심에서 오프셋하는 것입니다.
const grad = ctx.createRadialGradient(
cx - r * 0.3,
cy - r * 0.3,
0, // 하이라이트 중심 (좌상단 오프셋)
cx,
cy,
r, // 행성 중심, 반지름
);
grad.addColorStop(0, highlight); // 밝은 하이라이트 포인트
grad.addColorStop(0.5, color); // 기본 표면색
grad.addColorStop(1, darken(color, 0.5)); // 경계 어두운 음영
외곽 glow는 별도 radialGradient로 반지름 1.8배까지 뻗어나가는 레이어를 먼저 그린 뒤, 그 위에 구체를 덮습니다.
링 시스템 — CSS perspective 타원
링은 CSS transform으로 표현합니다. rotateX로 타원형 원근감을 주고, rotateZ로 기울임을 적용합니다.
// 링 div 스타일
const ringStyle: CSSProperties = {
width: `${size * RING_W_RATIO}px`,
height: `${size * RING_H_RATIO}px`,
borderRadius: '50%',
border: `${ringThickness}px solid ${ringColor}`,
opacity: ringOpacity,
transform: `rotateX(${ringRotateX}deg) rotateZ(${ringRotateZ}deg)`,
};
링이 행성 앞뒤를 가로지르는 효과는 z-index 레이어를 행성 앞/뒤 두 개로 분리하여 구현했습니다. 앞쪽 반원은 행성 위에, 뒤쪽 반원은 행성 아래 렌더링됩니다.
위성 궤도 애니메이션
각 위성은 공전 반지름, 속도, 크기, 색상을 개별 설정합니다. Canvas에서 매 프레임 극좌표로 계산합니다.
moons.forEach((moon, i) => {
const angle = moonAngles[i] + elapsed * moon.speed;
const mx = cx + Math.cos(angle) * moon.orbitR;
const my = cy + Math.sin(angle) * moon.orbitR * ORBIT_Y_RATIO;
// 뒤쪽 위성은 행성 렌더 전, 앞쪽 위성은 행성 렌더 후 그리기
drawMoon(ctx, mx, my, moon.size, moon.color, angle);
});
ORBIT_Y_RATIO를 0.4로 설정해 궤도를 타원형으로 보이게 합니다. 위성이 행성 뒤로 사라졌다 나타나는 효과는 angle % (Math.PI * 2) > Math.PI 조건으로 앞/뒤를 판단해 그리기 순서를 제어합니다.
localStorage 영속화
설정은 통합 스토리지(storageSet)로 저장합니다. 새로고침해도 마지막으로 만든 행성이 그대로 살아있습니다.
// 저장: 슬라이더 변경 시마다 debounce 300ms 후 저장
const saveSettings = useMemo(() => debounce((s: PlanetSettings) => storageSet('planet-forge', s), 300), []);
// 복원: 초기 상태 직접 주입
const [settings, setSettings] = useState<PlanetSettings>(() => storageGet('planet-forge', DEFAULT_PLANET_SETTINGS));
리셋 버튼을 누르면 storageRemove('planet-forge')로 저장된 설정을 지우고 기본값으로 돌아갑니다.
마치며
슬라이더 하나를 움직일 때 행성이 즉시 반응하는 것 — 그 즉각성이 이 도구의 핵심이라고 생각합니다. 저장 버튼을 따로 누르지 않아도 자동으로 영속화되기 때문에 사용자는 설정 관리가 아니라 행성 디자인 자체에만 집중할 수 있습니다.
한 번 만든 행성이 다음번에 들어와도 그 자리에 있다는 것 — 작지만 그 연속성이 꽤 중요하다고 느꼈습니다.