[Project] - 데이터 파일 몇 개로 정적 페이지 2,400장 만들기 (오늘꿈해몽)

토스 미니앱으로 만든 오늘꿈해몽 의 데이터를 그대로 써서, 검색용 정적 페이지 2,400장을 스크립트 하나로 만들고 GitHub Pages 하위 경로에 올린 과정을 정리합니다.

왜 정적 페이지를 따로 만들었나

  • 앱에는 꿈 2,001개, 상황별 풀이 9,327개가 JSON 으로 들어있음
    • 앱은 이걸 JS 로 그려서 보여주는데, 크롤러 대부분은 JS 를 안 돌림
    • “뱀 꿈” 을 검색한 사람이 들어올 문이 없는 상태
  • 그래서 같은 JSON 을 읽어서 꿈 하나당 HTML 한 장을 뽑는 build-web.mjs 를 만듦
    • 프레임워크 없이 Node 스크립트 하나, 템플릿 문자열로 HTML 을 찍어냄
    • 결과물 web-dist/ 를 공개 레포에 그대로 올리면 GitHub Pages 가 서빙

데이터 없이 규칙으로 만드는 페이지

  • 꿈 데이터만으로는 검색어 종류가 한정적이라, 사람들이 매년 찾는 걸 계산으로 붙임
  • 나이·띠 — 1940년생부터 올해생까지 년생마다 한 장
    • 띠는 ((year - 4) % 12 + 12) % 12, 간지는 천간 10개를 같은 방식으로 돌림
    • 올해 연도를 빌드 시점에서 읽으니, 해가 바뀌고 다시 빌드하면 나이가 다 같이 올라감
  • 삼재 — 띠를 네 묶음으로 나누면 묶음마다 들어오는 해가 정해져 있음
    • 쥐·용·원숭이는 호랑이해부터, 소·뱀·닭은 돼지해부터 … 이런 식이라 시작 지점이 (2 - 3g) mod 12 로 떨어짐
    • 들삼재·눌삼재·날삼재는 시작 지점에서 0·1·2년 차이

      1
      2
      3
      const sjStart = (g) => (((2 - 3 * g) % 12) + 12) % 12;
      // g = 띠 index % 4
      // 출력 결과: g=0 → 2(호랑이해), g=1 → 11(돼지해), g=2 → 8(원숭이해), g=3 → 5(뱀해)
      
  • 손없는날 — 음력 날짜 끝자리가 9·0 인 날
    • 음력 변환은 직접 짜지 않고 korean-lunar-calendar(MIT) 를 씀
    • 2027년 2월 6일이 음력 12월 30일(설 전날)로 나오는 걸 보고 맞는지 확인함
  • 공휴일 — 대체공휴일 규칙이 생각보다 갈래가 많음
    • 삼일절·광복절 같은 국경일은 토·일과 겹치면 다음 평일
    • 설·추석 연휴는 토요일은 안 치고 일요일이나 다른 공휴일과 겹칠 때만
    • 어린이날은 토·일·다른 공휴일 모두
    • 2026년부터 노동절·제헌절이 공휴일이 돼서 규칙에 추가함
    • 규칙으로 뽑은 2027년 대체공휴일 7일이 이미 공개된 2027년 달력과 날짜가 전부 같았음

‘꿈 꿈’ 버그

  • 데이터에 “돼지꿈” 처럼 이름 자체가 “꿈” 으로 끝나는 항목이 있었음
    • 템플릿이 ${name} 꿈 해몽 이라서 “돼지꿈 꿈 해몽” 이 631장에 찍혀 있었음
    • 제목에서 이게 반복되니 검색 결과에서 그대로 노출될 뻔 했음
  • 이름 끝을 보고 붙일지 말지 정하는 헬퍼 하나로 해결

    1
    2
    3
      const kkum = (name) => (name.endsWith('꿈') ? name : `${name} 꿈`);
      console.log(kkum('돼지꿈'), kkum('뱀'));
      // 출력 결과: 돼지꿈 뱀 꿈
    
    • 홈의 검색 JS 에도 같은 조합 로직이 있어서 거기도 같이 고침

sitemap lastmod 를 빌드 날짜로 찍으면 안 됨

  • 처음엔 빌드할 때마다 전 페이지 lastmod 를 오늘로 넣고 있었음
  • 지금은 페이지가 기대는 데이터 파일의 마지막 git 커밋 날짜를 씀

    1
    2
      const gitDate = (f) => execSync(`git log -1 --format=%cs -- ${f}`).toString().trim();
      // 꿈 페이지 → data/dreams, 연도 운세 → data/fortune/year-2027.json ...
    
    • 날짜가 박힌 페이지(그날의 띠별 운세 등)는 그 날짜를 그대로 씀
  • IndexNow 도 같은 기준으로 바꿈
    • 매일 배포하면서 2,400개를 매번 보내면 스팸에 가까움
    • sitemap 에서 lastmod 가 오늘인 주소만 골라서 보내고, 전부 보내고 싶을 땐 INDEXNOW_ALL=1

구조화 데이터와 llms.txt

  • 목록 페이지는 CollectionPage + ItemList, 상세는 WebPage, 전부 BreadcrumbList
  • 질문형 소제목이 있는 페이지는 FAQPage 를 같이 넣음
    • “1990년생은 2026년에 몇 살인가요?” 같은 질문 바로 아래 첫 문장에 답을 둠
    • 구글 FAQ 리치 결과는 지금 정부·의료 사이트에만 나오지만, AI 검색은 이 구조를 그대로 읽음
  • llms.txt 에 사이트 한 줄 소개와 주요 목록 링크를 둠
  • og:image(1200×630), favicon.ico, RSS 2.0 feed.xml 도 같이 생성

GitHub Pages 하위 경로의 함정

  • 사이트가 kangraemin.github.io/oneul-kkum/ 처럼 하위 경로에 있음
    • robots.txt 는 호스트 루트(kangraemin.github.io/robots.txt)만 읽힘
    • 하위 경로에 robots.txt 를 둬도 소용 없어서, 블로그 레포 루트 robots.txt 에 Sitemap: 줄을 추가함
  • 네이버 서치어드바이저는 사이트 단위(호스트)로만 등록돼서, 블로그 등록에 RSS 주소만 하위 경로로 따로 넣어야 함
  • 서치콘솔은 URL-prefix 속성이라 하위 경로 그대로 등록 가능
  • 배포 직후 URL 검사 API 로 보면 전부 “URL is unknown to Google” — 새 경로는 링크로 발견되기 전까지 이 상태가 한동안 감