ruehan.dev← 모든 글
#blog

데스크톱처럼 탐색하고, 웹 문서처럼 읽히는 블로그 만들기

ruehan··9분

블로그를 열 때마다 작업 화면과 멀어졌다

익숙한 템플릿이 내 공간처럼 느껴지지 않았다

기존 블로그를 열면 카드와 큰 히어로 이미지가 먼저 보였다. 정돈된 폰트와 익숙한 레이아웃도 있었다. 글을 올리기는 편했지만, 매일 바라보는 터미널과 에디터의 화면과는 결이 달랐다.

문제는 템플릿이 예쁜지 아닌지가 아니었다. 개발하면서 쌓인 취향은 어두운 패널, 촘촘한 정보, 키보드로 이어지는 탐색에 가까웠다. 그런데 정작 내가 가장 오래 운영할 글 공간은 그 감각에서 떨어져 있었다.

블로그를 열었을 때 보고 싶었던 장면은 비교적 분명했다.

운영체제를 웹에 그대로 복제하려는 것은 아니었다. 데스크톱에서 익힌 탐색 방식을 글을 읽고 찾는 경험에 옮기고 싶었다.

그래서 이 블로그의 출발점은 테마를 교체하는 일이 아니라, 글을 담을 작은 데스크톱 셸을 만드는 일이 됐다.

작업 환경의 미감을 장식으로 붙이는 대신, 블로그를 탐색하는 방식으로 사용하고 싶었다.

결과적으로 블로그는 두 개의 입구를 갖게 됐다. 방문자는 데스크톱 셸 안에서 글을 찾고 읽을 수 있고, 검색이나 공유 링크를 통해 들어온 사람은 독립된 글 페이지로 바로 접근할 수 있다.

이 글은 데스크톱 모양을 구현한 과정 자체보다, 그 셸을 평범한 웹 문서와 공존시키기 위해 어떤 경계를 나눴는지를 다룬다.

첫 화면은 68KB짜리 HTML 한 장이었다

먼저 화면의 감각부터 확인했다

첫 시도는 68KB짜리 index.html 하나였다. 부팅 시퀀스와 데스크톱 셸, Files와 Reader까지 모두 그 파일 안에 들어 있었다.

이때는 프레임워크나 콘텐츠 모델보다, 내가 원하는 화면의 감각이 브라우저에서도 성립하는지를 먼저 확인하고 싶었다.

검증하려던 범위도 작게 잡았다.

  1. 부팅 로그가 끝난 뒤 셸이 자연스럽게 나타나는가
  2. 창을 열고 닫는 동작이 브라우저 안에서도 어색하지 않은가
  3. Files에서 선택한 글을 Reader에서 읽을 수 있는가
  4. 데스크톱이라는 외형이 본문 읽기를 방해하지 않는가

단일 파일은 이 질문에 빠르게 답하기 좋았다. 상태와 마크업이 한곳에 있으니 수정한 결과를 바로 확인할 수 있었고, 데스크톱 형태의 블로그가 실제로 가능한지 판단하기에도 충분했다.

text
index.html (68KB)
├── boot sequence
├── desktop shell
├── window state
├── Files
└── Reader + post content

단순한 구조가 문서의 입구를 감췄다

문제는 글이 셸 안에만 존재한다는 점이었다.

특정 글을 공유해도 전달되는 주소는 항상 같은 index.html이었고, 검색 엔진이 글 하나를 독립된 문서로 이해하게 만들기도 어려웠다. Reader 창을 정교하게 만들어도 바깥에서는 하나의 인터랙티브 데모 페이지로만 보였다.

처음에는 파일 하나라는 단순함이 장점이었다. 그러나 글이 늘어나자 그 단순함이 셸과 문서의 역할을 흐리기 시작했다.

화면은 블로그처럼 보였지만, 웹 문서가 가져야 할 독립적인 입구가 없었다.

셸과 글을 서로 다른 입구로 나눴다

인터랙션과 문서가 맡을 일을 분리했다

Next.js 16 App Router로 옮기면서 구조를 두 영역으로 나눴다.

데스크톱 셸은 클라이언트 컴포넌트가 담당하고, 각 글은 /posts/[slug] 서버 라우트에서 독립된 문서로 렌더링한다. 어느 한쪽을 포기하는 대신, 서로 잘하는 일에 집중하도록 만든 구조다.

구분

단일 HTML

분리한 구조

인터랙션

셸과 글 상태가 한 파일에 섞임

클라이언트 셸이 창과 입력을 담당

글 주소

Reader 내부에만 존재

글마다 /posts/[slug] 제공

검색·공유

글별 메타데이터 구성 어려움

글별 메타, OG, JSON-LD 생성

읽기 경험

셸을 거쳐야만 접근 가능

일반적인 웹 문서로 바로 접근

셸에서는 Files로 글을 찾고 Reader로 본문을 읽는다. Reader에서 원문 링크를 선택하면 같은 콘텐츠의 서버 페이지로 이동한다.

반대로 검색 결과나 공유 링크를 통해 들어온 방문자는 데스크톱 셸을 거치지 않고도 글을 읽을 수 있다.

분리한 뒤에는 각 영역의 책임도 명확해졌다.

인터랙션은 셸에 남기고, 글은 평범한 웹 문서로 내보냈다. 데스크톱 모양보다 읽을 수 있는 주소가 먼저였다.

이 분리는 SEO를 위한 별도 페이지를 억지로 덧붙인 것이 아니었다. 셸은 탐색 경험을 풍부하게 만들고, 서버 라우트는 웹 문서의 기본을 지킨다. 둘은 같은 콘텐츠를 사용하지만 서로 다른 역할을 맡는다.

글 저장소는 Sanity를 거쳐 Markdown으로 돌아왔다

처음에는 웹 편집 환경이 필요했다

초기에는 블로그 글도 Sanity Studio에서 관리했다.

코드를 직접 수정하지 않고 글을 편집할 수 있었고, 앨범·아티스트·사진·트랙처럼 구조가 다른 콘텐츠를 한곳에서 다루기에도 적합했다. 셸은 60초 ISR 주기로 Sanity 콘텐츠의 변경 여부를 다시 확인했다.

하지만 2026년 7월, 가져온 글이 익명 API에서 보이지 않는 문제가 발생했다. 인증된 라이브 API에서는 읽을 수 있었지만, CDN과 익명 라이브 API에서는 조회되지 않았다.

당시에는 useCdn: false와 읽기 토큰을 사용해 화면을 복구했다. 다만 이 대응은 임시 우회라는 점도 함께 기록했다.

화면은 다시 동작했지만, 글 하나를 발행하기 위해 확인해야 할 경로는 더 많아졌다.

이전에는 글이 보이지 않으면 본문, 스키마, 공개 설정, CDN, 토큰을 차례로 살펴봐야 했다. 글을 쓰는 경험보다 콘텐츠가 전달되는 경로를 관리하는 일이 더 복잡해지기 시작했다.

모든 콘텐츠에 같은 저장소를 사용할 필요는 없었다

이후 블로그 글은 content/posts/*.md에서 직접 읽도록 변경했다.

반면 이미지 업로드와 문서 간 참조가 중요한 앨범·아티스트·사진·트랙은 Sanity에 남겼다. 하나의 도구로 모든 콘텐츠를 통일하는 대신, 각 콘텐츠의 운영 방식에 맞는 저장소를 선택했다.

콘텐츠

저장소

선택한 이유

블로그 글

로컬 Markdown

diff, PR, frontmatter 중심 운영

앨범·아티스트

Sanity Studio

문서 간 참조와 구조화된 필드

사진·트랙

Sanity Studio

이미지 업로드와 미디어 관리

현재 블로그 글의 작업 흐름은 단순하다.

text
Markdown 작성
→ 로컬 미리보기
→ 커밋과 PR
→ 배포

Markdown 글은 정적 빌드 과정에서 읽기 때문에 새 글을 반영하려면 다시 배포해야 한다. 웹에서 즉시 수정할 수 있었던 Sanity와 비교하면 분명한 비용이다.

대신 글의 원본과 변경 이력을 저장소에서 바로 확인할 수 있고, 문제가 발생하는 위치도 훨씬 좁아졌다.

Sanity가 잘못된 선택이었던 것은 아니다. 미디어와 구조화된 문서를 다루는 데는 여전히 잘 맞는다. 다만 코드 리뷰와 변경 이력을 중심으로 운영하는 블로그 글에는 Markdown이 더 자연스러웠다.

Markdown 표 하나가 변환의 경계를 드러냈다

저장 형식이 바뀌어도 렌더 경로는 유지했다

글 페이지와 셸의 Reader는 이미 Portable Text 렌더러를 공유하고 있었다. 저장소를 Markdown으로 바꿨다고 본문 렌더링 경로까지 두 개로 나누고 싶지는 않았다.

그래서 입력은 Markdown으로 받되, 기존 화면이 이해하는 Portable Text 블록으로 변환하기로 했다.

현재 흐름은 다음과 같다.

text
content/posts/*.md
  → parsePostFrontmatter()
  → @portabletext/markdown
  → Portable Text blocks
  → PostBody
  → Reader / /posts/[slug]

@portabletext/markdown은 헤딩, 인용, 목록, 이미지 같은 기본 요소를 Portable Text 블록으로 변환해 준다.

코드블록은 프로젝트에서 이미 사용하던 codeBlock 타입에 맞췄고, PostBody 내부의 클라이언트 컴포넌트가 shiki를 이용해 문법 강조를 처리한다.

덕분에 셸의 Reader와 서버 글 페이지는 저장 형식이 바뀐 뒤에도 같은 렌더러를 계속 사용할 수 있었다.

GFM 표는 필요한 범위만 직접 변환했다

예외는 표였다.

GFM 형식의 표를 넣자 행과 셀이 하나의 표 블록으로 유지되지 않았다. 그렇다고 Markdown 전체를 직접 파싱하거나, 기존 변환기를 모두 교체하고 싶지는 않았다.

대신 입력을 표 구간과 일반 Markdown 구간으로 먼저 나눴다.

  1. GFM 헤더와 구분선을 찾아 표의 시작을 확인한다.
  2. 표 앞뒤의 일반 Markdown은 기존 변환기에 맡긴다.
  3. 표의 행과 셀만 buildTableBlock으로 변환한다.
  4. PostBody가 해당 블록을 실제 <table> 요소로 렌더링한다.

이 글에 포함된 표도 같은 경로를 거친다.

변환 라이브러리를 사용한다고 해서 모든 입력을 위임할 필요는 없었다. 지원되지 않는 경계만 좁게 맡는 편이 더 단순했다.

핵심은 Markdown 전체를 직접 해석하는 것이 아니었다. 기존 도구가 잘 처리하는 영역은 그대로 두고, 프로젝트에 필요한 예외만 추가했다.

지금의 ruehanix는 셸과 문서를 함께 가진다

셸은 탐색을 풍부하게 만든다

현재 셸에는 Files, Reader, Foto, Hotlap, Music, About, Terminal, Settings, Web까지 아홉 개 앱이 있다.

콘텐츠는 다음 다섯 가지 카테고리로 나뉜다.

데스크톱에서는 타일형 워크스페이스를 중심으로 사용하는 G1과, 창을 자유롭게 배치하는 G2를 제공한다. 키보드 단축키와 focus trap도 적용했다.

모바일에서도 같은 셸과 콘텐츠에 접근할 수 있지만, 창을 이동하고 타일을 다루는 경험은 아직 데스크톱 쪽이 더 자연스럽다.

다만 앱의 수나 창 관리 방식보다 중요한 기준은 따로 있다. 셸 안에서 읽는 모든 글이 셸 밖에서도 독립된 웹 문서로 존재해야 한다는 점이다.

글은 셸 밖에서도 글이어야 한다

서버 라우트는 제목, 요약, 발행일과 본문을 독립된 문서로 렌더링한다. 각 글에는 canonical 메타데이터와 동적 OG 이미지가 생성되고, JSON-LD를 통해 문서의 성격을 전달한다.

방문자가 셸을 건너뛰더라도 글을 읽거나 공유하는 데 문제가 없다.

현재 구조에서 중요하게 남은 기준은 세 가지다.

소스 코드는 GitHub의 ruehanix 저장소에서 확인할 수 있다.

이번 글에서는 모든 구현 세부를 설명하기보다, 프로젝트의 방향을 바꾼 결정과 경계에 집중했다.

결정 기록이 다음 작업의 출발점이 됐다

코드는 결과를, ADR은 이유를 남긴다

단일 HTML을 나눈 이유, Sanity 우회를 임시 대응으로 본 이유, Markdown 표를 별도 블록으로 변환한 이유는 시간이 지나면 쉽게 흐려진다.

그래서 구조를 바꾸거나 중요한 선택을 내릴 때마다 ADR을 작성했다. 현재 기록은 50개를 넘었고, 프로젝트가 지금의 구조에 도달한 과정을 설명하는 기준점으로 사용하고 있다.

ADR에는 거창한 결론보다 당시의 조건을 남겼다.

코드는 현재 상태를 보여주고, 결정 기록은 그 상태까지 온 이유를 보여준다.

결정 기록은 과거를 정당화하기 위한 문서가 아니다. 같은 문제를 다시 만났을 때 처음부터 고민하지 않기 위한 출발점에 가깝다.

아직 다듬어야 할 부분

ruehanix는 한 명이 운영하는 사이드 프로젝트다. 외부 필자를 초대하거나 공동으로 글을 작성하는 흐름은 아직 없다. 모바일 셸도 사용할 수 있지만, 데스크톱만큼 자연스럽지는 않다.

앞으로는 다음 범위를 우선적으로 다듬을 계획이다.

  1. 모바일에서 창 탐색과 전환 흐름 개선
  2. 셸의 About과 정보 패널 보강
  3. Markdown 운영이 안정된 뒤 사진과 랩타임 데이터의 저장 구조 재검토

템플릿에서 벗어난다는 것은 웹의 익숙한 규칙을 모두 버리는 일이 아니었다.

셸에는 내가 매일 사용하는 작업 환경의 감각을 담고, 글에는 주소와 메타데이터를 가진 웹 문서의 기본을 남겼다.

결국 새로 만든 것은 데스크톱의 외형보다, 취향과 접근성을 함께 유지할 수 있는 경계였다.