프로토타입과 운영 코드는 책임이 달랐다
Open Design v38에는 mint 계열 색상과 Maru Buri 제목, Pretendard 본문, JetBrains Mono 메타데이터, 에디토리얼 행 구성이 준비돼 있었다. 하지만 예시 데이터 배열과 Client Header, 자체 Markdown 파서와 문법 토크나이저를 사용하는 독립 프로토타입이었다.
운영 블로그는 백엔드 데이터와 Server Component, on-demand ISR, 요청 시 MDX 변환과 검색 Route Handler를 사용했다. 프로토타입을 그대로 복사하면 모의 데이터와 두 번째 렌더러, 넓어진 클라이언트 경계, Next 메이저 버전 전환까지 함께 들어온다.
가져올 대상은 애플리케이션 템플릿이 아니라 시각적 의도였다.
디자인 전 의존성 변경을 끝냈다
재설계와 실행 환경 업그레이드를 섞지 않기 위해 기준선을 세 단계로 정리했다.
- Next.js 15.5.12를 보안 패치인 15.5.23으로 올리고 ESLint와 PostCSS, lockfile을 정리했다.
- React 19.2.8, Radix 1.x,
next-themes0.4.6, KaTeX와rehype-pretty-codepatch를 적용했다. - Tailwind 4.3.3, TypeScript 5.9.3과 테스트 도구 체인을 올렸다.
매 단계에서 테스트와 lint, 일반 build와 백엔드에 연결할 수 없는 build, route와 cache, MDX와 검색을 확인했다. 프로토타입이 Next 16이라는 이유로 운영도 올리지 않았고 Shiki 4와 KaTeX 0.18 같은 메이저 버전도 디자인 변경에서 제외했다.
시각과 데이터의 기준을 나눴다
| 질문 | 기준 |
|---|---|
| 색상과 글꼴, 간격 | Open Design v38 |
| 공개 글과 순서 | 백엔드 API |
| 프로젝트 제목과 링크 | src/data/projects.ts |
| 경로와 페이지 나누기, 404 | 기존 애플리케이션 |
| Markdown과 코드·수식·이미지 | 기존 MDX 처리 과정 |
| 캐시와 무효화 | 기존 실행 계약 |
| 키보드와 포커스 | 운영 접근성 계약 |
충돌하면 실제 데이터와 운영 계약을 먼저 지키고 표현을 프로토타입 의도에 맞췄다.
첫 UI 변경은 페이지가 아니라 OKLCH token과 글꼴, 로고였다. 기존 Tailwind 의미 변수와 .dark, prose,
KaTeX와 Shiki 스타일을 유지해 globals.css를 통째로 복사하지 않았다. Pretendard는 본문, Maru Buri는 주요
제목, JetBrains Mono는 날짜와 숫자, 코드에 사용했다. 글꼴 원본과 라이선스를 확인하고 JetBrains Mono
OFL도 저장소에 남겼다.
서버와 클라이언트 책임은 유지했다
고정 Header와 내비게이션, 워드마크는 프로토타입을 따랐지만 Root Layout은 Server Component로 남겼다.
RootLayout Server
-> Header Server composition
-> Active Nav, Search, Theme, Mobile Sheet만 Client
-> Page Server
-> Footer Server테마는 기존 next-themes, Dialog와 Sheet는 Radix를 사용했다. 포커스 가두기와 Escape, 포커스 복귀를 다시
구현하지 않았다.
Home도 서버에서 실제 데이터를 준비했다. featuredProjects와 백엔드 0페이지의 최신 공개 글 세 개를 패널
마크업으로 만들고, 휠과 키보드, 터치, 동작 줄이기 설정만 HomePanelNavigator 클라이언트 아일랜드가 처리했다.
글이 없으면 모의 데이터로 채우지 않고 빈 상태를 보여 줬다. connection()과 300초 캐시, tag 무효화 계약도
유지해 build를 백엔드에 다시 결합하지 않았다.
실제 프로젝트와 글을 디자인에 연결했다
프로젝트는 실제 저장소 링크를 확인해 src/data/projects.ts 한 곳에 id, title, description, category,
featured·archive group, href만 저장했다. /projects와 Home은 같은 배열을 사용하고 짧은 category label은
표시용 함수에서만 만들었다.
블로그 목록은 공통 BlogListingPage에 제목과 주제 내비게이션, Featured 행, archive 목록과 페이지 나누기를
적용했다. 글과 전체 수, featured flag와 category count, totalPages는 백엔드 값을 그대로 사용했다. 디자인을
맞추려고 다시 정렬하거나 현재 페이지 밖의 글을 가져오지 않았다. 알 수 없거나 범위를 벗어난 404도 유지했다.
상세 화면은 기존 렌더러 주위만 바꿨다
프로토타입의 자체 파서를 가져오지 않았다. 운영에는 이미 다음 경계가 있었다.
rawBody + assetBaseUrl
-> GFM와 math
-> 자산 URL rewrite
-> KaTeX와 Shiki
-> serialized MDX cacheHero와 왼쪽 정보 영역, 본문 너비와 TOC를 이 출력 주변에 구성했다. TOC와 읽는 시간은 서버에서 만들고 스크롤 진행률과 활성 제목, 코드 복사만 클라이언트로 뒀다. 코드 블록 마크업 변화는 MDX 렌더러 버전을 올려 캐시 식별자에 반영했다.
디자인의 글 이동 영역에는 백엔드 데이터가 없었다. 프런트엔드에서 페이지나 slug로 추측하지 않고 상세
응답에 nullable platform, problemId, newerPost, olderPost projection을 추가했다. 이웃 글은 공개
전체 글의 postDate DESC, id DESC 순서를 따른다. 없는 영역은 비활성 상태로 남겼다.
검색은 표현만 바꿨다
Search Dialog의 에디토리얼 구성과 한국어 문구, 결과 위계는 새로 그렸지만 기존 흐름은 유지했다.
입력
-> 300ms debounce
-> 이전 request 취소
-> same-origin /api/search
-> 백엔드 순서의 summaryRadix 포커스 동작과 로딩·빈 결과·오류 상태, no-store와 제한된 응답도 그대로였다. 백엔드 오류를 빈
결과로 숨기지 않았다.
화면을 단계별로 배포했다
기반과 shell, Projects, Home, Listing, Detail, Search를 각각 독립 단계로 배포했다. 각 단계에서 테스트와 lint, 일반·백엔드 불가 build를 통과시키고 READY 배포 뒤 데스크톱과 모바일, 밝은 테마와 어두운 테마, 키보드와 포커스, MDX 코드·수식·이미지·표, 캐시 계약을 확인했다.
이 방식은 컴포넌트 책임과 실제 콘텐츠 길이, 빈 결과·오류 상태, 마우스와 키보드·터치·동작 줄이기 설정을 모두 검토해야 해 시간이 더 들었다. 글 이동 기능은 백엔드 API와 테스트, 문서까지 바꿔야 했다.
대신 모의 데이터와 두 번째 파서를 운영에 남기지 않았고 Home 전체를 클라이언트 애플리케이션으로 바꾸지 않았다. Posts checkout과 생성 콘텐츠도 돌아오지 않았다.
기능과 아키텍처가 정상이라는 검증이 디자인 충실도까지 증명하는 것은 아니다. 실제 데이터와 접근성 때문에 의도적으로 달라진 부분과 첫 구현이 프로토타입을 충분히 따르지 못한 부분은 별도의 시각 검수가 필요했다.