정적 사이트로 충분하다: 백엔드 없는 CDN과 빌드타임 예약발행
AI 회사 블로그인데 굴릴 서버가 없다. 이미지 변환은 CDN URL에 위임하고, 예약발행은 런타임 대신 빌드타임 게이트와 일일 스케줄 빌드로 풀었다. 백엔드를 '안 두는 것'도 설계라는 이야기.
지금 읽고 있는 이 블로그에는 뒤에서 돌아가는 서버가 없다. 데이터베이스도, 이미지 처리 워커도, 발행 스케줄러도 없다. AI를 다루는 회사의 기술 블로그라면 어딘가 그럴듯한 백엔드가 깔려 있을 법한데, 우리는 일부러 그 반대로 갔다.
콘텐츠 사이트에는 늘 두 가지 요구가 따라붙는다. 화면 크기에 맞게 이미지를 리사이즈·최적화해 달라는 요구, 그리고 글을 미리 써두고 정해진 날에 공개해 달라는 요구다. 기본 반사는 둘 다 서버를 세워 푸는 것이다 — 이미지 변환 마이크로서비스 하나, 발행 큐를 도는 CMS 하나. 우리는 그 반사를 멈추고 물었다. 이 두 요구에, 정말로 굴러가는 서버가 필요한가.
답은 아니었다. 그래서 이 글은 무엇을 만들었는지가 아니라, 무엇을 안 만들기로 했는지에 대한 기록이다.
이미지: 서버를 세우지 않고 CDN에 위임하다
반응형 이미지의 표준적 해법은 변환 파이프라인이다. 원본을 받아 여러 해상도로 리사이즈하고, 포맷을 바꾸고, 품질을 조정해 저장해 둔 뒤 서빙한다. 그러려면 변환을 돌릴 워커와, 파생본을 둘 스토리지와, 그걸 다 운영할 누군가가 필요하다.
우리는 그 층을 통째로 들어냈다. 원본 이미지는 객체 스토리지에 손대지 않은 채 그대로 둔다. 리사이즈·포맷·품질 조정은 CDN의 URL 기반 변환에 맡긴다. 변환 옵션을 경로에 실어 요청하면 CDN이 그 순간 변환해 캐시하고 응답하는 방식이다. 클라이언트는 미리 정해둔 프리셋(목록 카드용, 본문용, 풀사이즈용 등 너비·품질 조합)에 맞춰 변환 URL을 합성하기만 한다.
// 원본은 그대로. 변환은 CDN 경로에 옵션을 실어 위임한다.
function cdnImage(originUrl: string, preset: Preset): string {
const opts = PRESETS[preset]; // 예: { width, quality } — 실제 수치는 프리셋 정의에
return `https://cdn.example/transform/${opts}/${originUrl}`;
}
결과적으로 우리가 운영하는 이미지 파이프라인은 0이다. 변환 결과를 보관할 스토리지도, 깨질 워커도 없다. 새 해상도가 필요하면 프리셋 한 줄을 추가할 뿐, 원본도 인프라도 건드리지 않는다.
예약발행: 런타임 대신 빌드타임 게이트
두 번째 요구는 더 까다로워 보였다. 정적 사이트에는 “미래에 글을 공개”해 줄 서버가 없다. 정해진 시각에 깨어나 글의 상태를 published로 바꿔줄 런타임이 없는 것이다.
그래서 우리는 발행을 런타임 사건이 아니라 빌드타임 조건으로 재정의했다. 발행일을 글의 메타데이터(date)에 박아두고, 사이트를 빌드할 때 “오늘(KST 기준) 날짜가 도래한 글만” 노출하는 게이트를 통과시킨다. 도래하지 않은 글은 목록에도, RSS에도, 개별 URL에도 나타나지 않는다.
// 빌드 시점에 한 번 평가되는 결정론적 게이트.
function isPublished(date: KstDate, now = today()): boolean {
return kstMidnightOf(date) <= now; // 도래한 날짜만 통과
}
이 게이트만으로는 절반이다. 빌드는 한 번 돌면 그 시점의 판정으로 고정되기 때문이다. 그래서 나머지 절반은 매일 한 번 도는 스케줄 빌드다. 하루에 한 번 사이트를 다시 빌드하면, 그날 도래한 글이 자동으로 게이트를 통과해 공개된다. cron 한 줄이 발행 스케줄러를 대체한다. 과거 날짜를 박으면 다음 빌드에서 즉시 아카이브로 들어가고, 미래 날짜를 박으면 그날 빌드에서 알아서 공개된다.
왜 이렇게 — 안 만드는 쪽의 트레이드오프
동적 해법이 더 강력한 건 사실이다. 이미지 마이크로서비스는 임의의 변환 정책을 즉시 반영하고, 발행 큐를 가진 CMS는 분 단위 예약과 즉시 회수를 지원한다. 우리는 그 힘을 알면서도 버렸다.
버려서 얻은 것이 더 컸기 때문이다. 운영할 서버가 0이면 새벽에 깨질 워커가 없고, 패치할 보안 표면이 없고, 청구서에 붙는 상시 비용이 없다. 더 중요한 건 결정론적 재현성이다. 같은 입력(콘텐츠 파일 + 빌드 시각)이면 언제 돌려도 같은 사이트가 나온다. 상태가 서버 메모리나 DB가 아니라 버전 관리되는 파일에 있으니, 무엇이 언제 왜 공개됐는지가 git 이력 그대로 추적된다.
대가는 정직하게 인정한다. 발행 지연이 최대 하루 생긴다 — 분 단위 예약이나, 요청마다 달라져야 하는 화면에는 이 구조가 맞지 않는다. 우리의 결론은 “정적이 항상 옳다”가 아니다. 이 요구에는 정적이 충분하다는, 범위를 정확히 그은 판단이다.
무엇을 배웠나
흥미로운 건 이 결정들이 우리가 다른 곳에서 쓰는 철학과 정확히 같다는 점이다. AI 운영 시스템을 설계할 때 우리는 전 경로를 먼저 결정론으로 짜고, 동적인 부분(LLM 호출)은 꼭 필요한 자리에만 가산적으로 얹는다. 상태는 컨텍스트 윈도우나 DB가 아니라 파일에 두어 재현·검사·복구가 가능하게 한다.
이 블로그 인프라는 같은 사고의 작은 복제본이다. 결정론적 게이트가 먼저 있고, 동적 빌드 트리거(스케줄)는 그 위에 최소한으로 얹혔다. 화려한 스택이 깊이를 만드는 게 아니라, 요구를 정확히 읽고 가장 단순한 결정론적 메커니즘으로 내린 결정들의 합이 깊이를 만든다. 지금 이 글을 당신이 읽고 있다는 사실 자체가 그 증거다.
어디에 엔진을 두고, 어디엔 두지 않을지
우리가 파는 건 “AI를 얹어 드립니다”가 아니다. 우리는 어디에 엔진을 두고 어디엔 두지 않을지를 판단한다. 모든 표면에 서버와 DB와 운영팀을 붙이는 회사가 아니라, 그 아래에서 가장 단순하고 견고하게 굴러갈 구조를 고르는 회사다. 적게 만들어 더 오래 가게 하는 그 판단을, 당신의 제품 인프라에 그대로 이식할 수 있다.
같이 풀어볼 문제가 있다면 — partners@creativengine.ai.