
페이지가 늘어나면 손이 못 따라간다Manual Editing Stops Scaling Once Pages Multiply
이 사이트는 처음에 HTML 파일 몇 개로 시작했다. 메인 하나, 소개 하나, 게임 목록 하나. 이 정도면 손으로 고치는 게 가장 빠르다. 빌드 도구를 붙이는 시간이 아깝다.
This site started as a handful of HTML files: one home page, one about page, and one game list. At that scale, editing HTML by hand is the fastest way to work, and setting up a build tool is a waste of time.
문제는 페이지가 늘어나면서 시작됐다. 게임이 다섯 개가 되고 각각 상세 페이지가 생기자, 네비게이션 링크 하나를 바꾸는 데 파일을 열 개 넘게 열어야 했다. 그러다 한두 개를 빼먹고, 그 페이지만 예전 메뉴가 남아 있는 상태가 된다.
The trouble started as pages multiplied. Once there were five games, each with its own detail page, changing a single navigation link meant opening more than ten files. Inevitably I would miss one or two, leaving outdated menus on those pages.
더 나쁜 건 디자인 변경이었다. 버튼 스타일을 바꾸려면 모든 파일에서 같은 클래스를 찾아 고쳐야 했고, 결국 페이지마다 미묘하게 다른 버튼이 생겼다.
Design updates were worse. Changing a button style meant hunting down the same utility classes across every file, which eventually left each page with slightly different buttons.
전환: JSON 하나가 페이지가 된다The Switch: One JSON File Becomes One Page
그래서 구조를 바꿨다. 콘텐츠는 JSON에 두고, HTML은 스크립트가 만들게 했다.
So I changed the structure: keep content in JSON, and let Python scripts generate the 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
_content/posts/<date>-<slug>.json ──► build_posts.py ──► blog/posts/*.html
└► blog/posts.json
_content/games/NN-<slug>.json ──► build_games.py ──► <slug>/index.html
├► Home / list card injection
└► css/brutal-cards.css
build_sitemap.py ──► sitemap.xml
글을 쓸 때는 JSON에 블록을 나열한다. 제목, 문단, 코드, 표, 강조 박스. 목차와 읽는 시간, 이전/다음 글 링크, meta description, Open Graph, 구조화 데이터는 전부 스크립트가 붙인다.
When writing a post, I list content blocks in JSON: headings, paragraphs, code blocks, tables, and callouts. The scripts automatically generate the table of contents, reading time, previous/next links, meta description, Open Graph tags, and structured data.
{
"slug": "example-post",
"date": "2026-07-25",
"category": "개발 일지",
"title": "제목",
"summary": "목록과 검색, 메타태그에 쓰이는 요약",
"body": [
{ "type": "h2", "text": "소제목" },
{ "type": "p", "text": "문단이다. **굵게** 와 `코드` 를 쓸 수 있다." },
{ "type": "code", "lang": "csharp", "code": "// 코드 블록" }
]
}
{
"slug": "example-post",
"date": "2026-07-25",
"category": "Devlog",
"title": "Post Title",
"summary": "Summary used for cards, search index, and meta tags",
"body": [
{ "type": "h2", "text": "Section Heading" },
{ "type": "p", "text": "A paragraph supporting **bold** and `inline code`." },
{ "type": "code", "lang": "csharp", "code": "// Code block" }
]
}
ScriptableObject 로 빼는 것과 정확히 같은 판단이다.ScriptableObject in game development.카드 전체를 클릭 가능하게 만들기Making an Entire Card Clickable
구현 중에 재미있는 문제가 하나 있었다. 게임 카드를 아무 데나 눌러도 상세 페이지로 가게 만들고 싶은데, 카드 안에는 이미 "데모 플레이" 같은 별도 링크가 있다.
During implementation, I ran into an interesting markup problem. I wanted clicking anywhere on a game card to open its detail page, but the card already contained separate links like "Play Demo" inside it.
<a> 안에 <a> 를 넣는 건 HTML 규격 위반이라 브라우저가 태그를 강제로 끊어 버린다. 흔히 쓰는 해법은 stretched link 패턴이다. 투명한 링크의 가상 요소로 카드 전체를 덮고, 그 위에 올릴 버튼만 z-index 를 높인다.
Nesting an <a> inside another <a> violates the HTML specification, and browsers forcibly break the tags apart. The standard solution is the stretched link pattern: cover the entire card with a pseudo-element of the main link, and raise the z-index of the inner action buttons above it.
/* 카드는 기준점이 되어야 하므로 position: relative */
.game-card { position: relative; }
/* 이 링크의 가상 요소가 카드 전체를 덮는다 */
.stretched-link::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
}
/* 오버레이보다 위에 올라와야 하는 실제 버튼 */
.card-actions { position: relative; z-index: 10; }
/* The card acts as the positioning anchor */
.game-card { position: relative; }
/* This link's pseudo-element covers the entire card */
.stretched-link::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
}
/* Action buttons that must sit above the overlay */
.card-actions { position: relative; z-index: 10; }
<style> 안에만 넣어 두고 목록 페이지에는 빠뜨렸다. 그래서 목록에서 카드를 눌러도 아무 반응이 없었다. 에러가 안 나는 종류의 버그라 브라우저로 직접 클릭해 보기 전까지 몰랐다. 공용 CSS 파일로 빼서 해결했다.<style> of the detail page and forgot to include it on the list page. Clicking cards on the list page did nothing. Because missing CSS throws no console errors, I didn't notice until I clicked the card in a browser myself. Extracting it into a shared CSS file fixed the issue.호스팅이 하는 일을 알고 있어야 한다Know What Your Hosting Provider Actually Does
`_` 로 시작하는 폴더가 저절로 빠지지는 않는다Folders Starting with `_` Are Not Excluded Automatically
빌드 스크립트와 원본 데이터를 사이트와 같은 레포에 두면 관리가 편하다. 이 사이트도 그렇게 두고 있다.
Keeping build scripts and source JSON in the same repository as the static output makes maintenance simple. That is how this repository is organized:
_content/— 글과 게임의 원본 JSON_tools/— 빌드 스크립트_docs/— 문서_trash/— 지울 예정인 것들
_content/— Source JSON for posts and games_tools/— Build scripts_docs/— Project documentation_trash/— Files staged for deletion
그런데 _ 로 시작하면 배포에서 빠진다는 건 특정 호스팅의 동작이지 일반 규칙이 아니다. Jekyll 을 거치는 호스팅에서는 그렇고, 정적 파일을 그대로 올리는 호스팅에서는 전부 그대로 공개된다. 이 사이트는 후자인데 전자라고 믿고 있었다.
However, excluding _-prefixed folders from deployment is a Jekyll convention, not a universal rule. Hosts that run Jekyll strip them out; hosts that serve static files as-is publish everything in the directory. My site runs on the latter, while I had assumed the former.
robots.txt 에 Disallow 를 적어 뒀다고 안심하면 안 된다. 그건 크롤러에게 하는 부탁이지 접근 차단이 아니다. 브라우저에 직접 주소를 쳐 보는 것이 유일하게 확실한 확인 방법이다. 나는 /README.md 를 열어 보고 나서야 알았다.Disallow to robots.txt is not access control—it is a polite request to crawlers. Typing the URL directly into the browser address bar is the only reliable check. I only realized my source files were public after opening /README.md in the browser.서버 리다이렉트가 안 된다Server-Side Redirects Don't Work Out of the Box
정적 호스팅이라 301 리다이렉트를 직접 설정할 수 없다. 글 주소를 바꾸면 옛 주소는 그냥 404 가 된다. 앞단에 Cloudflare 를 쓰고 있다면 Redirect Rules 로 처리하는 게 가장 간단하다. 대시보드에서 규칙을 몇 개 넣으면 끝이고 추가 비용도 없다.
On basic static hosting, you cannot configure server-level 301 redirects directly in code. If you change a post URL, the old URL simply breaks. Since I use Cloudflare in front of the site, Redirect Rules in the Cloudflare dashboard solved this cleanly at no extra cost.
sitemap 은 자동으로 갱신되지 않는다`sitemap.xml` Does Not Update Itself
이번에 정리하면서 가장 크게 놀란 부분이다. sitemap.xml 에 등록된 글 주소 22개가 전부 존재하지 않는 파일을 가리키고 있었다. 예전에 글을 교체하면서 사이트맵을 갱신하지 않았기 때문이다.
This was the biggest surprise during the cleanup. All 22 blog URLs listed in sitemap.xml pointed to files that no longer existed. I had replaced old posts earlier without updating the sitemap.
검색 엔진 입장에서는 "사이트맵에 적힌 페이지가 대부분 없는 사이트"로 보인다. 그래서 사이트맵도 손으로 관리하지 않고, 실제 존재하는 파일만 훑어서 생성하도록 스크립트에 넣었다.
To a search engine, the site looked like a domain where most listed pages were dead. I stopped maintaining the sitemap by hand and wrote a script that scans the disk and includes only files that actually exist.
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
def build_post_entries() -> list[str]:
"""Include only posts whose generated HTML file exists on disk."""
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] Missing file, excluded from sitemap: {post['filename']}")
continue
entries.append(build_entry(post["url"], POST_PRIORITY, POST_CHANGEFREQ,
to_iso_date(post["date"])))
return entries
확인은 브라우저로Always Test Over HTTP, Not `file://`
정적 사이트라도 HTML 파일을 더블클릭해서 확인하면 안 된다. file:// 로 열면 /image/... 같은 절대 경로가 전부 깨지고, fetch 도 막힌다. 글 목록이 통째로 비어 보이는데 코드에는 문제가 없는 상황이 생긴다.
Even for a static site, double-clicking an HTML file to open it via file:// breaks root-relative paths like /image/... and blocks fetch requests due to CORS. The post list appears completely empty even when the code is fine.
그래서 더블클릭 한 번으로 로컬 서버가 뜨도록 배치 파일을 만들어 뒀다.
To avoid that, I added a batch script that launches a local HTTP server with a double-click.
.bat 파일이 실행되자마자 모든 줄이 깨진 명령으로 읽혔다. cmd 가 배치 파일을 현재 코드 페이지 기준으로 읽기 때문이다. 맨 위에 chcp 65001 을 넣어도 소용없다. 파일을 읽는 시점이 그보다 먼저다. 안내 문구까지 전부 영문으로 바꿔서 해결했다..bat file containing Korean comments failed immediately with garbled commands on every line. Windows cmd.exe reads batch files using the active system code page. Putting chcp 65001 at the top does not help because cmd parses the file before executing that line. Rewriting all comments and prompts in plain ASCII fixed it.정리Takeaways
- 페이지가 늘어나는 속도보다 손으로 고치는 속도가 느려지는 지점이 반드시 온다. 그때가 자동화할 때다.
- 정적 사이트라도 빌드 스크립트 하나만 있으면 관리 비용이 크게 줄어든다. 프레임워크까지는 필요 없었다.
sitemap.xml처럼 눈에 안 보이는 파일이 가장 오래 방치되고 가장 조용히 문제를 만든다. 생성 대상에 넣어 두는 게 안전하다.- 에러를 내지 않는 버그(스타일 누락, 인코딩)는 코드를 아무리 봐도 안 보인다. 실제로 눌러 보는 것이 유일한 검증이다.
- There is always a point where manual editing falls behind the number of pages. That is the point to automate.
- Even without a frontend framework, a single Python build script cuts static site maintenance overhead dramatically.
- Invisible files like
sitemap.xmlget neglected the longest and cause problems the most quietly. Make them build artifacts, not hand-edited files. - Silent bugs (missing CSS, file encoding) never show up by reading code alone. Clicking through the rendered page is the only real verification.
게임을 만들면서 배운 것들이 웹에서도 거의 그대로 통했다. 자주 바뀌는 값은 코드 밖으로, 같은 코드는 한 곳으로, 그리고 결국 직접 실행해서 확인하기. 도메인만 다를 뿐 같은 이야기였다.
Lessons from game development carried over directly to web development: move frequently changing data out of code, keep shared logic in one place, and always verify by running the actual build. Only the domain changed.