[Part 4] 복사해도 숫자가 안 올라가던 이유
TL;DR
- 공개한 글의 복사 수가 0으로 남았다. 중복 방지 기록을 공개 여부 확인보다 먼저 남긴 게 원인이었다.
- 순서를 바꾸고, 기준을 "IP당 하루 한 번"에서 "접속(세션)마다 글 하나에 한 번"으로 바꿨다. 부풀리기를 막으려 IP당 하루 60회 상한을 뒀다.
- 화면 숫자는 캐시 때문에 늦게 바뀌므로, 복사하면 그 화면에서 바로 +1 한다.
문제
복사 수는 프롬피드에서 중요한 숫자다. 이번 주 TOP 10 순위가 최근 7일 복사 수(와 저장 수×3)로 정해지기 때문이다.
그런데 공개한 글의 복사 수가 계속 0으로 남았다. 분명 복사가 일어났는데 세지지 않았다.
처음 방식
복사가 일어나면 브라우저가 Supabase의 DB 함수(track_event)를 부른다. 함수는 같은 사람이 여러 번 눌러 숫자를 부풀리지 못하게, IP 기준 하루 한 번만 셌다.
- 오늘 이 IP가 이 글을 복사한 기록이 있으면 → 세지 않는다.
- 없으면 → "오늘 셌음"을 기록하고, 복사 이벤트를 남긴다.
원인: 순서 하나
문제는 "오늘 셌음" 기록을 남기는 시점이었다. 이 기록을 글이 공개됐는지 확인하기 전에 남기고 있었다.
그래서 이런 일이 생겼다.
- 관리자가 공개 전에 미리보기에서 복사를 눌러 본다. 또는 공개 전 테스트로 복사한다.
- 글이 아직 공개되지 않았으니 복사 이벤트는 버려진다. 그런데 "오늘 셌음" 기록은 이미 남았다.
- 글을 공개한 뒤, 같은 IP에서 일어난 복사는 "오늘 이미 셌다"며 무시된다.
관리자가 자기 사이트를 가장 많이 누른다. 결국 공개 직후의 복사가 0으로 남았다.
선택지
- 관리자 IP를 빼고 센다. 관리자만의 문제가 아니다. 공개 전에 복사한 사람이면 누구든 같은 문제를 겪는다.
- 순서만 바꾼다. 공개 여부를 먼저 확인한다. 근본 원인은 고치지만, IP 기준이라 같은 와이파이를 쓰는 여러 사람이 하나로 세지는 문제는 남는다.
- 순서를 바꾸고, 세는 기준도 바꾼다.
내 선택: 세 가지를 같이 바꿨다
- 공개된 글인지 먼저 확인한다. 공개 전이면 아무 기록도 남기지 않는다.
- 접속(세션)마다 글 하나에 한 번 센다. 같은 와이파이를 쓰는 사람들도 따로 센다.
- 세션 값은 브라우저가 보내는 값이라 바꿔 가며 부풀릴 수 있다. 그래서 같은 IP는 하루 60번까지만 센다.
DB 함수의 앞부분은 이렇게 바뀌었다.
sql
create or replace function track_event(p_prompt_id bigint, p_type text, p_session text default null)
...
begin
-- ① 공개된 글인지부터 본다. 아니면 아무것도 남기지 않는다
perform 1 from prompts
where id = p_prompt_id and status = 'published' and published_at <= now();
if not found then return; end if;
-- ② 복사는 세션마다 한 번 (세션 값은 형식을 확인하고 해시로 저장)
-- ③ 세션을 바꿔 가며 부풀리지 못하게, 같은 IP 는 하루 60번까지만
...
end;화면 숫자는 바로 올린다
서버 숫자는 캐시 때문에 몇 분 늦게 바뀐다(1편 참고). 복사를 눌렀는데 숫자가 그대로면 "안 눌렸나?" 싶다.
그래서 복사하면 그 화면에서 바로 +1 한다. 서버에 보내는 것과 별개로, 화면에 먼저 반영하는 낙관적 UI다. 같은 접속에서 같은 글을 두 번 복사하면 두 번째는 올리지 않는다. 서버도 세지 않기 때문이다.
ts
export function trackEvent(promptId: number, type: "view" | "copy" | "like") {
if (!promptId) return; // 관리자 미리보기
// 같은 접속에서 처음일 때만
if (type !== "like" && !firstInSession(`pb:${type === "view" ? "viewed" : "copied"}:${promptId}`)) return;
// 화면의 숫자를 바로 올리도록 알린다 (서버 숫자는 캐시 때문에 늦게 바뀐다)
if (type === "copy") window.dispatchEvent(new CustomEvent(COUNTED_EVENT, { detail: { promptId, type } }));
// ... DB 함수 호출 (fetch, keepalive)
}예전 DB와도 동작하게
이 수정은 DB 마이그레이션(함수에 세션 값 인자 추가)이 필요했다. 코드가 먼저 배포되고 마이그레이션이 늦게 적용되면, 새 인자를 넣은 호출이 404로 실패한다. 그래서 404면 예전 방식으로 한 번 더 부르게 했다. 배포 순서가 어긋나도 숫자가 빠지지 않는다.
결과
- 공개 전 복사가 공개 후 복사를 가로막지 않는다.
- 같은 와이파이를 쓰는 사람들도 각각 센다.
- 세션 값을 바꿔 가며 누르는 부풀리기는 IP당 하루 60회에서 멈춘다.
배운 점
- 중복 방지 기록은 "센 뒤에" 남긴다. 세지도 않은 일을 셌다고 적으면, 나중에 셀 기회를 빼앗는다.
- 가장 많이 누르는 사람은 나 자신이다. 관리자의 행동이 통계를 오염시키지 않는지 먼저 본다.
- DB와 코드는 따로 배포된다. 둘 중 무엇이 먼저 나가도 동작하게 만들어 두면 사고가 줄어든다.
프롬피드 개발기
- 개요. SNS에서 유행하는 AI 프롬프트, 빈칸만 채워 바로 쓰게 만들었다
- 1편. 방문자가 늘어도 DB 조회는 그대로다
- 2편. AI 없이 유행 프롬프트 자동 수집기 만들었다
- 3편. 유튜브 API 무료 한도, 10,000번이 아니었다
- 4편. 복사해도 숫자가 안 올라가던 이유 (지금 읽는 글)
- 5편. 이미지가 깨져 보였던 이유 3가지
- 6편. Lighthouse 모바일 63점을 98점으로 올렸다
- 7편. 개인정보 없이 매주 알림 보내기 (웹 푸시)
- 8편. 테스트가 깨지면 배포가 안 되게 막았다
- 9편. 프롬피드 회고: 잘한 것과 아쉬운 것
프롬피드: promfeed.solfany.com