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 필터로 해결 가능)
  • 조회수 데이터가 중요하지 않은 경우

참고 자료

댓글