이 사이트에 대해

이곳은 Bojin Li의 개인 웹사이트입니다.

변천사

연도

기술 스택

2019

GitHub Pages + Jekyll + Disqus, Huxpro 테마

2020

Vercel + Jekyll + Disqus, Huxpro 테마

2021

Vercel + React 17 + Tailwind CSS + Vite + TypeScript

2022

Vercel + Next.js 12 + Notion API + Giscus + p5 + three.js

2024

Vercel + Next.js 14/15 + Tailwind CSS + shadcn/ui

2026(현재 버전, Haku)

Cloudflare Workers + TanStack Start + Vite + Tailwind CSS + Durable Objects

Haku에 대해

Haku는 Yohaku를 참고해 만들었습니다. 여백과 종이, 섬세한 모션이 주는 느낌 등, Yohaku가 중시하는 읽기 경험을 이어가며 리치 텍스트 글도 마찬가지로 완벽하게 렌더링합니다. Haku라는 이름은 Yohaku의 일부를 남긴 것이기도 하고, 마침 Haku가 제 이름에 들어가는 '帛'의 일본어 발음이기도 해서 붙였습니다.

Yohaku는 Next.js 기반이지만 이 사이트는 Cloudflare Workers에 배포되어 있으며, 중국 본토의 특수한 네트워크 환경에 맞춰 최대한 최적화할 필요도 있었습니다. 그래서 TanStack Start, TanStack Router, Vite를 사용하고 Durable Objects를 도입했습니다. 이렇게 하면 Worker에서 SSR 성능을 저수준부터 최대한 최적화할 수 있고, Next.js의 무거운 구조와 Vercel 이외의 플랫폼에서 생기는 불확실성도 피할 수 있습니다.

백엔드의 경우, Yohaku와 함께 제공되는 기능 중 이 사이트에서 쓰지 않는 것이 많고, Haku에는 별도의 맞춤 기능과 최적화도 적지 않습니다. 그래서 Yohaku를 참고해 CMS를 새로 만들었고, 프로토콜은 Yohaku와 연동되는 mx-space에 최대한 맞췄습니다.

전체 구조

monorepo는 Web, Core, Admin의 세 애플리케이션으로 나뉩니다.

애플리케이션

역할

기술

배포

Web

홈 화면, 글, 독립 페이지를 포함한 공개 웹사이트

React, TanStack Start / Router, Vite, Tailwind CSS

Cloudflare Workers

Core

콘텐츠, 미디어, 인증, 댓글 등의 백엔드 서비스

NestJS, Fastify, Drizzle, PostgreSQL, Redis

직접 운영하는 서버, Dokploy로 관리

Admin

콘텐츠 관리 화면

React, Vite, CodeMirror, Haklex / Lexical

직접 운영하는 서버, Dokploy로 관리

Mermaid diagram

세 애플리케이션은 TypeScript와 Zod로 정의한 공통 계약을 통해 연동되며, 요청과 응답의 경계에서도 이 계약으로 데이터를 검증합니다.

서버 사이드 렌더링

Haku는 서버 사이드 렌더링을 사용합니다. 처음 페이지를 열면 Worker가 라우트와 언어에 따라 필요한 데이터를 가져와 내용이 담긴 HTML을 생성한 뒤 브라우저에 스트리밍으로 전달합니다. 덕분에 스크립트가 모두 로드될 때까지 기다리지 않고도 내용을 볼 수 있습니다.

이어서 브라우저가 hydration을 수행해 서버가 만들어 둔 페이지를 이어받고, 탐색, 메뉴, 댓글 등의 상호작용이 작동하게 합니다. 그 뒤의 사이트 내 이동은 TanStack Router가 처리합니다. 다음 페이지에 필요한 데이터만 가져오므로 매번 문서 전체를 다시 로드하지 않아도 됩니다.

Mermaid diagram

서버 사이드 렌더링에서 브라우저로 자연스럽게 이어지도록, 서버의 각 요청마다 Router, QueryClient, Jotai store를 따로 만들고 브라우저에도 각각 하나씩 둡니다. 덕분에 서로 다른 독자의 데이터가 섞이지 않습니다. 진입 애니메이션도 라우트와 함께 처리합니다. 처음 열 때, 일반적인 페이지 이동, 뒤로 가기와 앞으로 가기로 복원할 때의 동작을 각각 구분하며, 데이터를 미리 가져오거나 백그라운드에서 갱신할 때 페이지 애니메이션이 이유 없이 다시 재생되지 않습니다.

캐시

사이트 설정, 탐색 정보, 홈 화면의 각종 요약은 모든 독자에게 동일하므로 공유하기에 적합합니다. 이 데이터는 Cloudflare의 Durable Objects에 공개 스냅샷으로 저장됩니다. 같은 데이터를 동시에 읽으려는 요청은 하나로 합치고, 스냅샷이 만료되면 보관 중인 이전 내용을 먼저 반환하면서 백그라운드에서 갱신하는 stale-while-revalidate 방식을 사용해 대기 시간을 줄입니다. 새 콘텐츠가 발행되면 기존 스냅샷을 만료 처리합니다. 캐시를 갱신할 때는 버전도 확인하므로, 먼저 시작된 요청이 나중에 도착해도 이전 데이터가 새 데이터를 덮어쓸 수 없습니다.

글 본문은 이 공유 캐시에 넣지 않고 매번 발행된 콘텐츠에서 읽습니다. HTML과 서버 함수의 응답도 방문자 사이에 공유하지 않습니다.

홈 화면에 표시하는 각종 활동과 통계 데이터도 비슷한 방식으로 처리합니다. 백엔드에서 주기적으로 미리 집계해 두므로 독자가 홈 화면을 열 때 그때그때 외부 서비스에 요청을 보낼 필요가 없고, 외부 서비스가 잠시 느려져도 본문을 읽는 데 영향을 주지 않습니다.

접속 속도 최적화

Haku는 접속 속도를 높이기 위해 다음을 적용했습니다.

  • 사이트 초기 데이터의 크기를 최대한 줄여 탐색과 현재 페이지에 필요한 정보만 전달하며, 관계없는 페이지의 내용은 포함하지 않습니다.
  • 라우트 loader를 쿼리 캐시와 연결해 한 번의 페이지 이동 중 데이터를 중복해서 읽는 일을 줄입니다.
  • 리치 텍스트, 지도, 3D처럼 무거운 의존성은 사용 상황별로 나누어 필요할 때만 로드합니다.
  • 본문에 쓰는 폰트, 언어, 렌더링 모듈을 미리 준비해 새로고침이나 hydration 때 불일치가 생기지 않도록 합니다.
  • 배경과 3D 모션에는 실행 스케줄링을 적용했습니다. 페이지가 숨겨져 있거나 독자가 모션 줄이기를 설정한 경우, 일시 정지하거나 정적인 표현으로 바꾸어 읽기에 쓸 자원을 최대한 아낍니다.

또한 Haku는 Chrome 개발자 도구의 네트워크 제한 모드에서 SSR과 첫 화면을 반복해서 테스트했습니다. Sentry도 도입해 운영 환경에서 발생하는 오류와 성능 문제를 추적합니다.

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...