2026. 08. - · 진행 중
jihun.io 리뉴얼
Vapor, Leaf, htmx로 통합한 서버 주도형 개인 웹
배경과 문제
기존 jihun.io는 여러 개인 페이지로 이동하는 링크 허브와 Next.js로 만든 개발 포트폴리오가 분리되어 있었습니다. 프로젝트를 더 잘 소개하는 화면이 필요했지만, 이번 작업의 출발점은 단순한 디자인 개선이 아니었습니다. Swift 기반의 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을 적용했습니다. 브라우저의 주소와 방문 기록, 서버 렌더링 문서는 그대로 유지하면서 전체 페이지 새로고침의 단절감을 줄였습니다.
Markdown을 읽기 전용 콘텐츠 저장소로 사용
프로젝트와 블로그 글은 YAML front matter가 포함된 Markdown 파일로 관리합니다. 공개 콘텐츠의 작성자가 한 명이고 변경이 Git 커밋과 배포를 통해 이뤄지므로, 별도의 데이터베이스와 관리자 화면을 두는 것보다 파일을 원본으로 삼는 편이 구조와 운영 방식에 적합했습니다.
애플리케이션이 시작될 때 모든 문서를 Swift 모델로 디코딩하고 Markdown을 HTML로 변환한 뒤 메모리에 색인합니다. 필수 메타데이터와 slug 중복, 날짜 형식, 참조한 이미지 파일의 존재 여부를 이 단계에서 검증해 잘못된 콘텐츠가 포함된 버전은 실행되지 않게 했습니다. 프로젝트는 시작 날짜순으로 정렬하고 기술별 색인을 만들며, 블로그는 날짜순 페이지와 제목·설명·태그·본문 검색을 위한 문자열을 미리 준비합니다.
이 방식은 실행 중 쓰기 기능이나 배포 없는 게시에는 적합하지 않습니다. 반대로 현재처럼 읽기 전용인 규모에서는 스키마 마이그레이션과 데이터 백업 계층을 만들지 않고도 콘텐츠와 코드의 변경 이력, 검토와 롤백을 Git 하나로 관리할 수 있습니다.
디자인과 정적 자산을 애플리케이션의 일부로 관리
화면은 작은 너비에서 먼저 설계하고, 개발 포트폴리오의 초록색과 블로그의 파란색을 각 영역의 키 컬러로 사용했습니다. 시맨틱 HTML을 우선하고 동작 줄이기 설정, 키보드 포커스, 충분한 터치 영역, 이미지 크기와 대체 텍스트를 함께 고려했습니다. 기술 필터는 모바일에서 긴 목록을 접을 수 있는 details 요소로 만들고, 선택된 조건이 있으면 처음부터 펼쳐지도록 했습니다.
CSS는 별도 빌드 도구 없이 디자인 토큰과 기반 스타일, 페이지·기능별 스타일시트를 여러 <link> 요소로 불러옵니다. 서체와 htmx, 프로젝트 및 블로그 이미지는 애플리케이션에서 직접 제공하며, 파비콘과 Open Graph 이미지는 같은 색상 팔레트의 HTML·CSS 원본에서 다시 내보낼 수 있도록 구성했습니다.
구현 및 트레이드오프
htmx 응답은 요청 목적에 따라 전체 문서와 HTML 조각이 달라지므로 HX-Request, HX-Boosted, HX-Target을 구분하고 Vary 헤더를 설정했습니다. 필터를 빠르게 연속 선택할 때는 앞선 요청을 교체하고, 로딩 표시가 짧게 깜빡이지 않도록 지연과 최소 노출 시간을 조정했습니다. details의 접힘 애니메이션과 View Transition은 Safari의 구현 차이도 확인하며, 의미 있는 마크업과 안정적인 화면 전환을 모두 유지하는 방향으로 보완했습니다.
배포는 GitHub Actions와 Argo CD의 역할을 분리한 GitOps 흐름으로 구성했습니다. Pull Request에서는 Linux 환경의 테스트와 이미지 빌드를 검증하고, main 브랜치에 반영되면 통과한 런타임 이미지를 커밋 SHA 태그로 GHCR에 게시합니다. 이후 Actions는 별도의 GitOps 저장소에 있는 Deployment의 이미지 참조를 갱신하고, Argo CD가 해당 변경을 Kubernetes 클러스터에 자동으로 동기화합니다. CI가 클러스터를 직접 변경하지 않으면서 애플리케이션 커밋과 이미지, 배포 선언의 관계를 Git 이력으로 추적할 수 있게 했습니다.
서버가 HTML을 완성해 보내는 구조는 클라이언트 상태 관리가 단순하고 기본 기능이 JavaScript에 의존하지 않는다는 장점이 있습니다. 대신 작은 상호작용도 서버 응답과 템플릿 경계를 함께 설계해야 하고, 한 애플리케이션에서 여러 도메인을 다루기 때문에 요청 호스트와 링크 생성 규칙을 테스트로 지속해서 확인해야 합니다.
결과
개인 웹 허브, 개발 포트폴리오와 기술 블로그를 하나의 Vapor 애플리케이션으로 통합했습니다. 프로젝트는 기술별로 탐색할 수 있고, 각 상세 페이지에서 문제와 담당 범위, 선택과 결과를 독립된 문서로 읽을 수 있습니다. 기존 블로그 게시글과 이미지도 새 디자인 언어로 옮겨 목록 페이지네이션과 검색, RSS를 포함한 읽기 흐름을 다시 구성했습니다.
라우팅과 리다이렉트, 일반 요청과 htmx 요청의 응답 차이, Markdown 검증과 검색·정렬 규칙은 자동화된 테스트로 확인하고 있습니다. 현재는 공개 배포를 준비하는 단계이므로 방문자 성능 지표와 운영 안정성에 관한 결과는 배포 후 실제 측정값으로 추가할 예정입니다.
회고
처음에는 Swift, Vapor, Leaf와 htmx를 사용해 보고 싶다는 기술적 호기심에서 시작했지만, 프레임워크를 선택하는 것만으로 서버 주도형 웹이 완성되지는 않았습니다. URL에 상태를 남기는 방법, 전체 문서와 조각 응답의 경계, JavaScript가 없을 때의 동작, 콘텐츠 검증과 배포까지 함께 설계해야 비로소 하나의 제품이 되었습니다.
이번 작업을 통해 서버 렌더링과 점진적 향상은 과거 방식으로 돌아가는 선택이 아니라, 필요한 복잡성만 클라이언트에 두기 위한 적극적인 설계가 될 수 있음을 확인하고 있습니다. 공개 이후에는 실제 탐색 성능과 운영 경험을 바탕으로 현재의 선택을 다시 검토하고 기록할 계획입니다.
화면과 흐름
URL에 상태를 남기는 프로젝트 필터
하나로 연결한 개인 웹과 기술 블로그