
페이지가 늘어나면 손이 못 따라간다
이 사이트는 처음에 HTML 파일 몇 개로 시작했다. 메인 하나, 소개 하나, 게임 목록 하나. 이 정도면 손으로 고치는 게 가장 빠르다. 빌드 도구를 붙이는 시간이 아깝다.
문제는 페이지가 늘어나면서 시작됐다. 게임이 다섯 개가 되고 각각 상세 페이지가 생기자, 네비게이션 링크 하나를 바꾸는 데 파일을 열 개 넘게 열어야 했다. 그러다 한두 개를 빼먹고, 그 페이지만 예전 메뉴가 남아 있는 상태가 된다.
더 나쁜 건 디자인 변경이었다. 버튼 스타일을 바꾸려면 모든 파일에서 같은 클래스를 찾아 고쳐야 했고, 결국 페이지마다 미묘하게 다른 버튼이 생겼다.
전환: JSON 하나가 페이지가 된다
그래서 구조를 바꿨다. 콘텐츠는 JSON에 두고, HTML은 스크립트가 만들게 했다.
_content/posts/<날짜>-<슬러그>.json ──► build_posts.py ──► blog/posts/*.html
└► blog/posts.json
_content/games/NN-<슬러그>.json ──► build_games.py ──► <슬러그>/index.html
├► 메인/목록 카드 주입
└► css/brutal-cards.css
build_sitemap.py ──► sitemap.xml
글을 쓸 때는 JSON에 블록을 나열한다. 제목, 문단, 코드, 표, 강조 박스. 목차와 읽는 시간, 이전/다음 글 링크, meta description, Open Graph, 구조화 데이터는 전부 스크립트가 붙인다.
{
"slug": "example-post",
"date": "2026-07-25",
"category": "개발 일지",
"title": "제목",
"summary": "목록과 검색, 메타태그에 쓰이는 요약",
"body": [
{ "type": "h2", "text": "소제목" },
{ "type": "p", "text": "문단이다. **굵게** 와 `코드` 를 쓸 수 있다." },
{ "type": "code", "lang": "csharp", "code": "// 코드 블록" }
]
}
ScriptableObject 로 빼는 것과 정확히 같은 판단이다.카드 전체를 클릭 가능하게 만들기
구현 중에 재미있는 문제가 하나 있었다. 게임 카드를 아무 데나 눌러도 상세 페이지로 가게 만들고 싶은데, 카드 안에는 이미 "데모 플레이" 같은 별도 링크가 있다.
<a> 안에 <a> 를 넣는 건 HTML 규격 위반이라 브라우저가 태그를 강제로 끊어 버린다. 흔히 쓰는 해법은 stretched link 패턴이다. 투명한 링크의 가상 요소로 카드 전체를 덮고, 그 위에 올릴 버튼만 z-index 를 높인다.
/* 카드는 기준점이 되어야 하므로 position: relative */
.game-card { position: relative; }
/* 이 링크의 가상 요소가 카드 전체를 덮는다 */
.stretched-link::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
}
/* 오버레이보다 위에 올라와야 하는 실제 버튼 */
.card-actions { position: relative; z-index: 10; }
<style> 안에만 넣어 두고 목록 페이지에는 빠뜨렸다. 그래서 목록에서 카드를 눌러도 아무 반응이 없었다. 에러가 안 나는 종류의 버그라 브라우저로 직접 클릭해 보기 전까지 몰랐다. 공용 CSS 파일로 빼서 해결했다.호스팅이 하는 일을 알고 있어야 한다
`_` 로 시작하는 폴더가 저절로 빠지지는 않는다
빌드 스크립트와 원본 데이터를 사이트와 같은 레포에 두면 관리가 편하다. 이 사이트도 그렇게 두고 있다.
_content/— 글과 게임의 원본 JSON_tools/— 빌드 스크립트_docs/— 문서_trash/— 지울 예정인 것들
그런데 _ 로 시작하면 배포에서 빠진다는 건 특정 호스팅의 동작이지 일반 규칙이 아니다. Jekyll 을 거치는 호스팅에서는 그렇고, 정적 파일을 그대로 올리는 호스팅에서는 전부 그대로 공개된다. 이 사이트는 후자인데 전자라고 믿고 있었다.
robots.txt 에 Disallow 를 적어 뒀다고 안심하면 안 된다. 그건 크롤러에게 하는 부탁이지 접근 차단이 아니다. 브라우저에 직접 주소를 쳐 보는 것이 유일하게 확실한 확인 방법이다. 나는 /README.md 를 열어 보고 나서야 알았다.서버 리다이렉트가 안 된다
정적 호스팅이라 301 리다이렉트를 직접 설정할 수 없다. 글 주소를 바꾸면 옛 주소는 그냥 404 가 된다. 앞단에 Cloudflare 를 쓰고 있다면 Redirect Rules 로 처리하는 게 가장 간단하다. 대시보드에서 규칙을 몇 개 넣으면 끝이고 추가 비용도 없다.
sitemap 은 자동으로 갱신되지 않는다
이번에 정리하면서 가장 크게 놀란 부분이다. sitemap.xml 에 등록된 글 주소 22개가 전부 존재하지 않는 파일을 가리키고 있었다. 예전에 글을 교체하면서 사이트맵을 갱신하지 않았기 때문이다.
검색 엔진 입장에서는 "사이트맵에 적힌 페이지가 대부분 없는 사이트"로 보인다. 그래서 사이트맵도 손으로 관리하지 않고, 실제 존재하는 파일만 훑어서 생성하도록 스크립트에 넣었다.
def build_post_entries() -> list[str]:
"""실제 파일이 존재하는 글만 사이트맵에 넣는다."""
posts = json.loads(POSTS_JSON.read_text(encoding="utf-8"))
entries = []
for post in posts:
file_path = SITE_ROOT / "blog" / "posts" / post["filename"]
if not file_path.exists():
print(f"[SKIP] 파일 없음, 사이트맵 제외: {post['filename']}")
continue
entries.append(build_entry(post["url"], POST_PRIORITY, POST_CHANGEFREQ,
to_iso_date(post["date"])))
return entries
확인은 브라우저로
정적 사이트라도 HTML 파일을 더블클릭해서 확인하면 안 된다. file:// 로 열면 /image/... 같은 절대 경로가 전부 깨지고, fetch 도 막힌다. 글 목록이 통째로 비어 보이는데 코드에는 문제가 없는 상황이 생긴다.
그래서 더블클릭 한 번으로 로컬 서버가 뜨도록 배치 파일을 만들어 뒀다.
.bat 파일이 실행되자마자 모든 줄이 깨진 명령으로 읽혔다. cmd 가 배치 파일을 현재 코드 페이지 기준으로 읽기 때문이다. 맨 위에 chcp 65001 을 넣어도 소용없다. 파일을 읽는 시점이 그보다 먼저다. 안내 문구까지 전부 영문으로 바꿔서 해결했다.정리
- 페이지가 늘어나는 속도보다 손으로 고치는 속도가 느려지는 지점이 반드시 온다. 그때가 자동화할 때다.
- 정적 사이트라도 빌드 스크립트 하나만 있으면 관리 비용이 크게 줄어든다. 프레임워크까지는 필요 없었다.
sitemap.xml처럼 눈에 안 보이는 파일이 가장 오래 방치되고 가장 조용히 문제를 만든다. 생성 대상에 넣어 두는 게 안전하다.- 에러를 내지 않는 버그(스타일 누락, 인코딩)는 코드를 아무리 봐도 안 보인다. 실제로 눌러 보는 것이 유일한 검증이다.
게임을 만들면서 배운 것들이 웹에서도 거의 그대로 통했다. 자주 바뀌는 값은 코드 밖으로, 같은 코드는 한 곳으로, 그리고 결국 직접 실행해서 확인하기. 도메인만 다를 뿐 같은 이야기였다.