블로그 사용자가 한 글만 읽고 떠나는 문제, PHP 플러그인 하나로 해결했다
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
블로그에 글 25개를 올렸다. 대부분 2,000자 이상, Mermaid 다이어그램도 넣고, 표도 있고, 내부 링크도 걸었다. 그런데 사용자는 한 글만 읽고 떠난다.
시리즈물은 이어지지만, 다른 글과의 연결은 단절되어 있다. Ep.3을 읽은 사람이 Ep.4로 넘어가는 경로가 없다. 같은 주제를 다룬 두 글이 서로를 모른다. 블로그가 페이지의 집합일 뿐, 하나의 공간이 아니다.
그래서 만들었다. Content Quicksand — 읽을수록 빠져드는 개미지옥.
TL;DR
Content Quicksand는 WordPress 플러그인 하나로 블로그 체류 시간을 늘린다. 시리즈 자동 감지, 다축 관련글 추천, 읽기 경로 추적까지 — LLM도 Python도 cron도 필요 없다. 순수 PHP로 작동한다. GitHub에서 무료 공개.
문제는 추천 알고리즘이 아니었다
처음에는 “관련글 추천 알고리즘을 개선해야겠다”고 생각했다. TF-IDF로 유사도를 계산하고, 카테고리 매칭에 가중치를 주고, 신규 글에 부스트를 걸고.
이미 CGE(Content Graph Engine)가 매일 TF-IDF 기반 내부 링크 추천을 생성하고 있었다. 결과물은 꽤 쓸만했다. 문제는 다른 데 있었다.
추천 결과를 사람이 직접 글에 넣고 있었다.
CGE가 “이 글 상단에 A 링크, 하단에 B 링크”라고 알려줘도, 내가 직접 에디터를 열어서 HTML을 삽입해야 했다. 25개 글이 쌓이자 이건 불가능한 일이 됐다. 추천은 존재하는데, 적용되지 않고 있었다.
이 플러그인이 하는 일
Content Quicksand는 네 가지를 자동화한다:
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.
- 시리즈 자동 감지 — 제목에서 Ep.1/Part 2/[시리즈명] 패턴을 감지하고 이전/다음 글 네비게이션을 생성한다
- 다축 관련글 추천 — 시리즈(50점) + 카테고리(20점) + TF-IDF 유사도(30점) + 신규도(10점) + 수동 핀(100점)으로 점수를 매겨 상위 3-6개를 글 하단에 표시
- 인콘텐츠 링크 자동 삽입 — publish 버튼을 누르는 순간, 본문 내 자연스러운 위치에 관련글 링크를 삽입
- 읽기 경로(Reading Trail) — 사용자가 어떤 글을 거쳐왔는지 사이드바에 궤적으로 표시
핵심 설계 결정: LLM 없음. Python 없음. cron 없음. 순수 PHP로 publish 훅에 연결되어 작동한다. WordPress가 이미 가지고 있는 인프라(cron, REST API, post meta)만 사용한다.
왜 PHP 하나로 충분한가
WordPress 플러그인 생태계는 흔히 이렇게 진화한다: PHP 플러그인 → “LLM 기능 추가!” → Python 백엔드 분리 → Docker Compose → cron job → n8n → “아 맞다 이거 블로그 플러그인이었지?”
그 함정에 빠지지 않기로 했다.
내가 운영하는 humanerd.kr은 이미 launchd cron 12개, Docker 컨테이너 2개, MCP 서버 4개, Cloudflare Tunnel, 다양한 Python 스크립트가 돌아가는 시스템이다. 에이전트 14개에서 6개로 압축했고, 5개월 동안 계속 덜어내고 있다. 플러그인 하나 때문에 운영 복잡도를 올리는 건 말이 안 된다.
TF-IDF 유사도 계산조차 PHP로 한다. `similar_text()` + term frequency 벡터로 충분하다. CGE가 생성한 content-graph.json이 있으면 그걸 읽어서 더 정확하게 계산할 수도 있고, 없으면 내장 계산으로 폴백한다. 어느 쪽이든 따로 돌아가는 프로세스는 없다.
$score = 0;
// 같은 시리즈면 +50
if (same_series($post, $candidate)) $score += 50;
// 같은 카테고리면 +20
if (shared_categories($post, $candidate)) $score += 20;
// TF-IDF 유사도 0~30
$score += content_similarity($post, $candidate) * 30;
// 신규 글에 +10
$score += recency_boost($candidate);
// 수동 핀 +100
if (is_pinned($candidate, $post)) $score += 100;
return $score;
}
빌드 경로 — 4단계로 쪼갰다
처음부터 기능을 다 넣으려고 하면 절대 출시하지 못한다. 그래서 각 단계가 혼자서도 쓸모 있게 나눴다.
| 단계 | 만든 것 | 왜 중요한가 |
|---|---|---|
| Slice 1 | 시리즈 자동 감지 | 제목 패턴(Ep.1, Part 2)만으로 시리즈를 찾는다. 별도 설정 없이 작동한다는 걸 증명 |
| Slice 2 | 카테고리 기반 관련글 | 가장 단순한 축으로 기본 추천을 작동시킨다. 시리즈가 없어도 동작 |
| Slice 3 | TF-IDF 유사도 + 수동 핀 | 내용 기반 추천으로 정확도를 높이고, 에디터가 직접 연결을 제어할 수 있게 |
| Slice 4 | 읽기 경로(Reading Trail) + 관리자 UI | 사용자 경험을 완성하고, 관리자가 설정을 조정할 수 있게 |
시리즈 감지는 이렇게 작동한다
플러그인이 글 제목을 분석한다. 패턴은 이렇다:
Origin Story Ep.1,Ep.2,Ep.3→ 시리즈 “Origin Story”로 그룹화Part 1,Part 2→ 같은 prefix를 가진 글끼리 연결[에이전트 설계] 1부,[에이전트 설계] 2부→ 대괄호 시리즈명 감지
감지된 시리즈는 각 글 하단에 이전글 ← 현재글 → 다음글 네비게이션을 자동으로 생성한다. 설정 화면 한 번 열 필요 없다.
기본 패턴으로 감지되지 않는 시리즈는 관리자가 정규식을 등록할 수 있다. 예를 들어 /(d+)장/ 같은 한국어 패턴도 추가 가능.
publish 버튼이 트리거다
WordPress의 save_post 훅에 연결했다. 글을 publish 하는 순간:
- 시리즈 감지 → 같은 시리즈 글들의 푸터 업데이트
- 다축 스코어링 → 상위 3-6개 관련글 선택
- 인콘텐츠 링크 삽입 → 본문 내 적절한 위치에 관련글 링크 추가
- 스코어링 결과를 post meta에 캐싱 (24시간 TTL, 다음 publish 시 갱신)
따로 cron을 돌릴 필요가 없다. WordPress의 내장 cron보다 더 자연스러운 타이밍이 어디 있겠는가 — 글을 쓸 때마다 추천을 갱신하면 된다.
관리 도구 — 에디터를 위한 설계
개발자만 쓰는 플러그인으로 만들 생각은 없었다. 콘텐츠 에디터가 쓸 수 있어야 한다.
- 에디터 버튼 — Gutenberg 편집기 사이드바에서 “이 글에 수동 핀 추가” 버튼. 특정 글을 직접 연결하거나 제외
- Bulk 액션 — 글 목록에서 여러 글을 선택 → “시리즈로 연결” 또는 “스코어 재계산”
- WP-CLI —
wp quicksand rescore --all한 줄로 전체 글의 추천을 재계산 - 관리자 설정 페이지 — 스코어링 가중치 조정, 시리즈 패턴 추가, 표시 위치/개수 설정
추천 시스템이 망가지는 방식은 예측 가능하다. 미리 방어했다:
| 실패 모드 | 대응 |
|---|---|
| 글이 너무 적어서 추천이 안 나옴 | 전체 글이 5개 미만이면 추천 블록 자체를 숨긴다. 없는 걸 보여주느니 안 보여주는 게 낫다 |
| 같은 글만 계속 추천됨 | 이미 읽은 글은 추천 목록에서 제외. post meta에 기록된 최근 추천 이력을 보고 중복 방지 |
| TF-IDF가 엉뚱한 글을 추천 | 카테고리 mismatch 방지: 카테고리가 전혀 다른 글은 유사도가 높아도 제외. 에디터가 수동 핀으로 오버라이드 가능 |
| publish 훅이 너무 오래 걸림 | 스코어링은 publish 후 별도 프로세스가 아니라 동기 실행. 하지만 100개 글 기준 0.3초 미만. 1,000개 넘어가면 WP-CLI로 비동기 배치 처리 |
| 플러그인 비활성화 시 흔적 남음 | 모든 출력은 the_content 필터로 주입. 플러그인을 끄면 글 내용이 원래대로 돌아간다. DB에 저장된 post meta는 유지되지만 출력되지 않음 |
무료 vs Pro — 포크해서 직접 만들라고
GitHub에 무료로 올렸다. 시리즈 감지 + 카테고리 기반 관련글은 Free 버전에 다 들어있다. TF-IDF, 수동 핀, Reading Trail은 Pro 기능으로 분리할 계획이지만, 그냥 PHP를 읽을 수 있다면 직접 추가해도 된다.
구조는 단순하다:
content-quicksand/
├── content-quicksand.php # 플러그인 헤더 + 활성화
├── includes/
│ ├── class-settings.php # 관리자 설정 페이지
│ ├── class-scorer.php # 다축 점수 계산
│ ├── class-renderer.php # 프론트엔드 출력
│ └── class-series.php # 시리즈 자동 감지
└── assets/
└── style.css # 프론트엔드 스타일
각 클래스는 혼자서도 의미가 있다. class-series.php만 떼어내서 다른 플러그인에 붙여도 시리즈 감지가 작동한다. 모놀리스가 아니라 레고 블록이다.
이게 왜 Drewgent의 연장선인가
이 플러그인을 만든 이유는 두 가지다.
첫째, 내 블로그가 내 시간을 잡아먹고 있었다. 글 쓰는 시간보다 글 사이에 링크 거는 시간이 더 길어지고 있었다. CGE는 추천을 생성하는 데 성공했지만, 적용은 내 몫이었다. 자동화는 ‘결정’과 ‘실행’ 사이의 간극을 메우는 것이다. 이 플러그인은 그 간극을 없앴다.
둘째, AI가 모든 문제의 답은 아니다. 5개월 동안 시스템을 덜어내면서 배운 교훈이 있다. 문제를 해결하는 가장 좋은 방법은 종종 기술 스택을 추가하는 게 아니라, 이미 있는 스택에서 더 잘 작동하는 것이다. WordPress 안에서 PHP로 해결할 수 있는 문제를 Python+LLM+cron으로 풀 필요가 없다.
핵심 원칙: WordPress 플러그인은 WordPress 안에 머물러야 한다. PHP로 충분한 기능에 LLM을 붙이는 건, 망치가 필요한 자리에 핵폭탄을 배달하는 일이다.
로드맵
- 지금 — 시리즈 감지 + 카테고리 기반 관련글 (Free, GitHub 공개)
- 다음 — TF-IDF 내장 계산 (CGE 없이도 작동), 관리자 UI, WP-CLI 통합
- 그다음 — Reading Trail, 수동 핀, 커스텀 시리즈 패턴, 다국어
GitHub에서 직접 보고 포크할 수 있다. WordPress Plugin Directory에도 올릴 예정이다.
사용자가 한 글만 읽고 떠나는 블로그는 파일 시스템에 가깝다. 디렉토리 구조는 있지만 길이 없다. Content Quicksand는 그 길을 만드는 플러그인이다. PHP 하나로, publish 버튼 하나로.
이 글은 Build Log 시리즈의 일부입니다. 14개에서 6개로, 5개월의 contraction 기록, ARD 구현기도 함께 읽어보세요.