dev.jihun.io
프로젝트 목록

2026. 08. - · 진행 중

jihun.io 리뉴얼

Vapor, Leaf, htmx로 통합한 서버 주도형 개인 웹

개발자 소개 문구와 프로필 일러스트를 나란히 배치한 dev.jihun.io 첫 화면
직무와 작업 범위를 먼저 설명하고, 개발 포트폴리오의 초록색과 프로필 일러스트로 개인 브랜드를 이어갑니다.

배경과 문제

기존 jihun.io는 여러 개인 페이지로 이동하는 링크 허브와 Next.js로 만든 개발 포트폴리오가 분리되어 있었습니다. 프로젝트를 더 잘 소개하는 화면이 필요했지만, 이번 작업의 출발점은 단순한 디자인 개선이 아니었습니다. Vapor와 Leaf로 HTML을 렌더링하고 htmx를 필요한 상호작용에만 사용하는 서버 주도형 웹을 직접 설계하고 운영해 보고 싶었습니다.

리뉴얼 범위는 개발 포트폴리오에서 개인 웹 허브와 기술 블로그까지 확장되었습니다. jihun.io, dev.jihun.io, blog.jihun.io가 서로 다른 역할을 가지면서도 하나의 애플리케이션 안에서 일관된 디자인과 탐색 경험을 제공해야 했습니다. 프로젝트와 게시글은 계속 수정할 수 있어야 했고, JavaScript가 없거나 htmx 요청이 아닌 상황에서도 모든 URL이 온전하게 작동해야 했습니다.

담당 범위

정보 구조와 모바일 우선 화면을 기획하고, Vapor 애플리케이션 구조와 Leaf 템플릿, CSS 디자인 시스템을 구현했습니다. 기존 포트폴리오와 블로그 콘텐츠를 Markdown 기반 저장소로 이관하고, 프로젝트 필터와 블로그 검색·페이지네이션, 도메인별 라우팅과 메타데이터를 구성했습니다.

Docker 이미지와 상태 확인 엔드포인트, 정적 파일 캐시와 보안 헤더도 함께 다뤘습니다. 화면 구현뿐 아니라 콘텐츠를 추가하고 검증한 뒤 배포하고 운영하는 흐름 전체를 개인 프로젝트의 범위로 삼았습니다.

핵심 기술적 결정

SPA를 모방하지 않는 서버 주도형 탐색

모든 공개 URL은 Vapor와 Leaf가 완전한 HTML 문서로 응답하도록 만들었습니다. htmx는 기본 탐색을 대체하는 전제 조건이 아니라, 프로젝트 필터와 블로그의 글 더 보기처럼 페이지 일부만 갱신하면 좋은 기능을 향상하는 용도로 사용했습니다. 같은 GET 요청이라도 일반 요청에는 전체 페이지를, htmx의 부분 갱신 요청에는 재사용 가능한 Leaf partial을 반환합니다.

프로젝트 기술 필터는 선택한 기술 중 하나라도 포함한 결과를 보여주는 OR 조건으로 구현했습니다. 선택 상태를 반복되는 tech 쿼리 파라미터에 기록해 결과 URL을 공유하거나 새로고침해도 같은 화면을 복원할 수 있습니다. JavaScript가 없으면 일반 GET 폼과 제출 버튼으로 같은 기능을 사용할 수 있고, 활성화된 환경에서는 결과 목록과 URL만 갱신됩니다.

사이트 전체의 내부 링크에는 htmx의 boosted navigation을 적용했습니다. 브라우저의 주소와 방문 기록, 서버 렌더링 문서는 그대로 유지하면서 전체 페이지 새로고침의 단절감을 줄였습니다. 페이지 전환 애니메이션은 사용하지 않되 View Transition의 렌더링 경계를 이용해 DOM 교체 중간 상태가 노출되지 않도록 구성했습니다.

Markdown을 읽기 전용 콘텐츠 저장소로 사용

프로젝트와 블로그 글은 YAML front matter가 포함된 Markdown 파일로 관리합니다. 공개 콘텐츠의 작성자가 한 명이고 변경이 Git 커밋과 배포를 통해 이뤄지므로, 별도의 데이터베이스와 관리자 화면을 두는 것보다 파일을 원본으로 삼는 편이 구조와 운영 방식에 맞았습니다.

애플리케이션이 시작될 때 모든 문서를 Swift 모델로 디코딩하고 Markdown을 HTML로 변환한 뒤 메모리에 색인합니다. 필수 메타데이터와 slug 중복, 날짜 형식, 참조한 이미지 파일의 존재 여부를 이 단계에서 검증해 잘못된 콘텐츠가 포함된 버전은 실행되지 않게 했습니다. 프로젝트는 시작 날짜순으로 정렬하고 기술별 색인을 만들며, 블로그는 날짜순 페이지와 제목·설명·태그·본문 검색을 위한 문자열을 미리 준비합니다.

이 방식은 실행 중 쓰기 기능이나 배포 없는 게시에는 적합하지 않습니다. 반대로 현재처럼 읽기 전용인 규모에서는 스키마 마이그레이션과 데이터 백업 계층을 만들지 않고도 콘텐츠와 코드의 변경 이력, 검토와 롤백을 Git 하나로 관리할 수 있습니다.

하나의 애플리케이션에서 세 도메인의 역할 분리

로컬에서는 /, /dev, /blog 경로로 모든 영역을 개발하고, 운영에서는 요청 호스트에 따라 각각 jihun.io, dev.jihun.io, blog.jihun.io의 경로 체계로 렌더링하도록 구성했습니다. 메인 도메인의 /dev/blog 접근은 해당 서브도메인으로 영구 이동시키고 쿼리 파라미터도 보존합니다.

같은 콘텐츠가 로컬 경로와 운영 서브도메인에서 서로 다른 링크를 가져야 하므로, URL을 템플릿에 직접 흩어 놓지 않고 영역별 경로 값으로 분리했습니다. 각 페이지의 canonical URL과 Open Graph 메타데이터, RSS의 게시글 주소도 정식 도메인을 기준으로 생성해 하나의 서버가 여러 정보 구조를 제공하더라도 공개 주소는 모호해지지 않게 했습니다.

규모에 맞춘 MVC 구조

초기에는 라우트 정의와 렌더링 로직이 한 파일에 모여 있었지만, 기능이 늘어나면서 RouteCollection 기반 Controller와 콘텐츠 Repository로 역할을 나눴습니다. Controller는 호스트와 요청 헤더를 해석하고 응답할 템플릿을 선택하며, Repository는 Markdown 로딩·검증·정렬·검색을 담당합니다. 공통 경로와 뷰 데이터는 별도의 타입으로 표현하고 routes.swift는 구성 요소를 조립하는 진입점으로만 남겼습니다.

반면 별도의 Service 계층은 두지 않았습니다. 아직 복잡한 업무 규칙이나 데이터 쓰기 흐름이 없어 Controller와 Repository 사이에 전달만 하는 계층을 추가하는 것은 구조를 설명하기보다 파일 수만 늘린다고 판단했습니다.

기존 블로그 문서를 보존하는 렌더러

기존 블로그의 게시글과 이미지를 그대로 옮기되 댓글 기능은 제외했습니다. Ink를 기반으로 내부 링크와 상대 이미지 경로를 현재 도메인 구조에 맞게 변환하고, 제목 식별자와 각주, 코드 블록처럼 기존 문서에 필요한 표현을 보완하는 렌더러를 만들었습니다. 게시글 목록에는 명시적인 페이지 URL과 htmx 기반의 글 더 보기를 함께 제공하고, 제목·설명·태그·본문을 서버에서 검색하는 GET 엔드포인트를 추가했습니다. RSS는 화면에서 강조하지 않지만 기존 구독 주소와 검색 엔진이 발견할 수 있는 문서 연결은 유지했습니다.

디자인과 정적 자산을 애플리케이션의 일부로 관리

화면은 작은 너비에서 먼저 설계하고, 개발 포트폴리오의 초록색과 블로그의 파란색을 각 영역의 키 컬러로 사용했습니다. 시맨틱 HTML을 우선하고 동작 줄이기 설정, 키보드 포커스, 충분한 터치 영역, 이미지 크기와 대체 텍스트를 함께 고려했습니다. 기술 필터는 모바일에서 긴 목록을 접을 수 있는 details 요소로 만들고, 선택된 조건이 있으면 처음부터 펼쳐지도록 했습니다.

CSS는 별도 빌드 도구 없이 디자인 토큰과 기반 스타일, 페이지·기능별 스타일시트를 여러 <link> 요소로 불러옵니다. 서체와 htmx, 프로젝트 및 블로그 이미지는 애플리케이션에서 직접 제공하며, 파비콘과 Open Graph 이미지는 같은 색상 팔레트의 HTML·CSS 원본에서 다시 내보낼 수 있도록 구성했습니다.

구현 및 트레이드오프

htmx 응답은 요청 목적에 따라 전체 문서와 HTML 조각이 달라지므로 HX-Request, HX-Boosted, HX-Target을 구분하고 Vary 헤더를 설정했습니다. 필터를 빠르게 연속 선택할 때는 앞선 요청을 교체하고, 로딩 표시가 짧게 깜빡이지 않도록 지연과 최소 노출 시간을 조정했습니다. details의 접힘 애니메이션과 View Transition은 Safari의 구현 차이도 확인하며, 의미 있는 마크업과 안정적인 화면 전환을 모두 유지하는 방향으로 보완했습니다.

배포용 이미지는 다단계 Docker 빌드로 만들고 비루트 사용자로 실행합니다. 정적 파일은 운영 환경에서 ETag와 캐시를 사용하며, 애플리케이션 응답에는 CSP와 HSTS를 비롯한 보안 헤더를 적용했습니다.

배포는 GitHub Actions가 테스트와 컨테이너 이미지 게시를 담당하고, Argo CD가 Git에 선언된 Kubernetes 리소스를 동기화하는 GitOps 흐름으로 구성할 예정입니다. CI가 운영 환경을 직접 변경하지 않게 역할을 분리하고, 커밋과 이미지, 배포 상태의 관계를 추적할 수 있게 하는 것이 목표입니다. 상태 확인 엔드포인트는 readiness와 liveness probe에 연결하고, 실행 권한과 capability 제한은 컨테이너와 Pod의 보안 설정에 함께 적용할 계획입니다.

서버가 HTML을 완성해 보내는 구조는 클라이언트 상태 관리가 단순하고 기본 기능이 JavaScript에 의존하지 않는다는 장점이 있습니다. 대신 작은 상호작용도 서버 응답과 템플릿 경계를 함께 설계해야 하고, 한 애플리케이션에서 여러 도메인을 다루기 때문에 요청 호스트와 링크 생성 규칙을 테스트로 지속해서 확인해야 합니다.

결과

개인 웹 허브, 개발 포트폴리오와 기술 블로그를 하나의 Vapor 애플리케이션으로 통합했습니다. 프로젝트는 기술별로 탐색할 수 있고, 각 상세 페이지에서 문제와 담당 범위, 선택과 결과를 독립된 문서로 읽을 수 있습니다. 기존 블로그 게시글과 이미지도 새 디자인 언어로 옮겨 목록 페이지네이션과 검색, RSS를 포함한 읽기 흐름을 다시 구성했습니다.

라우팅과 리다이렉트, 일반 요청과 htmx 요청의 응답 차이, Markdown 검증과 검색·정렬 규칙은 자동화된 테스트로 확인하고 있습니다. 현재는 공개 배포를 준비하는 단계이므로 방문자 성능 지표와 운영 안정성에 관한 결과는 배포 후 실제 측정값으로 추가할 예정입니다.

회고

처음에는 Vapor, Leaf와 htmx를 사용해 보고 싶다는 기술적 호기심에서 시작했지만, 프레임워크를 선택하는 것만으로 서버 주도형 웹이 완성되지는 않았습니다. URL에 상태를 남기는 방법, 전체 문서와 조각 응답의 경계, JavaScript가 없을 때의 동작, 콘텐츠 검증과 배포까지 함께 설계해야 비로소 하나의 제품이 되었습니다.

이번 작업을 통해 서버 렌더링과 점진적 향상은 과거 방식으로 돌아가는 선택이 아니라, 필요한 복잡성만 클라이언트에 두기 위한 적극적인 설계가 될 수 있음을 확인하고 있습니다. 공개 이후에는 실제 탐색 성능과 운영 경험을 바탕으로 현재의 선택을 다시 검토하고 기록할 계획입니다.

화면과 흐름

URL에 상태를 남기는 프로젝트 필터

htmx와 Vapor 기술이 선택되고 해당 조건의 프로젝트 한 개가 표시된 프로젝트 목록 화면
여러 기술을 OR 조건으로 선택하면 서버가 결과를 다시 렌더링하고, 선택 상태를 쿼리 파라미터에 기록합니다.

하나로 연결한 개인 웹과 기술 블로그

개발, 블로그와 갤러리로 이동하는 jihun.io 모바일 허브 화면
루트 도메인은 서로 다른 역할을 가진 개인 페이지로 이동하는 간결한 웹 허브로 구성했습니다.
Cloudflare 검색어와 여섯 개의 결과를 보여주는 blog.jihun.io 모바일 검색 화면
기존 게시글의 제목과 설명, 태그와 본문을 서버에서 검색하고 공유 가능한 GET 결과로 제공합니다.