이곳은 내 블로그, 宙と木다. 페이지를 넘길 때마다 호박색 점 하나가 이 페이지에서 다음 페이지로 날아가, 다음 페이지가 가진 그 선 위에 내려앉는다. 10월 25일 새벽, 이 사이트는 태어난 지 572일째 되는 날 지금의 모습으로 갈아입었다.

이 사이트는 처음에 내가 맨땅에서 혼자 조립한 포트폴리오일 뿐이었고, 그 뒤로 페이지가 하나 늘 때마다 그때의 느낌대로 한 페이지씩 덧붙였다. 1년쯤 지나자 사이트에서 AI 냄새가 나기 시작한다고 느꼈다. 여러 군데가 아무 템플릿에서나 나올 법한 모양이었다. 그래서 올해 5~6월에 innei의 블로그에서 몇 가지 방식을 참고했고, 나중에는 WebGPU로 그린 별하늘 배경으로 바꿨다(그 글은 여기). 글, 혼잣말, 본 애니메이션, 찍은 사진이 전부 여기에 있으니, 나는 늘 이곳이 어떤 테마를 조금 고친 것이 아니라 "내 것"으로 보이길 바랐다.

솔직히 반년 전만 해도 나는 이 블로그에 꽤 만족했다. 다만 늘 한 페이지씩 따로 고쳤을 뿐, 사이트 전체를 한데 놓고 보면 어떤 모습일지는 생각해 본 적이 없었다. 4월에 "성장 궤적" 페이지를 고칠 때 카드 배경판을 걷어 내고 배경에 떠 있는 거대한 연도로 바꿨는데, 보기에는 예뻐졌지만 뭔가 빠진 것 같았다. 나중에야 알았다. 빠진 것은 몰입감이었다. 카드 하나하나가 따로 노는 슬라이드 같았고, 그 사이를 이어 주는 것이 아무것도 없었다.

나는 평소에 멍하니 하늘을 올려다보는 일이 많은 사람이고, 내 안이 이 사이트가 담을 수 있는 것보다 훨씬 넓다고 늘 생각했지만 그걸 어떻게 표현해야 할지 몰랐다. 그러다 10월 어느 날 음악, 책장, 장비 세 페이지를 나란히 놓고 보니 셋이 똑같이 생겼다는 걸 알았다. 큰 제목, 부제목, 그 아래로 늘어선 카드 한 줄. 그 순간 이 페이지들은 그저 보여 주려고 거기 놓여 있을 뿐이라는 생각이 들었다. 모든 페이지가 이렇게 생겼다면 독자는 무엇을 탐험할까? 나는 한 번에 다 뒤집기로 했다.

「그리고 지금부터, 지구를 움직이겠다.」

《지。 -지구의 운동에 대하여-》

내가 아주 좋아하는 작품의 대사다. 이야기 속 사람들은 몇 세대에 걸쳐 목숨을 걸고 지동설을 이어 전한다. 내가 하는 일은 물론 그보다 훨씬 작지만, 사이트 전체를 뒤집기로 마음먹은 순간 머릿속에 떠오른 건 이 문장이었다.

결국 나흘이 걸렸다. 10월 22일에 방향을 고민하기 시작해 25일 새벽에 공개했고, 병합한 PR은 파일 449개를 건드려 41,744줄을 더하고 53,184줄을 지웠다. 열네 개 페이지를 전부 다시 그렸고, 각 페이지는 천체 하나, 혹은 우주에서 실제로 쓰이는 무언가로 바뀌었다. 이 글은 그 나흘, 그리고 그 나흘 전의 572일에 관한 이야기다. 전환 효과와 폰트의 기술적인 내용은 글 끝의 부록에 모아 두었으니, 직접 만들어 보고 싶은 사람은 바로 건너뛰어도 된다.

첫 번째 버전은 실패했다#

처음에는 디자인 시안을 먼저 그리지 않았다. 로컬에서 바로 고쳐 보는 편이 효과를 더 잘 알 수 있을 것 같았고, 시안과 실제 배경을 깔았을 때의 느낌은 많이 다르니까. 첫 번째 버전은 사실적인 재질의 토성을 그대로 두고, 페이지를 넘길 때 행성이 폭발해 별 먼지로 부서지게 했다. 노트의 카드는 페이지 아래까지 길게 자라 지평선에 닿았고, 혼잣말 페이지는 픽셀 스타일로 만들었다.

만들어 놓고 보니 스스로도 영 마음에 들지 않았다. 토성은 움직일 때 과노출됐고, 폭발하며 흩어지는 입자는 배경에 원래 있던 별들과 이어지지 않아서 타노스의 핑거 스냅 같기도, 지우개로 지운 것 같기도 했다. 노트의 별 궤적은 처음에 너무 빽빽해서 보고 있으면 어지러웠고, 내용이 거의 가려졌다. 레이아웃은 어떻게 고쳐도 지난 시대 블로그의 냄새가 났다. 옛날 야후 같았다.

오후 내내 고쳤지만, 한 군데를 고칠 때마다 다른 곳이 더 어울리지 않아 보였다. 문제는 내가 기조를 먼저 정하지 않았다는 데 있었다. 페이지마다 따로 효과를 얹다 보니 재질도, 색도, 움직임도 제각각이었다. 솔직히 그 버전이 못생긴 건 아니었고, 그저 내가 원하던 게 아니었을 뿐이다. 그래서 지우지 않고 GitHub의 archive/sky-morph 브랜치에 봉인해 두고, 방향부터 다시 생각하기로 했다.

「틀렸다고 해서 의미가 없는 것은 아니다.」

《지。 -지구의 운동에 대하여-》

이 문장은 《지。》에서 몇 번이고 되풀이된다. 첫 번째 버전은 틀렸다. 하지만 먼저 틀려 보지 않았다면 내가 정말 원하는 게 무엇인지도 몰랐을 것이다.

점과 선, 그리고 약간의 호박색만 남기다#

앱을 만들 때도 나는 원래 세리프체 특유의 질감을 좋아했고, 사이트 이름 「宙と木」의 「宙」도 원래 우주라는 뜻이다(「木」는 내 닉네임 기무라 분재에서 왔다). 그래서 방향은 사실 금방 좁혀졌다. 별하늘은 원래 점과 선만으로 그릴 수 있다. 별은 점이고, 궤도와 별의 궤적과 혜성의 꼬리는 선이다. 선으로 페이지를 이으면, 내가 빠져 있다고 느낀 그 몰입감을 딱 채울 수 있었다.

가장 내려놓기 어려웠던 건 홈의 토성이었다. 재질까지 모델링한 그 토성을 나는 올해 4월까지 계속 다듬었다. 성능, 화질, 전체적인 인상을 몇 차례나 갈고닦았기에 통째로 버리기가 정말 아까웠다. 결국 토성은 남기되 점과 선으로 그리는 방식으로 바꿨다. 불투명한 구 위에 하얀 붓질 자국을 한 줄 한 줄 긋고, 고리도 선으로 그렸다. 바꾸고 나니 훨씬 좋아져서, 꼭 사실적인 재질을 고집할 필요는 없었다는 걸 깨달았다. 그래도 아까운 건 아까운 거라, 예전 토성은 예전 사이트에 기념으로 남겨 두었다.

정한 규칙은 몇 개 되지 않는다.

항목결정
색검정(#0A0A0C)은 종이, 따뜻한 흰색(#F1ECE3)은 먹, 호박색(#E9C17A)은 오직 "점 하나"의 신호로만
재질카드, 그림자, 반투명 유리 효과는 쓰지 않고, 구분선도 되도록 쓰지 않는다
중국어 글꼴본명조(Noto Serif TC)
영문 글꼴문단 전체가 영어인 큰 글자에만 쓰고, Instrument Serif로, 굵게는 절대 하지 않는다

나는 색 배합에 대해 잘 모르지만, 한 가지는 확실히 알았다. 색이 많아지면 화면에 주인공이 없어진다. 그래서 사이트 전체에서 호박색은 그 점 하나뿐이다. 이번에는 나도 교훈을 얻어서, 방향을 먼저 분명히 정하고 나서 코드를 썼다. 페이지마다 먼저 브라우저에서 바로 클릭해 볼 수 있는 시안을 만들어 한 판씩 고쳤고, 앞뒤로 15판을 고쳐 모든 페이지가 확정된 뒤에야 사이트를 건드리기 시작했다.

모든 페이지는 하나의 천체다#

방향을 정한 뒤로는 페이지마다 같은 질문을 던졌다. 이것은 우주에서 무엇일까? 페이지마다 붙은 부제목은 모두 내가 직접 썼다. 사이트 전체가 조금은 문학적으로 읽히길 바랐지만, 군더더기는 없기를 바랐다.

페이지부제목
홈코드를 쓰는 사람. 가끔 뭔가를 만든다.
노트별들이 흐르는 동안, 순간의 호기심마다 한 줄기 호를 남긴다.
혼잣말정적을 가르는 잡음 하나를 붙잡는다.
작품소리 없는 심우주에, 한 치씩 흔적을 새긴다.
컬렉션중력이 닿는 곳마다, 나를 비춰 주었던 빛의 조각들.
사진허공을 건너온 희미한 신호가, 이곳에서 한 줄씩 현상된다.
장비정밀함을 해체하고, 집착을 내려놓을 자리를 만든다.
친구들심우주는 길고 쓸쓸하지만, 돌아보면 언제나 별빛이 있다.
메시지소리 없는 긴 밤, 먼 곳에서 올 펄스를 고요히 기다린다.
항해 일지중력을 벗어나면, 이 몸이 곧 드넓은 하늘이 된다.
소개꽉 잡으세요.
이 사이트여기엔 다른 것이 없다. 밤과 흰 먹, 그리고 희미한 빛뿐.

홈: 아무것도 두지 않는다#

홈에는 그 토성이 있고, 노트, 혼잣말, 작품, 컬렉션, 소개 다섯 구역은 그 주위를 도는 다섯 개의 위성이다. 나는 일부러 첫 화면에 아무 정보도 두지 않았다. 평소에는 선과 점뿐이고, 마우스가 어느 위성에 가까이 가야 그 궤도가 밝아지며 그 구역의 이름과 가장 최근 항목이 떠오른다. 가만히 있을 때는 가끔 위성 하나가 혼자 반짝여서, 누를 수 있다는 걸 알려 준다. 들어온 사람이 스스로 탐험하길 바랐다. 내가 이 우주 속에서 나 자신을 탐험하듯이.

토성을 3D로 남기겠다고 고집한 건, 언젠가 홈을 그 안을 자유롭게 떠다닐 수 있는 입체 공간으로 만들고 싶어서다.

혼잣말: 스쳐 가는 혜성#

나에게 혼잣말은 머릿속을 스쳐 가는 순간들이다. Threads처럼 손 가는 대로 올리는 것이라 혜성으로 만들었다. 혜성은 커다란 S자 호를 따라 날아간다. 가장 최근 항목이 혜성의 핵이고, 오래된 것들은 꼬리를 따라 좌우로 늘어서며, 아주 오래된 것은 꼬리 위의 점 하나로만 남아 있다가 마우스가 지나가야 떠오른다.

혼잣말 하나를 누르면 카메라가 혜성 꼬리의 그 구간으로 확대해 들어간다. 망원경으로 당겨 보듯이 그 구간이 밝아지고 나머지는 어두워지며, 아래의 댓글은 그 꼬리에서 흩어져 나온 부스러기다. 내가 원한 건 다른 페이지로 넘어가는 게 아니라 "혜성의 어느 한 구간을 보고 있다"는 느낌이었다.

노트: 장노출 별의 궤적#

노트는 내가 가장 좋아하는 페이지다. 글 한 편이 별의 궤적 한 줄이다. 최근 글일수록 바깥쪽을 돌고, 길게 쓴 글일수록 호가 길게 늘어진다. 배경의 흐릿한 호들도 별의 궤적이라, 밤하늘 전체가 장노출 사진처럼 보인다. 페이지를 열면 셔터가 막 열린 것처럼 모든 별이 한꺼번에 궤적을 그리고, 그 뒤로는 하늘 전체가 천구의 극을 중심으로 천천히 돈다. 별의 궤적은 사실 지구의 자전이 남긴 흔적이다. 하늘의 별이 도는 게 아니라, 우리가 서 있는 이 별이 돌고 있는 것이다.

가장 마음에 드는 건 글을 누르는 그 순간이다. 누른 글의 별 궤적이 곧게 펴지고 가까이 당겨져, 글 페이지 오른쪽의 궤도가 된다.

이 궤도는 목차이기도 하다. 각 장은 궤도 위의 눈금이고, 눈금 사이의 거리는 장의 실제 길이에 따라 정해진다. 읽은 부분은 가는 실선, 아직 읽지 않은 부분은 점선이며, 호박색 점이 지금 읽고 있는 당신이다. 노트 목록은 한 바퀴 한 바퀴 별의 궤적이고, 글 하나를 누르는 건 카메라가 그중 한 줄로 다가가는 것이다.

장비: 아직 마음에 들지 않는 도면#

장비 페이지는 끝까지 고쳤는데도 여전히 썩 마음에 들지 않는 페이지다. 첫 번째 버전은 무중력 조립에 우주선 서브시스템 라벨과 실시간 원격 측정을 더한 것이었는데, 만들어 보니 밋밋했고 사실 우주와도 별 상관이 없었다. 마음속으로는 늘 공학 개념 설계도로 만들고 싶었지만, 한 획씩 직접 그리기에는 시간이 너무 많이 들었다. 결국 Gemini로 흰 바탕에 검은 선의 분해 등각 투상도를 만들어 밑그림으로 쓰고, 그 위에 코드로 번호 원, 지시선, 치수선, 부품표를 얹었다. 번호 위로 마우스를 올리면 밑그림 속 그 부품이 호박색으로 물든다.

첫 번째 도면 SHEET 0은 본가에 있는, 아마 2012년쯤 산 컴퓨터다. 사진이 없어서 케이스는 기억에 의지해 점으로 그린 스케치이고, 점들이 살짝 흔들리며, 사양은 부팅 자가 진단처럼 한 줄씩 찍혀 나온다. 그 컴퓨터는 내 어린 시절이라 맨 앞에 두었다.

이 버전도 여전히 마음에 들지 않아서, 아마 앞으로 또 고칠 것 같다.

그 밖의 페이지#

나머지 페이지들도 저마다 천체 하나씩을 가지고 있다.

  • 작품은 중심에서 바깥으로 뻗어 나가는 나선 항로다. 출발점에는 졸업 프로젝트 VoltiCar를 두었다. 탄소 배출권을 게임으로 만든 프로젝트다. 주제 선정도 썩 좋지 않았고 팀 구성도 순탄하지 않았으며, 지금 돌아보면 기술적으로는 유치한 수준이다. 그래도 그때 할 줄 알던 것을 전부 쏟아부은 첫 프로젝트였고, 1년 넘게 매달리면서 먹고 자는 것도 잊을 때가 많았다. 그래서 그것이 항로의 출발점이다. 작품 하나하나는 선으로 그린 작은 행성이고, 크기, 지형, 고리와 위성은 모두 GitHub의 데이터로부터 자라난다.
  • 사진은 우주 사진이 지구로 전송되는 과정을 따라 그렸다. 탐사선이 찍은 사진을 0과 1로 바꾸면 심우주 통신망의 커다란 접시 안테나가 받아 내고, 한 줄씩 흑백으로 현상한 뒤 마지막에야 색을 입힌다.
  • 친구들의 중심은 이 사이트이고, 친구들은 그 주위를 도는 별이다. 별 하나를 누르면 그와 도킹한다. 두 우주선이 맞물리고, 옆에 그가 최근에 쓴 글이 펼쳐진다.
  • 메시지는 한 통 한 통이 수신된 신호다. 글자를 입력하면 쓴 글자가 실시간으로 파형이 되고, 보내면 안테나로 날아간다.
  • 항해 일지는 페이지 전체가 발사 장면이다. 지면에서의 높이는 누적된 GitHub 기여 수이고, 이 페이지는 글 끝에서 한 번 더 나온다.
  • 이 사이트는 별하늘을 향한 육분의 하나다. 그 안의 장난감은 모두 사이트에 실제로 있는 것들이고, 하나씩 뜯어 보며 처음에 어떻게 생각했는지 적어 두었다. 누군가 다 가지고 놀고 나서 "이렇게도 할 수 있구나" 하고 느끼면 좋겠다.

소개: 여기 내려앉다#

소개 페이지는 자유 낙하다. 점이 바로 위에서 천천히 떨어져 「꽉 잡으세요.」의 마침표가 되어 멈춘다. 독자가 아래로 스크롤하는 건 그 점과 함께 아래로 떨어지는 것이다.

영감은 예전에 뉴스에서 본 영상 하나였다. 포르투갈의 깊은 밤, 푸른 불덩이 하나가 하늘을 가로지르며 밤하늘 전체를 몇 초 동안 환하게 밝혔다. 천문 현상은 늘 나를 압도했다. 별 하나가 지구로 떨어지는 건 인생과 같다. 가장 찬란한 빛을 내뿜고 나서 순식간에 사라진다. 그래서 이 페이지는 서툴고 헤매던 어제들을 스쳐 지나 「여기 내려앉다」에서 멈춘다. 하지만 내려앉은 뒤에야 새로운 시작이다. 더 내려가면 땅속이고, 한 층씩 파 내려가면 내가 해 온 일들이 나오며, 가장 깊은 곳에는 내가 가장 좋아하는 문장이 놓여 있다.

「아마 감동은 수명의 길이보다 소중한 것이라고 생각해.」

《지。 -지구의 운동에 대하여-》

컬렉션: 네 번 고친 신호#

컬렉션 페이지(보는 중, 책장, 듣는 중)는 가장 많이 고친 페이지다. 여기서 보여 주고 싶었던 게 취향이었기 때문이다. 내가 많은 것을 보았다는 것, 그리고 무엇을 좋아하는지가 보이길 바랐다. 첫 번째 버전은 세 구역을 나란히 놓아서 너무 반복적이었다. 두 번째 버전은 탭 전환에 별 먼지 재조합을 더했는데 너무 작위적이었다. 세 번째 버전은 세 개의 성운이었고, 스크롤하면 카메라가 세 성운 사이를 날아다녔지만 여전히 우주의 광활함이 느껴지지 않았고, 원 안에 담긴 표지도 대부분 잘려 나갔다.

세 버전이 모두 틀리고 나서, 나는 "표지를 어떻게 배치할까"를 그만 생각하고 대신 이렇게 물었다. 우주에서 이런 것들은 실제로 어떻게 쓰일까? 답은 신호였다. 작품 하나하나는 내가 그것을 다 본 순간 지구에서 내보낸 한 겹의 신호이고, 지구에서 얼마나 멀리 있는지가 지금으로부터 얼마나 오래전인지를 뜻한다. 아래로 스크롤하면 지구에서 바깥으로 계속 물러난다. 실제 위치에 놓인 달, 웹 망원경, 행성들, 보이저 1호, 태양권계면, 오르트 구름을 지나, 마지막에는 약 9광년 밖의 가까운 항성들까지 물러난다.

바깥으로 물러나는 길에 보이저 1호를 지난다. 1990년, 보이저 1호는 약 60억 킬로미터 밖에서 뒤돌아 지구를 한 장 찍었고, 사진 속 지구는 한 픽셀도 되지 않았다. 칼 세이건은 그 사진을 보며 이렇게 썼다.

「저 점을 다시 보라. 저것이 여기다. 저것이 우리의 집이다. 저것이 우리다.」

칼 세이건, 『창백한 푸른 점』

컬렉션 페이지에 이런 느낌을 담고 싶었다. 바깥으로 멀리 물러날수록 지구는 작아지고, 나를 비춰 주었던 작품들은 모두 그 작은 점에서 내보낸 신호라는 것.

점 하나가 모든 페이지를 잇는다#

리뉴얼에서 딱 하나만 남길 수 있다면 나는 전환 효과를 고르겠다. 전환의 매끄러움과 이어지는 정도가 한 사이트의 완성도를 결정한다고 늘 생각해 왔다. 독자는 무엇이 좋은지 꼭 말하지 못해도, 한 번 끊기거나 깜빡이거나 위치가 튀면 반드시 느낀다.

"매끄러움"에 대한 집착은 이번에 시작된 게 아니다. 7월에 글 페이지의 로딩을 고칠 때도 스켈레톤 화면과 본문 사이의 연결에서 막혔고, 10월에 별하늘 성능을 손볼 때 목표는 들어온 사람이 "3D인데 어떻게 이렇게 매끄럽지" 하고 느끼게 하는 것이었다. 그래서 리뉴얼 첫날의 데모는 특히 받아들이기 힘들었다. 페이지 전환의 비행은 건조하고 단조로웠고, 들어올 때와 나갈 때 방향이 같았다. 다음 날 고친 버전도 페이드 인과 페이드 아웃으로 두 페이지를 억지로 이어 붙였을 뿐이었다.

그래서 나 스스로 규칙을 하나 정했다. 페이지를 넘길 때마다 점 하나에서 뻗어 나가야 한다. 홈에서는 궤도 위의 위성, 혼잣말에서는 혜성의 핵, 노트에서는 별 궤적 하나하나에 별이 하나씩 있다. 페이지마다 자기만의 점이 있기만 하면, 페이지 전환은 점 하나가 이 페이지에서 다음 페이지로 날아가는 일이 될 수 있다.

첫 번째 버전은 억지로 그린 것 같았다#

첫 번째 버전은 아주 단순했다. 점이 정해진 경로를 따라 날아가고, 도착하면 새 페이지가 페이드 인된다. 실제로 보니 점과 페이지 위의 선이 맞지 않았다. 호박색 점은 혜성의 핵에 정렬되지 않았고, 전환이 그린 선도 혜성 꼬리와 조금씩 어긋났다.

몇 차례 고치고 나니 "작품 페이지로 들어가는" 구간이 마침 가장 매끄럽게 나왔다. 정말 단숨에 이어졌다.

나는 그것을 기준으로 삼아 나머지 페이지를 모두 그에 맞춰 고쳤다. 매끄러운 이유를 뜯어 보면 세 가지였다.

이전 페이지가 스스로 그 점으로 접혀 들어간다

홈의 토성은 누른 위성 속으로 접혀 들어가고, 혼잣말의 혜성 꼬리는 핵으로 줄어든다. 페이지 위의 것들이 모두 그 점으로 모여들고, 다 모인 뒤에야 페이지가 넘어간다.

카메라는 영화의 추적 촬영처럼

내가 가장 신경 쓴 부분이다. 카메라가 먼저 호박색 점으로 이동해 점을 정중앙에 놓는다. 점이 출발하면 카메라가 따라가서, 점은 계속 화면 한가운데에 있다. 거의 도착할 즈음 카메라가 멈추는데, 이때 점은 아직 움직이고 있어서 그 뒤로 남은 궤적이 화면에 잡힌다. 영화가 먼저 한 대상을 확대해 따라가다가, 카메라가 멈췄을 때 비로소 그 대상이 지나온 길이 보이는 것처럼.

마지막 구간은 페이지 자신의 선을 따라간다

점은 마지막에 다음 페이지에 원래 있던 선(별의 궤적, 혜성 꼬리, 항로)을 따라 내려앉는다. 점 뒤의 꼬리는 시간이 지나면 흩어지기 때문에, 내려앉았을 때 화면에 남는 건 그 페이지 자신의 선뿐이다.

페이지마다 점을 받아 내는 방식#

이 세 가지를 정하고 나니, 페이지마다 할 일은 두 질문에 답하는 것이었다. 점은 마지막에 어느 선을 따라 내려앉는가, 그리고 떠날 때 이 페이지는 어떻게 점 하나로 접혀 들어가는가. 내가 가장 재미있다고 느낀 부분도 여기다. 같은 점 하나가 페이지마다 다른 의미를 가진다.

페이지점이 내려앉는 방식떠날 때
홈토성의 중심으로 날아가고, 토성이 그 점에서 펼쳐진다전체가 누른 위성 속으로 접혀 들어간다
노트별 궤적 하나로 끼어들어 천구의 극을 따라 한참 돌다가, 그 글의 별 위에 멈춘다글을 누르면 그 글의 별 궤적이 펴져 글의 목차가 된다
혼잣말혜성 꼬리를 따라 내려앉아 혜성의 핵이 된다혜성 꼬리가 핵으로 줄어든다
작품출발점에 내려앉아 펜촉이 되어 항로 전체를 그린다항로가 출발점부터 지워지고, 가장 최근 작품의 위성 속으로 접혀 들어간다
컬렉션심우주에서 지구로 급강하한다카메라가 지구로 돌아오고, 점이 대만에서 한 겹의 파면을 두르고 발사된다
사진점이 곧 그 신호이고, 탐사선에서 안테나로 떨어진다페이지 전체가 0과 1로 돌아가고, 점이 탐사선에서 발사된다
메시지출발점이 한 번 진동하고, 호박색 빛 한 줄기가 채널을 따라 안테나로 달려간다빛이 안테나에서 출발점으로 거꾸로 달려간다
친구들중심에 내려앉고, 궤도가 안쪽에서 바깥쪽으로 한 줄씩 자라난다궤도가 중심으로 거둬진다
항해 일지발사대에 내려앉고, 로켓이 솟아오른다로켓이 기체를 따라 날아가고, 점은 기수를 따라간다
소개바로 위에서 천천히 마침표 속으로 떨어진다아래로 떨어지던 속도를 그대로 지닌 채 떠난다
이 사이트망원경 경통 앞쪽 끝에 내려앉고, 시선이 첫 번째 별로 당겨진다지금 측정하고 있는 별에서 날아간다

방문자가 꼭 홈으로 들어오는 건 아니라서, 첫 화면도 같은 방식이다. 처음 사이트에 들어오면 온통 밤빛이고 가운데에 원 하나가 그려진다. 페이지 로딩이 끝나면 그 원이 다 채워지고, 가운데의 점이 화면 정중앙에서 이 페이지로 날아간다.

마음을 굳힌 뒤로 이 나흘 동안 멈추고 싶었던 적이 한 번도 없었다. 지금 내가 가장 좋아하는 건 사진, 컬렉션, 노트 페이지이고, 특히 노트의 별 궤적이 글 페이지로 이어지는 그 구간이다. 아직 살짝 끊기는 데가 있고, 빠르게 연달아 페이지를 넘길 때의 손맛도 만족스럽게 다듬지 못했지만, 지금으로서는 내 마음속 "매끄러움"에 가장 가까운 모습이다.

아직 다 못 한 것들#

여기까지 쓰고 보니 모든 게 순조로웠던 것처럼 보이지만, 아직 제대로 하지 못한 부분이 몇 군데 있다는 걸 나 스스로 잘 안다.

휴대폰과 iPhone Duo#

이 디자인은 전부 넓은 화면을 기준으로 생각한 것이라, 휴대폰에서는 "볼 수는 있는" 정도다. 홈의 3D 토성은 휴대폰에서 아주 작게 줄어들고, 위성 이름은 마우스를 올려야 떠오르는데 휴대폰에는 마우스를 올린다는 개념이 없다. 작품 페이지의 나선은 휴대폰에서 세로로 바뀌면서 작품 이름들이 한데 몰린다. 올해 9월에 나온 iPhone Duo는 더 골치 아프다. 펼치면 안쪽 화면이 7.6인치에 비율이 거의 정사각형이라 휴대폰과 데스크톱 사이에 딱 걸치는데, 지금의 레이아웃은 어느 쪽으로도 어울리지 않는다.

이 부분은 아직 답을 찾지 못했다. 여러 페이지는 화면 자체가 내용이라 카드를 한 줄로 늘어놓는다고 해결되지 않는다. 아마 페이지마다 "작은 화면에서는 이 천체를 어떻게 보여 줘야 할까"를 다시 생각해야 할 것이다.

가독성#

공개한 뒤 X에서 가독성이 좀 떨어진다는 댓글을 받았다. 글자가 읽기 어렵고, 어디를 눌러야 할지도 잘 모르겠다는 것이었다. 솔직히 처음 디자인했을 때 나도 그렇게 생각했다. 내비게이션 바는 평소에 접혀 있다가 마우스를 올려야 펼쳐지고, 홈에 처음 들어오면 토성과 선 몇 줄뿐이라 "여기를 누르세요"라고 알려 주는 버튼이 하나도 없다.

이건 의도한 선택이다. 들어온 사람이 가장 먼저 느끼는 게 화면 가득한 압도감, 그리고 우주 속을 스스로 더듬어 가는 느낌이길 바랐다. 하지만 이 두 가지는 "읽기 쉽고 찾기 쉬운 것"과 균형을 맞추기가 어렵다. 글자를 굵게 하고 글 내용에 색을 조금 더 넣으면, 예전의 MDX 블록처럼 알록달록하게 하면, 읽기에는 분명 더 또렷하겠지만 사이트 전체의 분위기가 어긋나기 시작한다. 조금씩 다듬고는 있지만, 전체적으로 어디까지 맞춰야 할지 그 선을 아직 찾지 못했다.

누군가 봐 준 날#

공개한 다음 날, 관리자 화면에서 X에서 들어온 트래픽이 한꺼번에 몰린 걸 보고 따라가 보니 innei가 이 사이트를 소개하는 게시물을 따로 올리고, 내 혼잣말 아래에 댓글까지 남겨 두었다. 나는 그의 블로그를 오래전부터 좋아했다. 일본풍 종이 질감의 디자인이 아름답고, 글 속 여러 요소의 인터랙션과 조판 엔진이 아주 섬세하며, 그의 개발 정신도 존경해 왔다. 이런 작은 사이트는 아마 보지 않을 거라고 생각했기에, 그 순간은 정말 실감이 나지 않았고 기쁘면서도 몸 둘 바를 몰랐다. 그런데 기뻐하자마자 바로 생각이 들었다. 아직 어디를 더 고쳐야 할지, 어디가 부족한지. 그래서 또 조금은 남에게 보여 주기 싫어졌다.

나는 그의 방식을 참고했고, 이제 그가 내가 만든 것을 보았다. 이 느낌은 《지。》에서 말하는 계승과 조금 닮았다. 한 사람이 남긴 것을 다음 사람이 이어받아, 더 먼 곳으로 가져간다.

「내가 더 멀리 보았다면, 그것은 거인의 어깨 위에 서 있었기 때문이다.」

뉴턴, 1675년 훅에게 보낸 편지

innei의 그룹에서 내 링크 교환 신청을 보고 찾아온 사이트 운영자도 있었다. 사이트가 아주 예쁘다고 하면서, 메시지 페이지에서 글이 전송되지 않는다는 것도 알려 줬고, 그 버그는 그날 바로 고쳤다. 나는 이 사이트가 조금 깊숙이 숨겨진 보물 같다고 늘 생각했다. 디테일은 많지만 누가 꼭 봐 준다는 보장은 없었다. 이번 리뉴얼 뒤로 다른 곳에서 찾아와 이곳저곳을 눌러 보고, 어디가 읽기 어렵다고 다시 와서 알려 주는 사람들이 생기기 시작했다.

572일째#

리뉴얼을 공개한 날은 이 사이트의 572일째였다. 항해 일지 페이지가 그리는 게 바로 이 시간이다. 먼 곳으로 갈수록 시간이고, 지면에서의 높이는 누적된 GitHub 기여 수로, 지금까지 5,356번이다.

이 선을 돌아보면, 처음에는 Hero 한 페이지에 포트폴리오를 붙인 것뿐이었다. 2025년 8월에야 블로그가 생겼고, 10월에 내 도메인을 갖게 됐으며, 올해 1월에는 사이트 전체를 집의 HomeLab으로 옮겼다. 그 뒤로는 거의 매달 허물고 다시 지었다. 4월에는 다국어를 지원하고 처음으로 디자인을 대대적으로 다시 했고, 6월에는 사이트 전체를 JavaScript에서 TypeScript로 옮기고 프런트엔드를 TanStack Start로 이전했으며, 7월에는 Express 백엔드를 통째로 Rust로 바꾸고 WebGPU 우주 배경을 정식으로 들였다. 그리고 이번이다. WebGPU, Rust 백엔드, 직접 운영하는 서버, 다섯 개 언어의 번역. 많은 것을 이 사이트 덕분에 처음 만져 봤고, 대부분은 함정에 빠져 보고 나서야 배웠다. 그 과정은 대부분 노트로 남겼다.

예전 사이트는 지우지 않고 archive.koimsurai.com에 얼려 두었다. 더는 내가 원하는 모습이 아니지만, 새 사이트가 이렇게 매끄러울 수 있었던 것도, 이 나흘을 이렇게 빨리 해낼 수 있었던 것도, 상당 부분은 예전 사이트가 1년 넘게 다져 온 구조 덕분이다.

이 572일 동안 가장 힘들었던 건 사실 코드를 쓰는 일이 아니라 글감을 고민할 때였다. 이 사이트를 그만두겠다고 생각한 적은 한 번도 없지만, 자주 이런 생각이 들었다. 나는 왜 이걸 하고 있을까? 늘 나 혼자서만 하고 있는 것 같았다. 그래서 이번에 누군가 다른 곳에서 찾아와 이곳저곳을 눌러 본 일은, 나에게는 그 무엇보다 실감 나는 일이었다.

이번 리뉴얼은 이 572일 동안 배운 것을 처음으로 내 미감으로 정리해 하나의 완전한 모습으로 만든 것에 가깝다. 아직 완벽하지 않고, 모바일, 가독성, 성능 모두 고칠 게 한가득이지만, 이 선은 앞으로도 계속 위로 올라갈 것이다.

부록: 직접 만들어 보고 싶은 사람에게#

아래는 전환 효과, 폰트, 예전 사이트의 구현 세부 사항이다. 위의 이야기와는 상관없으니 관심이 있으면 읽어 보면 된다.

접고, 날고, 자란다#

전환은 접기, 날기, 자라기 세 단계로 나뉜다. 페이지 전환은 먼저 막아 두었다가 이전 페이지가 다 접혀 들어간 뒤에야 통과시킨다. 날아가는 동안 새 페이지는 이미 마운트되어, 카메라를 따라 점이 향하는 쪽에서 미끄러져 들어온다. 점이 내려앉은 뒤에야 새 페이지가 자기 것을 자라게 하기 시작한다.

링크 클릭 / 뒤로 가기history blocker가 이번 페이지 전환을 막음leave()접혀 들어간 점, 초속도, 접기 완료 Promise다 접힌 뒤에야 통과새 페이지 마운트(먼저 카메라 밖에 숨김)anchor(), arrive()로 앵커와 마지막 경로 전달초점 맞춤, 추적, 급정지, 점이 approach를 따라 착지점 착지, 페이지가 자라기 시작독자라우터전환 레이어이전 페이지새 페이지독자라우터전환 레이어이전 페이지새 페이지

전환 레이어(TransitionLayer)는 페이지 전환을 막고, 점과 꼬리를 그리고, 카메라를 움직이는 일을 맡는다. 각 페이지는 hook 하나로 네 가지만 알려 주면 된다. 앵커가 어디인지, 점이 마지막에 어느 경로를 지나는지, 어떻게 접히는지, 착지했을 때 무엇을 할지. 아래는 소개 페이지의 버전으로, 점이 바로 위에서 곧장 떨어져 「꽉 잡으세요.」의 마침표에 들어간다.

tsx
usePageTransition({
  // 점이 날아 들어와 제목의 마침표로 멈춘다
  anchor: () => stopPoint(titleStopRef.current),
  // 마지막 구간: 마침표 바로 위에서 곧장 아래로 떨어지고, 카메라도 따라 내려가며, 페이지는 아래에서 올라온다
  arrive: () => {
    const p = stopPoint(titleStopRef.current);
    if (!p) return null;
    const drop: Pt[] = [];
    for (let d = FALL_IN_PX; d >= 0; d -= 6) drop.push({ x: p.x, y: p.y - d });
    return { approach: drop, join: Math.round((FALL_IN_PX - FALL_SHOWN_PX) / 6), pace: FALL_PACE };
  },
  // 접기: 글자가 물러나고, 호박색 점은 아래로 떨어지던 속도를 지닌 채 떠난다
  leave: () => {
    const point = dotRef.current;
    if (!point) return null;
    pageRef.current?.classList.add('is-leaving');
    return { point, velocity: { x: 0, y: 0.8 }, done: after(300) };
  },
});

approach는 점이 마지막에 지나갈 경로이고, join은 카메라가 어느 지점에서 급정지하는지 표시하며, 그 앞 구간은 화면 밖에 있어도 된다. pace가 1보다 작으면 느려진다. 소개 페이지는 자유 낙하라서 다른 페이지보다 천천히 떨어지게 했다. 페이지가 leave를 주지 않으면 전환 레이어가 원형 마스크로 페이지 전체를 앵커 속으로 접어 넣기 때문에, 새 페이지를 만들 때 처음부터 전환까지 다 써 둘 필요는 없다. 노트에서 글로 들어갈 때는 그 별 궤적을 전환 레이어에 넘겨 계속 그리게 하고, 글 페이지가 자라난 뒤에야 흐려지게 해서 두 페이지 사이의 선이 끊기지 않는다. 페이지 전환을 막는 데는 TanStack Router의 router.history.block을 썼다. 뒤로 가기와 앞으로 가기(popstate)도 같은 blocker를 거치기 때문에, 마우스 측면 버튼으로 페이지를 넘겨도 똑같이 전환 효과가 나온다.

경계 상황#

비행 자체를 매끄럽게 만든 뒤로 가장 많은 시간이 든 건 온갖 경계 상황이었다. 가장 눈에 띈 건 빠르게 연달아 페이지를 넘길 때다. 점이 그리다 만 선과 떠 있는 점이 다음 페이지에 남아 버렸다.

  • 빠르게 연달아 페이지 넘기기: 전환 중에 또 페이지를 넘기면 이전 전환은 바로 마무리하고, 이번 전환은 연출하지 않는다. 페이지의 "접기"는 최대 0.8초까지 기다리고, 전환 한 번이 8초를 넘기면 문제가 생긴 것으로 보고 바로 정리한다.
  • 배경 별하늘이 페이지를 넘길 때마다 원래 자리로 돌아감: 하늘 전체가 위치를 옮기는 것처럼 보였다. 카메라의 이동량을 누적하고 0으로 되돌리지 않도록 바꿨다.
  • 마우스 측면 버튼으로 페이지를 넘기면 기본 비행 방식으로 돌아감: 원인은 페이지가 "어디로 접혀 들어갈지" 판단할 때 누른 요소를 보는데, 측면 버튼으로 넘길 때는 누른 것이 없기 때문이었다. 목적지 ctx.to로 판단하도록 바꿨다.
  • 글에서 다른 글로 넘어갈 때는 전환을 연출하지 않음: 글 페이지는 그 자리에서 바뀐다. 왼쪽 칸은 그대로 두고, 제목, 본문, 목차만 바뀐다.
  • 모든 빛 번짐과 밝은 선은 서서히 사라지게: 순간적으로 어두워지면 정전된 것처럼 보인다. 사이트 전체에서 페이지 원래의 밝기로 천천히 돌아가도록 통일했다.
  • 첫 화면: 네트워크가 느리면 최대 3.5초까지만 기다리고 날아가서, 로딩 화면에 갇히지 않게 했다. 언어만 바꾸거나 관리자 화면을 드나들 때, 또는 시스템에서 "동작 줄이기"를 설정한 독자에게는 전환이 보이지 않는다.

한 페이지에 필요한 글자만 불러오기#

번체 중국어 본명조의 가변 굵기 버전은 한 파일에 16.8MB이고, 간체 중국어, 일본어, 한국어도 각각 한 파일씩 있다. 글의 폰트 전환에는 고딕체, LXGW WenKai, MiSans 몇 가지가 따로 더 있어서, 파일 전체를 독자에게 던져 줄 수는 없다. 내 방법은 페이지별로 글자를 잘라 내는 것이다. 브라우저가 MutationObserver로 화면에 쓰인 글자를 모아 서버에 /fonts/<style>/<weight>/<key>.woff2?c=<chars>&f=<locale>를 요청하면, 서버는 subset-font(내부적으로는 HarfBuzz의 wasm)로 그 글자만 담은 woff2를 잘라 낸다. URL이 글자 집합으로 정해지니 영구 캐시가 가능하다. 지난 글 FurSpark의 본문으로 실측해 보니 전체에 쓰인 서로 다른 글자는 836자였고, 16.8MB짜리 원본에서 잘라 낸 400 굵기 woff2는 185KB였다.

서버에서 글자를 잘라 내는 핵심 코드
ts
import subsetFont from 'subset-font';
// subset-font는 실행 시점에야 require.resolve로 HarfBuzz의 wasm을 찾아서 번들러가 추적하지 못한다.
// 명시적으로 한 번 import해야 harfbuzzjs 패키지 전체(wasm 포함)가 출력에 포함된다
import { Blob as HbBlob, Face as HbFace } from 'harfbuzzjs';

// 한 번 자를 때마다 원본 파일 전체를 wasm으로 복사해야 해서 긴 글 하나에 약 0.7초: 한 번에 하나씩만 자르고 줄을 세운다
let queue: Promise<unknown> = Promise.resolve();
const serial = <T>(job: () => Promise<T>): Promise<T> => {
  const run = queue.then(job, job);
  queue = run.catch(() => undefined);
  return run;
};

async function build(file: string, weight: 400 | 600, cps: number[]) {
  const font = await sourceOf(file); // 원본 파일은 최근에 쓴 몇 개만 메모리에 둔다
  return subsetFont(font, String.fromCodePoint(...cps), {
    targetFormat: 'woff2',
    variationAxes: { wght: weight }, // 가변 굵기 원본에서 필요한 굵기를 꺼낸다
  });
}

기록해 둘 만한 함정이 두 가지 있다. 첫째는 번들링이다. subset-font는 실행 시점에야 require.resolve로 HarfBuzz의 .wasm을 찾는데, Nitro의 추적 도구는 이게 의존성이라는 걸 알아채지 못해서 배포한 뒤에야 파일을 찾을 수 없다는 걸 알았다. 해결책은 이를 외부 의존성(traceDeps)으로 설정하고, harfbuzzjs를 명시적으로 한 번 import하는 것이었다. 둘째는 빠진 글자다. 번체 중국어 페이지에 가끔 간체자가 나오는데(보통은 댓글), 이제 요청에 x=sc를 하나 더 붙일 수 있다. 그러면 서버가 먼저 번체 원본에 어떤 글자가 빠졌는지 확인하고, 빠진 글자만 간체 파일에서 잘라 채운다.

예전 사이트를 남겨 둔 방법#

공개하기 전에 예전 버전을 git tag v1-final과 Docker 이미지로 얼려 두고, 컨테이너를 하나 따로 띄워 archive.koimsurai.com에 걸었다. 백엔드는 새 사이트와 같은 것을 쓴다.

백엔드를 함께 쓰면 예전 사이트의 폼으로 여전히 데이터를 쓸 수 있다는 위험이 있어서, nginx 단계에서 읽기 전용으로 잠갔다. 로그인은 바로 빈 응답을 돌려주고, API는 읽기만 허용하고, 관리자 화면은 메인 사이트로 돌려보내며, 검색 엔진이 색인하지 못하게 했다.

예전 사이트의 nginx 설정
nginx
server {
    server_name archive.koimsurai.com;
    add_header X-Robots-Tag "noindex, nofollow" always;

    # 검색 엔진이 예전 사이트를 색인하지 않게 한다. 새 사이트와 같은 글을 두고 경쟁하지 않도록
    location = /robots.txt {
        default_type text/plain;
        return 200 "User-agent: *\nDisallow: /\n";
    }
    # 관리자 화면은 메인 사이트로 돌려보낸다
    location /admin { return 302 https://koimsurai.com/admin; }
    # 로그인은 바로 빈 응답
    location ^~ /api/auth/ { return 204; }
    # API는 읽기만 허용
    location /api/ {
        if ($request_method !~ ^(GET|HEAD)$) { return 204; }
        proxy_pass http://127.0.0.1:<backend-port>;
    }
    location / {
        proxy_pass http://127.0.0.1:<archive-port>;
    }
}

성능과 공개 이후#

성능 수정의 원칙은 화면이 바뀌면 안 된다는 것이다. 노트에서 글로 들어갈 때 원래는 한 번 멈칫했는데, 본문 전체가 같은 프레임에 마운트되기 때문이었다. 사이트 안에서 페이지를 넘겨 들어올 때는 나눠서 마운트하도록 바꿨다. 컬렉션 페이지를 새로고침할 때의 멈칫거림은 배경 별하늘의 worker가 WebGPU device를 만들고 있었기 때문이라, 등장 애니메이션이 끝나고 메인 스레드가 한가해진 뒤에 만들도록 바꿨다.

50–67ms
노트에서 글로 들어가는 그 한 프레임
기존 183–217ms
0ms
컬렉션 페이지 새로고침 시의 멈칫거림
기존 67–85ms

아직 해결하지 못한 건 컬렉션 페이지의 가장 바깥쪽이다. 신호 고리의 점선은 가장 바깥까지 스크롤했을 때 매 프레임 4만~6만 개 구간을 다시 잘라야 해서, 65fps에서 45fps 안팎으로 떨어진다. 장비 페이지의 네트워크 도식과 원격 측정에도 원칙이 하나 있다. 구조는 남기고 민감한 모델명은 빼며, 원격 측정은 온도, 부하, 메모리, 전력만 보여 주고, 숫자는 반올림하며 30초간 캐시한다. 공개 후 이틀 동안 PR을 스무 개 넘게 더 올려서, 이전하면서 빠뜨린 우클릭 메뉴, 검색 패널, 라이트박스, 뉴스레터, 관리자 화면을 채웠고, 페이지마다 공유 이미지도 백엔드에서 Rust의 tiny-skia로 다시 그려 APNG로 내보냈다.


여기까지 쓰는 동안에도 이 사이트는 계속 자라고 있다. 리뉴얼은 끝이 아니라, 그 선 위의 또 하나의 마디일 뿐이다. 마지막으로, 역시 내가 가장 좋아하는 그 말로 맺는다.

Stay hungry, stay foolish.

參考連結