[Part 1] 방문자가 늘어도 DB 조회는 그대로다
TL;DR
- 공개 화면 전부가 DB 조회 2번의 결과를 서버 캐시에 두고 쓴다. 필터·정렬·검색은 서버 메모리에서 한다.
- 그래서 방문자가 늘어도 DB 조회 수는 그대로다.
- 관리자가 저장하면
revalidateTag로 캐시를 바로 버려서, "캐시 때문에 안 바뀌어요" 문제가 없다.
문제
프롬피드는 Supabase 무료 플랜으로 운영한다. 초기 서비스라 트래픽이 얼마나 될지 모른다. 그래서 처음부터 이렇게 정했다.
방문자가 늘어도 비용과 DB 부하가 늘지 않는 구조를 먼저 만든다.
평범하게 만들면 페이지를 열 때마다 DB를 조회한다. 목록, 카테고리, 태그, 검색, 상세가 모두 각자 쿼리를 날린다. 방문자가 두 배가 되면 조회도 두 배가 된다.
선택지
- 페이지마다 필요한 만큼 조회한다. 가장 흔한 방법이다. 방문자 수에 비례해 조회가 늘어난다.
- 유료 플랜으로 올린다. 트래픽이 불확실한 초기에 고정비를 만든다.
- 공개 데이터를 통째로 캐시하고, 화면은 캐시에서 그린다. 프롬프트 수가 많지 않은 지금 규모에서는 전체를 메모리에 올려도 부담이 없다.
내 선택: 조회 2번, 캐시 두 종류
세 번째를 골랐다. 공개 화면은 전부 아래 두 조회의 결과만 쓴다.
| 캐시 | 내용 | 유지 시간 |
|---|---|---|
| ① 목록용 | 목록에 필요한 가벼운 칸 + 통계 뷰(조회·복사 수 등) | 10분 |
| ② 본문용 | 본문, 빈칸, 팁 | 하루 (거의 바뀌지 않음) |
둘을 나눈 이유는 바뀌는 빈도가 다르기 때문이다. 조회 수와 복사 수는 계속 바뀌지만, 본문은 관리자가 고칠 때만 바뀐다.
ts
import { unstable_cache } from "next/cache";
// ① 목록용: 가벼운 칸 + 통계 뷰. 10분마다 새로
const loadMeta = unstable_cache(
async () => {
const { data, error } = await supabase
.from("prompts_public").select(META_COLUMNS)
.order("copy_count", { ascending: false }).limit(5000);
if (error) throw error;
return data;
},
["public-meta-v1"],
{ tags: [PUBLIC_DATA_TAG], revalidate: META_TTL },
);
// ② 본문용: 거의 안 바뀌어서 하루 동안 둔다
const loadBodies = unstable_cache(
async () => { /* slug, content, variables, tips … */ },
["public-bodies-v3"],
{ tags: [PUBLIC_DATA_TAG], revalidate: BODY_TTL },
);목록, 카테고리, 태그, 검색, 정렬은 이 두 결과를 합친 배열에서 서버 메모리로 거른다. 검색어가 바뀌어도, 정렬이 바뀌어도 DB에는 가지 않는다.
관리자가 저장하면 바로 버린다
캐시를 오래 두면 생기는 문제가 있다. 관리자가 글을 고쳤는데 화면에 반영이 안 된다. 그래서 두 캐시에 같은 태그(PUBLIC_DATA_TAG)를 달고, 저장·발행 액션마다 그 태그를 무효화한다.
ts
import { revalidateTag } from "next/cache";
// 저장·발행이 끝나면
revalidateTag(PUBLIC_DATA_TAG, { expire: 0 }); // 캐시된 조회 결과를 바로 버린다오래된 기록은 매일 지운다
무료 플랜의 DB 용량은 500MB다. 조회·복사 기록은 매일 쌓이므로, 필요한 기간만 남기고 pg_cron으로 매일 정리한다.
| 기록 | 보관 기간 |
|---|---|
| 조회·복사 이벤트 | 35일 |
| 중복 방지 기록 | 3일 |
TOP 10은 최근 7일, 대시보드 그래프는 14일을 쓰므로 35일이면 넉넉하다.
결과
- 방문자가 늘어도 공개 화면이 만드는 DB 조회 수는 그대로다.
- 관리자가 저장하면 바로 반영된다.
- 기록 정리 덕분에 500MB 한도에 여유가 있다.
배운 점
- 무료로 운영한다는 제약이 구조를 단순하게 만들었다. "조회를 어떻게 줄일까"를 먼저 고민하니 캐시 경계가 자연스럽게 정해졌다.
- 캐시는 무효화 방법과 같이 설계해야 한다. 오래 두는 캐시일수록 "언제 버리나"가 분명해야 한다.
- 이 구조는 데이터가 적을 때 맞는 구조다. 글이 아주 많아지면 전체를 메모리에 올리는 방식은 다시 봐야 한다.
프롬피드 개발기
- 개요. SNS에서 유행하는 AI 프롬프트, 빈칸만 채워 바로 쓰게 만들었다
- 1편. 방문자가 늘어도 DB 조회는 그대로다 (지금 읽는 글)
- 2편. AI 없이 유행 프롬프트 자동 수집기 만들었다
- 3편. 유튜브 API 무료 한도, 10,000번이 아니었다
- 4편. 복사해도 숫자가 안 올라가던 이유
- 5편. 이미지가 깨져 보였던 이유 3가지
- 6편. Lighthouse 모바일 63점을 98점으로 올렸다
- 7편. 개인정보 없이 매주 알림 보내기 (웹 푸시)
- 8편. 테스트가 깨지면 배포가 안 되게 막았다
- 9편. 프롬피드 회고: 잘한 것과 아쉬운 것
프롬피드: promfeed.solfany.com