3분에 한 바퀴 — 배경 밤하늘을 돌리며 배운 것들
Task 목록 뒤에는 진짜 별로 그린 밤하늘이 깔려 있습니다. 이 하늘을 북극성을 축으로 천천히 돌렸습니다. 아무도 눈치채지 못할 장식 하나에도 물리와 상태와 예산이 있었습니다.
3분에 한 바퀴 — 배경 밤하늘을 돌리며 배운 것들
Fecit의 Task 목록 화면에는 밤하늘이 깔려 있습니다. 할 일이 적어 화면이 비어 있어도 허전하지 않도록 배경에 별을 뿌렸는데, 이 별들은 장식용 랜덤 점이 아닙니다. 예일 밝은 별 목록(Yale Bright Star Catalogue)의 실측 항성들을, 천구의 극을 중심으로 한 정거방위 투영으로 그린 진짜 하늘입니다.
화면 정중앙이 천구의 극이고, 그 자리에 fecit 로고가 있습니다. 북두칠성의 포인터 두 별을 따라가면 정말로 로고에 닿습니다. 북극성 자리에 fecit이 있는 셈입니다. 남반구 사용자에게는 남십자성이 뜬 남천 하늘을 보여주고요.
여기까지 만들고 나니 욕심이 하나 생겼습니다.
진짜 하늘은 돕니다
밤하늘을 오래 보면 별이 움직입니다. 지구가 도니까, 하늘 전체가 북극성을 축으로 하루에 한 바퀴 도는 것처럼 보입니다. 일주운동입니다.
배경이 실측 성표인데 별이 박제되어 있는 건 어딘가 아쉬웠습니다. 그래서 돌리기로 했습니다. 실제 방향 그대로 — 북천은 반시계, 남천은 시계 방향으로.
속도는 한 바퀴에 3분으로 정했습니다. 초당 2도, 실제 하늘의 480배속 타임랩스입니다. 응시하면 도는 게 보이지만, 할 일을 보다가 문득 눈에 들어오는 정도로는 “어, 아까랑 다르네” 수준입니다. 배경은 배경이어야 하니까요.
장식이 배터리를 먹으면 안 된다
첫 걱정은 부하였습니다. 별이 수백 개인데, 매 프레임 좌표를 다시 계산해서 다시 그리면 장식 하나가 JS 스레드를 계속 깨우게 됩니다.
답은 간단했습니다. 별을 다시 그리지 않는 것. 별들은 SVG 캔버스에 한 번만 그려두고, 캔버스를 통째로 회전시킵니다. useNativeDriver를 켠 rotate transform 하나면 애니메이션은 네이티브 스레드에서 GPU가 처리하고, JS는 아무 일도 하지 않습니다.
Animated.loop(
Animated.timing(rotation, {
toValue: 1,
duration: ROTATION_PERIOD_MS, // 3분
easing: Easing.linear,
useNativeDriver: true,
}),
)
그런데 사각형 캔버스를 돌리면 모서리가 빕니다. 45도쯤 돌아갔을 때 화면 귀퉁이에 별이 없는 삼각형이 드러나는 거죠. 그래서 회전 모드에서는 캔버스를 화면 대각선 길이의 정사각형으로 잡습니다. 어떤 각도로 돌아도 화면은 항상 하늘 안쪽입니다.
하늘이 순간이동하는 버그
만들고 나서 탭을 오가며 써 보다가 이상한 점을 발견했습니다. 다른 탭에 다녀올 때마다 하늘이 원래 각도로 돌아가 있었습니다.
원인은 단순했습니다. 화면 포커스가 빠지면 배터리를 아끼려고 회전을 멈추는데, 멈출 때 각도를 0으로 리셋하고 있었던 겁니다. 하늘 입장에서는 순간이동입니다. 3분짜리 느린 움직임을 만들어 놓고, 탭 한 번에 그 시간을 다 무르는 셈이었습니다.
고치는 방법은 일시정지/재개입니다. 멈출 때 진행도를 저장하고, 다시 켜질 때 그 각도부터 이어서 돕니다. 한 가지 함정이 있었는데, React Native의 Animated는 애니메이션을 멈춰도 “지금 값이 얼마인지”를 알려주지 않습니다. 그래서 시작 시각을 따로 기억해 두고, 멈추는 순간 경과 시간으로 진행도를 계산합니다.
return () => {
animation.stop();
// 경과 시간으로 진행도 저장 — 멈춘 각도에 그대로 얼어 있는다
const elapsed = (Date.now() - startedAt) / ROTATION_PERIOD_MS;
progressRef.current = (progressRef.current + elapsed) % 1;
rotation.setValue(progressRef.current);
};
이제 탭을 오가도 하늘은 멈춘 자리에서 기다립니다.
안 보이면 돌지 않는다
마지막 디테일. 할 일이 많은 날에는 카드가 화면을 가득 덮어서 배경이 아예 보이지 않습니다. 안 보이는 하늘을 돌리는 건 배터리 낭비죠.
그래서 목록의 콘텐츠 높이가 화면을 가득 채우면 회전을 멈춥니다. 스크롤하다 빈 공간이 드러나는 날, 그러니까 할 일이 얼마 없는 여유로운 날에만 하늘이 돕니다. 생각해 보면 어울리는 규칙입니다 — 바쁠 때는 하늘 볼 시간도 없으니까요.
배운 것
배경 장식 하나를 돌리는 작업이었는데, 끝나고 보니 세 가지가 남았습니다.
물리: 실측 데이터로 시작했으면 움직임도 실측을 따라야 합니다. 방향이 틀린 회전은 아는 사람 눈에 바로 걸립니다.
상태: 애니메이션에도 상태가 있습니다. 멈춤과 리셋은 다릅니다. 느린 애니메이션일수록 그 차이가 크게 보입니다.
예산: 장식은 공짜가 아닙니다. 네이티브 드라이버로 JS 비용을 0으로 만들고, 안 보일 때는 그마저도 끕니다.
아무도 눈치채지 못할 3분짜리 회전에 이만큼의 규칙이 필요했습니다. 그리고 그게 눈치채지 못하게 만드는 비용이었다고 생각합니다.