SSG에서 "3시간 전"은 낡는다 — 상대 시각을 클라이언트에 맡기는 패턴

블로그·웹 만들기 2026-08-15 15:51:36 astrossgprogressive-enhancement상대시각

글 목록 →

이 블로그 홈의 글 카드에는 “3시간 전” 같은 상대 시각이 붙는다. 그런데 이 사이트는 SSG다 — 페이지는 빌드 때 완성된 HTML로 굳고, 서버는 그 파일을 내려보내기만 한다. 그렇다면 “3시간 전”이라는 텍스트도 빌드 순간에 계산되어 박힌 값이다. 빌드 다음 날 접속한 독자에게도 화면은 여전히 “3시간 전”이라고 말한다. 시간이 흐를수록 거짓이 되는 표시인 셈이다.

굳어도 되는 시각과 낡는 시각

모든 시간 표현이 문제인 건 아니다. 갈림길은 값이 변하느냐다.

  • 절대 시각 — 발행일 “2026-08-15”는 언제 봐도 같은 값이다. 빌드 때 굳혀도 아무 문제가 없고, 대부분의 정적 블로그가 그렇게 한다.
  • 상대 시각 — “N분 전”은 보는 순간에 따라 달라지는 값이다. 빌드에 맡기면 반드시 틀려진다.

여기에 시간대 문제도 겹친다. 빌드 서버(예: GitHub Actions 러너)는 보통 UTC로 돌기 때문에, 서버가 계산한 시각 표기는 독자의 시간대와 어긋날 수 있다.

패턴: 원본은 속성에, 표시는 보는 쪽에서

해법은 역할을 나누는 것이다. 빌드는 변하지 않는 원본 시각만 내려보내고, “지금으로부터 얼마 전인가”라는 계산은 페이지를 보는 브라우저가 한다. HTML 표준의 time 요소가 정확히 이 용도다 — 사람이 읽는 텍스트와 기계가 읽는 datetime 속성을 한 요소에 함께 담는다.

<time datetime={post.data.pubDate.toISOString()} data-relative>
  {fmtRelative(post.data.pubDate)}
</time>

빌드 시점에는 fmtRelative()가 계산한 텍스트가 들어가고, datetime에는 ISO 형식의 원본 시각이 실린다. 그리고 전역 스크립트 하나가 페이지 로드 때 이 요소들을 찾아 텍스트를 덮어쓴다.

document.querySelectorAll("time[data-relative]").forEach((el) => {
  const t = new Date(el.getAttribute("datetime")).getTime();
  const min = Math.floor((Date.now() - t) / 60000);
  el.textContent =
    min < 1 ? "방금"
    : min < 60 ? `${min}분 전`
    : min < 1440 ? `${Math.floor(min / 60)}시간 전`
    : `${Math.floor(min / 1440)}일 전`;
});

Date.now()는 독자의 기기에서 실행되므로 기준 시각은 언제나 “보는 지금”이고, 시간대도 독자의 것이다. 빌드가 며칠 전이었든 표시는 정확해진다.

빌드 텍스트를 지우지 않는 이유

스크립트가 어차피 덮어쓸 텍스트를 왜 빌드 때도 넣어둘까. JS가 실행되지 않는 환경 — 스크립트를 차단한 브라우저, 일부 크롤러, 로딩 실패 — 에서는 그 폴백이 최종 표시가 되기 때문이다. 기본 HTML만으로 일단 동작하고 JS가 있으면 더 정확해지는 이 접근을 progressive enhancement라 부른다. 매일 재빌드되는 블로그라면 폴백의 오차도 하루 안쪽이라, JS 없는 독자에게도 크게 틀린 값은 아니다.

트레이드오프와 변형

이 패턴의 비용은 두 가지다. 첫째, JS 실행 전 폴백 텍스트가 잠깐 보였다가 바뀌는 깜빡임이 이론상 존재한다. 값 차이가 작으면 체감되지 않아 보통 무시하고, 민감하면 스크립트 실행 전까지 CSS로 숨기는 식으로 다듬는다. 둘째, 표시 규칙이 서버(폴백)와 클라이언트(덮어쓰기) 두 곳에 존재하게 되어 한쪽만 고치면 어긋난다 — 주석으로라도 두 구현을 묶어둘 필요가 있다.

직접 스크립트를 짜는 대신 GitHub이 공개한 relative-time 커스텀 엘리먼트처럼 갱신과 현지화까지 맡아주는 웹 컴포넌트를 쓰는 선택지도 있다. 반대로 시각 하나 때문에 React 컴포넌트를 아일랜드로 하이드레이션하는 건 런타임 비용에 비해 과하다.

원칙은 한 줄로 남는다. 변하지 않는 값은 빌드에 굳히고, 흐르는 값은 원본만 실어 보내 보는 쪽에서 계산하게 한다.

댓글

이 글에 대한 의견은 아래 댓글로 남겨주세요 (GitHub 계정 필요). 로그인 없이 남기고 싶다면