URL 대신 page_id로 조회수 추적하기
블로그 글을 정리하다가 폴더 구조를 변경한 적 있으신가요? 저도 최근에 CSS 관련 문서들을 주제별로 재분류했습니다. 그런데 문제가 생겼습니다. 조회수가 전부 0으로 리셋된 겁니다.
변경 전: /web-development/css/object-fit/ → 조회수 127
변경 후: /web-development/css/layout/object-fit/ → 조회수 0 😱
왜 이런 일이 생긴 걸까요?
왜 조회수가 리셋될까요?
대부분의 분석 시스템은 URL 경로를 기준으로 페이지를 식별합니다.
// ❌ 일반적인 방식 - URL 기반 추적
const pageUrl = window.location.pathname;
await supabase.rpc('increment_page_view', { p_url: pageUrl });
이 방식의 문제점:
| 상황 | URL | 결과 |
|---|---|---|
| 원래 위치 | /css/object-fit/ |
조회수 127 |
| 폴더 이동 | /css/layout/object-fit/ |
새 페이지로 인식, 조회수 0 |
| 파일명 변경 | /css/object-fit-guide/ |
새 페이지로 인식, 조회수 0 |
같은 콘텐츠인데 URL만 바뀌면 완전히 다른 페이지로 인식됩니다.
해결책: page_id 도입하기
파일 경로와 독립적인 고유 ID를 사용하면 됩니다.
# ✅ front matter에 page_id 추가
---
title: object-fit으로 이미지 비율 맞추기
page_id: css-object-fit # 파일을 어디로 옮겨도 이 ID는 유지
---
이제 파일을 이동해도:
/css/object-fit/ → page_id: css-object-fit → 조회수 127
/css/layout/object-fit/ → page_id: css-object-fit → 조회수 127 ✅
/archive/2024/css-tips/ → page_id: css-object-fit → 조회수 127 ✅
구현하기
1단계: front matter에 page_id 추가
모든 마크다운 파일에 page_id를 추가합니다.
---
title: 버블 정렬 알고리즘
date: 2025-02-13
layout: page
page_id: algorithms-sorting-bubble-sort # 고유 ID
---
page_id 생성 규칙:
- 파일 경로를 기반으로 생성 (초기 생성 시에만)
- 하이픈(
-)으로 연결 - 소문자 사용
- 한번 생성하면 변경하지 않음
수동으로 추가하기 힘들다면 스크립트로 자동화할 수 있습니다:
// scripts/add-page-ids.js
import fs from 'fs';
import matter from 'gray-matter';
function generatePageId(filePath) {
return filePath
.replace(/\.md$/, '')
.replace(/\//g, '-')
.toLowerCase();
}
function addPageId(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const { data, content: body } = matter(content);
// 이미 page_id가 있으면 스킵
if (data.page_id) return;
const pageId = generatePageId(filePath);
const newData = { ...data, page_id: pageId };
const newContent = matter.stringify(body, newData);
fs.writeFileSync(filePath, newContent, 'utf8');
}
2단계: HTML에 page_id 전달
Jekyll 레이아웃에서 data-page-id 속성을 추가합니다.
<!-- _layouts/page.html -->
<span class="meta-item" id="pageViews" data-page-id="web-development-guides-page-id-based-analytics">
<span id="pageViewCount">0</span>
</span>
3단계: JavaScript에서 page_id 사용
// assets/js/modules/page-views.js
const incrementPageView = async (pageId) => {
const { data, error } = await supabase.rpc('increment_page_view', {
p_page_id: pageId // URL 대신 page_id 사용
});
return data;
};
export const init = async () => {
const pageViewsElement = document.getElementById('pageViews');
// data-page-id 속성에서 가져오기, 없으면 URL 사용 (하위 호환)
const pageId = pageViewsElement?.dataset?.pageId
|| window.location.pathname;
if (!hasRecentlyViewed(pageId)) {
await incrementPageView(pageId);
recordView(pageId);
}
};
4단계: Supabase 스키마 변경
-- page_id 컬럼 추가
ALTER TABLE page_views
ADD COLUMN IF NOT EXISTS page_id TEXT;
-- 기존 URL을 page_id로 변환
UPDATE page_views
SET page_id = LOWER(
REGEXP_REPLACE(
TRIM(BOTH '/' FROM page_url),
'/', '-', 'g'
)
)
WHERE page_id IS NULL;
-- 인덱스 추가
CREATE INDEX IF NOT EXISTS idx_page_views_page_id
ON page_views(page_id);
-- RPC 함수 수정
CREATE OR REPLACE FUNCTION increment_page_view(p_page_id TEXT)
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
v_count INTEGER;
BEGIN
INSERT INTO page_views (page_id, view_count, created_at, updated_at)
VALUES (p_page_id, 1, NOW(), NOW())
ON CONFLICT (page_id)
DO UPDATE SET
view_count = page_views.view_count + 1,
updated_at = NOW()
RETURNING view_count INTO v_count;
RETURN v_count;
END;
$$;
마이그레이션 순서
기존 시스템에서 마이그레이션할 때는 순서가 중요합니다:
1. Supabase SQL 실행 (page_id 컬럼 추가)
↓
2. 마크다운 파일에 page_id 추가
↓
3. JS 코드 수정 및 빌드
↓
4. 배포
흔한 실수와 함정
1. page_id를 나중에 변경하기
# ❌ page_id를 변경하면 조회수 리셋!
page_id: css-object-fit-guide # 원래: css-object-fit
page_id는 한번 정하면 절대 변경하지 마세요. 변경하면 URL 변경과 같은 결과가 됩니다.
2. page_id 중복
# ❌ 두 파일이 같은 page_id를 가지면 조회수가 합쳐짐
# file1.md
page_id: css-layout
# file2.md
page_id: css-layout # 중복!
스크립트로 자동 생성할 때 파일 경로 기반으로 만들면 중복을 방지할 수 있습니다.
3. 하위 호환성 무시
// ❌ page_id가 없는 기존 페이지 고려 안함
const pageId = pageViewsElement.dataset.pageId;
// page_id가 없으면 undefined!
// ✅ 하위 호환성 유지
const pageId = pageViewsElement?.dataset?.pageId
|| window.location.pathname;
언제 이 방식을 사용해야 할까요?
추천하는 경우:
- 콘텐츠 구조를 자주 재구성하는 사이트
- 장기적으로 조회수 데이터를 유지해야 하는 경우
- 자체 분석 시스템을 운영하는 경우
불필요한 경우:
- URL 구조가 고정된 사이트
- Google Analytics만 사용하는 경우 (URL 필터로 해결 가능)
- 조회수 데이터가 중요하지 않은 경우
댓글