사이트 제작기 4부 — 디자인 커스터마이징과 Git 기반 관리자 패널

사이트 제작기 시리즈 · 1부 — 왜 정적 홈페이지인가 / 2부 — 콘텐츠 3구역 / 3부 — 자동화 파이프라인 / 4부 (이 글, 완결) — 커스터마이징과 관리자 패널


1부에서 “Hugoplate 테마를 바닥부터 만들지 않고 가져와 필요한 부분만 고쳐 썼다"고 했습니다. 이번 완결편은 바로 그 길들이기(커스터마이징) 이야기입니다. 남이 만든 옷을 사서 내 몸에 맞게 수선한 셈인데, 그 수선 자국을 하나씩 보여드리겠습니다.


1. 목록을 3열 카드 + 사이드바로

테마 기본 목록은 단순한 한 줄 나열이었습니다. 이걸 왼쪽 3열 카드 + 오른쪽 사이드바 구성으로 바꿨습니다(layouts/blog/list.html). Hugoplate는 12칸 그리드를 쓰는데, 본문에 9칸, 사이드바에 3칸을 줬습니다.

<div class="row gx-5">
  <!-- 글 목록: 9칸 -->
  <div class="lg:col-9">
    <div class="row post-list-row">
      {{ $paginator := .Paginate $pages 9 }}
      {{ range $paginator.Pages }}
        <div class="md:col-6 lg:col-4 mb-14">   <!-- 한 카드가 4칸 → 한 줄에 3개 -->
          {{ partial "components/blog-card" . }}
        </div>
      {{ end }}
    </div>
  </div>
  <!-- 사이드바(카테고리·태그): 3칸 -->
  <div class="lg:col-3">
    {{ partialCached "widgets/widget-wrapper" (dict "Widgets" site.Params.widgets.sidebar "Scope" .) }}
  </div>
</div>

카드 자체도 손봤습니다. 작성자·카테고리 같은 잡다한 메타 정보는 숨기고, 요약문만 200자에서 잘라 깔끔하게 보이도록 했습니다.

<p class="mb-6">{{ .Summary | plainify | truncate 200 "..." }}</p>

2. 정적 사이트의 딜레마 — “화면 크기를 모른다”

여기서 흥미로운 문제가 있습니다. 정적 사이트는 방문자의 화면 크기를 미리 알 수 없습니다. 빌드 시점에 이미 HTML이 굳어버리기 때문입니다. 그런데 우리는 데스크톱(3열)에서는 9개, 태블릿(2열)에서는 8개, 모바일(1열)에서는 6개만 딱 떨어지게 보여주고 싶었습니다.

해법은 우아합니다. 일단 9개를 다 만들어 두고, 화면 크기에 안 맞는 카드만 CSS로 숨기는 것입니다(assets/css/custom.css).

/* 모바일(<768px): 7번째 카드부터 숨김 → 6개만 노출 */
@media (max-width: 767.98px) {
  .post-list-row > div:nth-child(n+7) { display: none; }
}

/* 태블릿(768~1023px): 9번째 카드 숨김 → 8개만 노출 */
@media (min-width: 768px) and (max-width: 1023.98px) {
  .post-list-row > div:nth-child(n+9) { display: none; }
}

“서버가 판단할 수 없으면, 다 만들어 놓고 브라우저(CSS)에게 판단을 넘긴다.” 정적 사이트에서 자주 쓰는 발상의 전환입니다.


3. 다크 모드에서 로고가 사라지는 문제

이 사이트의 로고는 검은색 글자입니다. 그런데 다크 모드나 어두운 푸터 위에서는 검은 글자가 배경에 묻혀 안 보였습니다. 이미지를 두 벌 만드는 대신, 브라우저에서 색을 반전시키는 CSS 한 줄로 해결했습니다.

/* 다크 모드일 때 헤더 로고 색 반전 */
.dark .header .navbar-brand img {
  filter: invert(1) hue-rotate(180deg);
}
/* 항상 어두운 푸터의 로고는 상시 반전 + 70% 축소 */
.footer-logo img {
  width: 105px !important;
  filter: invert(1) hue-rotate(180deg);
}

⚠️ 여기엔 함정이 있었습니다. 다크 모드에서 푸터 로고에 헤더용 반전과 푸터용 반전이 동시에 걸리면, 반전이 두 번 되어 도로 검은색이 됩니다. 그래서 푸터 로고에서는 헤더 반전 규칙에 걸리는 클래스를 빼서 이중 반전을 막았습니다.

이 밖에도 카드·인용구·태그 배지의 다크 모드 배경색을 손봤습니다. 테마 원본이 “연한 회색 글자색"을 실수로 “카드 배경색"으로도 써버려서, 다크 모드에서 글자색과 배경색이 똑같아져 안 보이던 버그가 있었거든요. 배경을 한 단계 밝은 회색(dark:bg-[#444444])으로 분리하고 글자색을 명시해 대비를 확보했습니다.


4. 코드를 안 고치고 정렬을 바꾸기

카테고리마다 원하는 정렬이 다릅니다. IT뉴스는 최신순이 좋지만, 강의 시리즈는 “1부, 2부, 3부” 순서(제목순)여야 합니다. 이걸 템플릿에 하드코딩하지 않고 설정 파일로 제어하도록 만들었습니다(config/_default/params.toml).

[sorting]
  [sorting.sections]
  "/blog/second-brain/" = "title"   # 제목(가나다)순
  "/blog/ssg/"          = "title"
  # 지정 안 하면 기본값은 날짜 최신순

템플릿은 이 설정을 읽어 분기합니다.

{{ $sortOrder := index site.Params.sorting.sections .RelPermalink | default "date" }}
{{ if eq $sortOrder "title" }}
  {{ $pages = $pages.ByTitle }}
{{ else }}
  {{ $pages = $pages.ByDate.Reverse }}
{{ end }}

덕분에 정렬 규칙을 바꿀 때 코드가 아니라 설정 한 줄만 고치면 됩니다. (실은 이 설정마저도 다음 항목의 관리자 패널에서 브라우저로 바꿀 수 있습니다.)


5. 코딩 없이 관리하는 Git 기반 관리자 패널

이 사이트에서 가장 야심 찬 기능입니다. jesusiswith.us/admin/ 주소로 들어가면, 개발 환경 없이 웹 브라우저만으로 카드 표시 옵션·폰트 크기·메뉴별 정렬 기준 같은 설정을 바꾸고 즉시 반영할 수 있습니다.

서버 없이 어떻게?

정적 사이트에는 백엔드 서버가 없다고 1부에서 강조했는데, 그럼 이 패널은 어떻게 설정을 저장할까요? 비밀은 GitHub REST API에 브라우저가 직접 말을 거는 것입니다.

관리자가 브라우저에서 설정 변경 → 저장 클릭
        ↓
브라우저 JS가 GitHub API를 직접 호출해 params.toml 수정(커밋)
        ↓
Cloudflare가 커밋 감지 → 자동 재빌드·배포
        ↓
잠시 뒤 사이트에 반영
  • 인증: GitHub 개인 접근 토큰(PAT)을 쓰되, 이 토큰은 서버가 아니라 관리자 브라우저의 localStorage에만 저장됩니다.
  • 배포 상태 표시: 저장 후 GitHub Actions 상태를 주기적으로 확인해 “배포 중 / 성공 / 실패"를 화면에 보여줍니다.

로컬 작업과 충돌하지 않으려면

관리자 패널은 원격 저장소를 직접 수정하기 때문에, 로컬 컴퓨터의 파일보다 원격이 앞서게 됩니다. 이 상태에서 로컬 변경을 그냥 올리면 거절(rejected)당합니다. 그래서 3부의 auto_push.sh도, 사람이 손으로 올릴 때도 항상 먼저 당겨받는 습관이 중요합니다.

git pull --rebase && git push

원칙 하나: 메뉴 수정은 관리자 패널이든 로컬 파일이든 한쪽에서만 하세요. 양쪽에서 동시에 건드리면 병합 충돌이 납니다.


6. 제작 중 실제로 부딪힌 문제들 (트러블슈팅 하이라이트)

교육을 겸한 글인 만큼, “이런 것도 문제가 되는구나"를 남기는 게 가장 값진 부분입니다. 실제 제작 기록(docs/issue_report.md, JIWU_Mission_Troubleshooting_Report.md)에서 대표적인 것만 골랐습니다.

path 필드 때문에 빌드가 통째로 실패

노션에서 내보낸 파일에는 path: 필드가 딸려 오는데, 최신 Hugo(v0.144+)는 이 필드를 만나면 빌드를 아예 중단합니다.

# content 폴더 전체에서 path: 줄을 일괄 삭제
find content/ -name "*.md" ! -name "_index.md" -exec sed -i '' '/^path:/d' {} \;

노션 동기화 때마다 다시 붙을 수 있으므로, 올리기 전 npm run build로 빌드를 미리 검증하는 습관이 안전합니다.

② 확장자는 .png인데 실제론 WebP — “unknown format”

파일 이름은 .png인데 내용은 사실 WebP인 경우, Hugo의 이미지 리사이징이 image: unknown format 오류로 멈춥니다. file 명령으로 진짜 포맷을 확인하고 확장자를 실제와 일치시켜 해결했습니다. 이후로는 아예 모든 이미지를 WebP로 통일해, 용량도 줄이고(약 26%↓) 이 오류도 원천 차단했습니다.

③ 맥에서만 정렬이 깨지던 한글 유니코드 문제

macOS는 한글을 자모 분리 방식(NFD)으로 저장하는데, 코드 속 한글 문자열은 결합형(NFC)이라 “세컨드브레인” == “세컨드브레인” 비교가 실패하는 황당한 버그가 있었습니다. 해법은 한글 문자열을 비교하는 대신 영문으로 된 폴더 경로를 비교하도록 바꾼 것입니다.

{{ if in .RelPermalink "/blog/second-brain/" }}   {{/* 한글 대신 ASCII 경로로 판별 */}}
  {{ $isSeries = true }}
{{ end }}

④ 로컬 미리보기에서 화면이 깨지던 문제

로컬 개발 서버를 돌리면 hugo.toml의 운영 도메인 주소를 물어와서, CSS를 엉뚱한 곳(운영 서버)에서 찾다가 화면이 깨졌습니다. 개발 환경 전용 설정 파일을 따로 두어 격리했습니다.

# config/development/hugo.toml
baseURL = "http://localhost:1313/"

이러면 개발 중에는 이 값이 우선 적용되고, 실제 배포 빌드에서는 무시됩니다. Cloudflare 배포에서도 비슷하게 --baseURL $CF_PAGES_URL로 배포 주소를 동적으로 넣어 미리보기 URL에서도 디자인이 안 깨지게 했습니다.


7. 시리즈를 마치며

네 편에 걸쳐 이 사이트의 안팎을 함께 열어 봤습니다. 전체를 한 문단으로 되짚으면:

jesusiswith.us는 서버 없이 돌아가는 정적 홈페이지입니다. 글은 Markdown으로 쓰고 Hugo가 HTML로 변환하며, GitHub에 올리면 Cloudflare가 전 세계로 배포합니다(1부). 콘텐츠는 Blog·Class·Databank 세 구역에 나뉘어, 노션·수동·AI 세 경로로 채워집니다(2부). 밤사이엔 AI가 IT뉴스를 쓰고 스케줄러가 자동으로 올리며, 접속하면 그날의 QT가 팝업으로 인사합니다(3부). 그리고 가져온 테마를 반응형·다크모드·정렬·관리자 패널로 길들여, 코딩을 몰라도 운영할 수 있게 만들었습니다(4부).

코딩 경험이 없어도, 월 0원에 가까운 비용으로, AI의 도움을 받아 이만큼 만들 수 있다는 것이 이 프로젝트가 전하고 싶은 메시지입니다. 이 시리즈가 여러분만의 사이트를 시작하는 작은 계기가 되기를 바랍니다.

📚 더 깊이 들어가려면 — 같은 섹션의 “교회 홈페이지 무료 제작 매뉴얼(Chapter 1~5)” 시리즈가 처음부터 따라 만드는 실습 가이드를 제공합니다. 이 제작기가 “무엇을·왜"라면, 그 매뉴얼은 “어떻게"에 해당합니다.