AI 사용기
AI 활용기: 같은 통계 기능을 저장소 4개에 동시에 심었는데 코드가 다 달랐다
쿠키 없는 방문자 집계 기능 하나를 백엔드·정적 블로그·플레인 HTML·Next.js 앱에 같은 원리로 넣었더니, 프레임워크마다 코드 형태가 달라진 이유와 Plausible 같은 실제 프라이버시 분석 도구의 설계를 비교했다.
여러 저장소에 같은 기능을 넣어야 하는 사람에게, AI에게 '복붙'과 '같은 원리를 각 스택에 맞게'가 왜 다른 결과를 내는지 실제 코드로 보여준다.
정적 사이트 둘(Astro 블로그, 플레인 HTML)에는 문자 그대로 동일한 바닐라 JS 파일이 들어갔지만, React 기반 웹 도구 저장소에는 같은 로직이 useEffect 훅과 클린업 함수를 갖춘 컴포넌트로 다시 쓰였다. 서버 쪽은 방문자·세션 식별자를 원문 그대로 저장하지 않고 '그날 날짜 + 비밀키'로 해시해 하루 단위로만 재식별 가능하게 했는데, 이 방식은 실제로 Plausible Analytics가 문서화한 일일 로테이션 해시 방식과 원리가 같다.
여러 저장소에 같은 기능을 넣을 때는 AI에게 파일을 그대로 복사하라고 하지 말고, 각 저장소가 정적 사이트인지 프레임워크 컴포넌트인지 먼저 구분해서 요청하면 나중에 유지보수할 코드가 그 저장소 관례에 맞게 남는다.
목차
먼저 답하면
같은 기능을 여러 저장소에 넣어달라고 하면 AI는 파일을 그대로 복사하지 않는다. 정적 HTML 사이트 둘에는 똑같은 바닐라 JS 파일이 들어갔지만, React 기반 저장소에는 같은 로직이 그 프레임워크의 생명주기(useEffect, 클린업 함수)에 맞춰 다시 쓰였다. 서버는 방문자 식별자를 원문으로 저장하지 않고 날짜별로 해시했는데, 이 방식은 실제 프라이버시 분석 서비스인 Plausible이 쓰는 방식과 원리가 같았다.
오늘 오전 11시 23분, 저장소 4개가 동시에 바뀌었다
블로그에 광고를 붙이기 전에 “방문자가 몇 명인지도 모르고 광고 얘기를 하는 게 이상하다”는 생각이 들어서, 페이지뷰만 세는 통계 기능을 넣어달라고 요청했다. 대상은 네 곳이었다. 이 블로그(Astro 정적 사이트), 웹 도구 모음(Next.js), 개인 홈페이지(플레인 HTML), 그리고 이 셋의 데이터를 받아줄 백엔드 서비스(FastAPI). 커밋 로그를 보면 네 저장소 모두 같은 분(11:23)에 각각 버전이 하나씩 올라가 있다.
같은 요청을 네 번 한 게 아니라 한 번 요청했는데, 결과물을 나중에 diff로 비교해 보니 코드가 저장소 성격에 따라 갈라져 있었다.
- 이 블로그(
web/public/site-metrics.js)와 개인 홈페이지(js/site-metrics.js)는 바이트 단위로 완전히 동일한 파일이다.diff결과가 아무것도 안 나온다. - 웹 도구 저장소는 같은 파일이 아니라
components/site-metrics.tsx라는 React 컴포넌트로 들어갔다.useEffect로 이벤트 리스너를 등록하고, 언마운트 시 리스너를 정리하는return클린업까지 갖췄다. 로직(방문자 ID 발급, 세션당 1회 전송,.tool-page내부 조작만tool_use로 집계)은 바닐라 버전과 동일하지만, 문법과 생명주기 처리는 그 저장소의 관례를 그대로 따랐다.
정적 사이트 둘은 “같은 파일을 놓기만 하면 되는 곳”이라 파일 자체가 복제됐고, React 앱은 “그 프레임워크의 컴포넌트 트리 안에서 살아야 하는 곳”이라 코드가 다시 쓰인 셈이다. 지시문에 “React 컴포넌트로 만들어라”라고 쓰지 않았는데도 이렇게 갈렸다는 점이 흥미로웠다.
서버 쪽 설계: 원문을 안 남기는 방식
백엔드의 저장 로직(app/modules/site_metrics/repository.py)을 보면, 방문자 식별자와 세션 식별자를 그대로 저장하지 않는다. 대신 HMAC(비밀키, "오늘날짜:식별자") 값을 저장한다.
def anonymize(self, value: str, day: str) -> str:
return hmac.new(self.secret, f"{day}:{value}".encode(), hashlib.sha256).hexdigest()
이렇게 하면 같은 브라우저가 하루 동안 여러 페이지를 봐도 “그날 하루의 고유 방문자”로는 묶이지만, 날짜가 바뀌면 해시값도 바뀌어서 어제와 오늘의 방문을 하나의 사람으로 이어붙일 수 없다. 원본 로컬스토리지 값도 400일이 지나면 지워지는 이벤트 테이블에서 함께 정리된다. IP는 요청 속도 제한(같은 접속원에서 분당 120건 초과 차단)에만 잠깐 메모리에서 쓰고 저장하지는 않는다.
Plausible도 같은 원리를 쓰고 있었다
이 방식이 특별히 새로운 발명은 아닌지 궁금해서 실제 서비스형 프라이버시 분석 도구를 찾아봤다. Plausible Analytics의 데이터 정책에는 정확히 같은 구조가 문서로 나와 있다. 방문자 식별값을 hash(일일 솔트 + 도메인 + IP + User-Agent)로 만들고, 이 솔트는 24시간마다 교체되고 폐기된다. 쿠키도, 영구 식별자도 남기지 않는다는 원칙까지 같다.
차이는 두 가지다. Plausible은 하루 단위로 “교체되는 솔트”를 쓰고, 이번 구현은 “고정 비밀키 + 날짜 문자열”을 HMAC에 섞어 같은 효과(날짜가 바뀌면 재식별 불가)를 낸다는 점, 그리고 Plausible은 애초에 로컬스토리지나 쿠키를 전혀 쓰지 않고 매 요청을 IP+UA 조합만으로 재구성하는 반면, 이번 구현은 로컬스토리지에 발급한 UUID를 하루짜리 해시로 감싸는 방식이라는 점이다. 목적(어제와 오늘의 방문자를 서로 연결하지 못하게 만드는 것)은 같고 구현 재료만 다르다.
프레임워크별 어댑터를 따로 두는 구조도 흔한 패턴이었다. remcostoeten/analytics는 Next.js/React 전용 SDK를 앞세우면서 그 외 앱에는 개별 옵저버 함수를 따로 연결하도록 안내하고, tally-web-analytics는 트래커 자체는 약 1KB짜리 순수 바닐라 JS로 통일하고 대시보드만 React로 분리해뒀다. 이번 네 저장소 구현과 방향이 같다 — 데이터를 보내는 쪽(트래커)은 최대한 가볍고 프레임워크 중립적으로 두고, 그걸 담는 그릇(정적 스크립트 vs. 컴포넌트, 대시보드)만 저장소 사정에 맞춘다.
다음에 여러 저장소에 같은 기능을 넣을 때
이번 일로 확인한 건, “네 곳에 다 넣어줘”라는 요청 하나만으로도 AI가 저장소별 관례를 알아서 구분한다는 것과, 그 구분이 항상 내가 원하는 경계와 일치하지는 않을 수 있다는 것이다. 이번엔 운 좋게 정적 사이트/프레임워크 앱의 경계가 정확히 갈렸지만, 다음에는 결과물을 저장소별로 diff까지 떠서 “왜 이 둘은 같고 이건 다른가”를 확인하는 습관을 붙이려 한다. 코드가 다르게 나온 이유를 납득할 수 있어야, 나중에 넷 중 하나만 고쳐야 할 때 나머지 셋도 따라 고쳐야 하는지 판단할 수 있기 때문이다.