김솔비 블로그
기술 블로그

[Part 4] 복사해도 숫자가 안 올라가던 이유

5분 읽기시리즈 5/10

TL;DR

  • 공개한 글의 복사 수가 0으로 남았다. 중복 방지 기록을 공개 여부 확인보다 먼저 남긴 게 원인이었다.
  • 순서를 바꾸고, 기준을 "IP당 하루 한 번"에서 "접속(세션)마다 글 하나에 한 번"으로 바꿨다. 부풀리기를 막으려 IP당 하루 60회 상한을 뒀다.
  • 화면 숫자는 캐시 때문에 늦게 바뀌므로, 복사하면 그 화면에서 바로 +1 한다.

문제

복사 수는 프롬피드에서 중요한 숫자다. 이번 주 TOP 10 순위가 최근 7일 복사 수(와 저장 수×3)로 정해지기 때문이다.

그런데 공개한 글의 복사 수가 계속 0으로 남았다. 분명 복사가 일어났는데 세지지 않았다.

처음 방식

복사가 일어나면 브라우저가 Supabase의 DB 함수(track_event)를 부른다. 함수는 같은 사람이 여러 번 눌러 숫자를 부풀리지 못하게, IP 기준 하루 한 번만 셌다.

  • 오늘 이 IP가 이 글을 복사한 기록이 있으면 → 세지 않는다.
  • 없으면 → "오늘 셌음"을 기록하고, 복사 이벤트를 남긴다.

원인: 순서 하나

문제는 "오늘 셌음" 기록을 남기는 시점이었다. 이 기록을 글이 공개됐는지 확인하기 전에 남기고 있었다.

그래서 이런 일이 생겼다.

  1. 관리자가 공개 전에 미리보기에서 복사를 눌러 본다. 또는 공개 전 테스트로 복사한다.
  2. 글이 아직 공개되지 않았으니 복사 이벤트는 버려진다. 그런데 "오늘 셌음" 기록은 이미 남았다.
  3. 글을 공개한 뒤, 같은 IP에서 일어난 복사는 "오늘 이미 셌다"며 무시된다.

관리자가 자기 사이트를 가장 많이 누른다. 결국 공개 직후의 복사가 0으로 남았다.

선택지

  1. 관리자 IP를 빼고 센다. 관리자만의 문제가 아니다. 공개 전에 복사한 사람이면 누구든 같은 문제를 겪는다.
  2. 순서만 바꾼다. 공개 여부를 먼저 확인한다. 근본 원인은 고치지만, IP 기준이라 같은 와이파이를 쓰는 여러 사람이 하나로 세지는 문제는 남는다.
  3. 순서를 바꾸고, 세는 기준도 바꾼다.

내 선택: 세 가지를 같이 바꿨다

  1. 공개된 글인지 먼저 확인한다. 공개 전이면 아무 기록도 남기지 않는다.
  2. 접속(세션)마다 글 하나에 한 번 센다. 같은 와이파이를 쓰는 사람들도 따로 센다.
  3. 세션 값은 브라우저가 보내는 값이라 바꿔 가며 부풀릴 수 있다. 그래서 같은 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와 코드는 따로 배포된다. 둘 중 무엇이 먼저 나가도 동작하게 만들어 두면 사고가 줄어든다.

프롬피드 개발기

프롬피드: promfeed.solfany.com