30개 글이 서로를 모른다 — LLM 없이 내부 링크를 자동으로 구축한 이야기
30개 의 글을 썼다. 그리고 그 글들은 서로를 모른다.
Wo 내부 s의 가장 오래된 SEO 문제 중 하나다. 내부 링크. 글을 쓸 때마다 “이전에 비슷한 내용을 다룬 글이 있었는데…” 하면서도, 찾아보고 링크 거는 건 귀찮아서 미룬다. 한두 개쯤 넣어도, 시스템적인 연결은커녕 기분에 따라 몇 개만 들어간다.
“Related Posts” 플러그인들은 이 문제를 회피한다. 글 하 LLM 에 관련글 박스를 띄운다. 하지만 그건 진짜 내부 링크가 아니다. 구글은 본문 속 링크를 하단 박스보다 훨씬 높게 평가한다. 무엇보다 독자가 글을 다 읽고 나서야 관련 링크를 보여주는 건, 이미 타이밍이 늦었다.
그래서 만들었다. Content Graph Engine (CGE) — LLM 없이, TF-IDF + 택소노미만으로, 매일 새벽 6시에 모든 글의 내부 링크를 자동으로 구축하는 엔진.
내가 만든 것
LLM 없이 TF-IDF만으로 30개 글 사이의 내부 링크를 추천하는 엔진 택소노미(카테고리/태그) + 의미 유사도의 Two-layer 구조 본문에 이미 존재하는 키워드를 앵커로 쓰는 자연스러운 링크 삽입 WordPress REST API + cron으로 매일 새벽 6시 완전 자동 실행
왜 내부 링크가 중요한가
내부 링크가 SEO에 좋다는 건 다 안다. 하지만 나한테 중요한 건 SEO 점수 올리기가 아니다. 독자가 내 사이트에 더 오래 머물게 하는 것이 진짜 목표다.
인간이 쓴 블로그가 200개라면, 그 안에는 자연스러운 정보 계층이 생긴다. “이 주제는 이전 글의 응용편이다”, “이 개념은 저 글에서 자세히 다뤘다” 같은. 내부 링크는 그 관계를 코드로 만드는 작업이다.
그런데 내 경우 30개쯤 되니까 이미 수동으로 관리할 수준이 아니었다. 그래서 Content Quicksand 플러그인으로 글 하단에 관련글 박스를 달았다. 하지만 그건 빠져나가는 문이었다. 진짜 필요한 건 본문 속에서 흐름을 따라가며 발견하는 문맥 링크였다.
왜 LLM을 쓰지 않았는가
내부 링크 추천에 LLM을 쓴다고 상상해보자. 30개 글 사이의 모든 쌍(435쌍)을 비교하려면, 매일 LLM 호출 400번 이상이다. 토큰 비용만 해도 꽤 나온다.
근데 이 문제, LLM 없이도 충분히 잘 풀린다. 내부 링크 추천의 본질은 두 가지다:
같은 주제를 다루는 글은 연결되어야 한다 (택소노미) 비슷한 단어를 많이 쓰는 글은 연결되어야 한다 (TF-IDF)
둘 다 통계로 풀 수 있는 문제다. 의미 이해가 필요한 게 아니다. “에이전트 위임 아키텍처”와 “task() 하나와 GJC Coordinator MCP의 차이”가 같은 글을 가리킨다는 걸 GPT-4가 알려줄 필요는 없다. TF-IDF만으로도 “위임”, “task”, “GJC” 같은 키워드 중첩을 찾아낸다.
단순히 비용 때문만은 아니다. 결정 가능한 문제에 LLM을 쓰면 오히려 불안정해진다. LLM은 매번 다른 결과를 낼 수 있지만, TF-IDF는 입력이 같으면 항상 같은 출력을 낸다. 내부 링크는 결정적(deterministic)이어야 하는 문제다.
Two-Layer 아키텍처
엔진은 두 개의 레이어로 작동한다.
Layer 0: 택소노미 유사도
같은 카테고리나 태그를 공유하는 글들은 일단 연결 후보로 올린다. 예를 들어 “Build Log”에 속한 글들은 서로 자연스러운 링크 대상이다. 간단한 Jaccard 유사도 — 두 글의 카테고리 집합 교집합을 합집합으로 나눈 값.
Layer 1: TF-IDF 의미 유사도
여기가 핵심이다. 각 글의 본문을 TF-IDF 벡터로 변환하고, 코사인 유사도로 비교한다.
TF-IDF(Term Frequency-Inverse Document Frequency)는 1970년대부터 쓰여온 정보검색 기법이다. 각 단어에 “이 글에서 얼마나 많이 나오는가(TF)” × “이 단어가 전체 글에서 얼마나 희귀한가(IDF)”를 곱해서 가중치를 준다.
예를 들어 “에이전트”라는 단어는 내 블로그 대부분의 글에 나오니까 IDF가 낮다. 반면 “MCP”나 “worktree”는 특정 글에서만 나오니까 IDF가 높다. 결과적으로 희귀한 단어를 공유하는 글일수록 높은 유사도를 받는다 — 이게 정확히 원하는 동작이다.
한국어는 형태소 분석이 필요해서 konlpy의 Okt 토크나이저를 쓴다. 한글을 명사 단위로 분해한 뒤, scikit-learn의 TfidfVectorizer로 벡터를 만든다.
핵심 아이디어만
from sklearn.feature_extraction.text import TfidfVectorizer from konlpy.tag import Okt
okt = Okt() texts = [‘ ‘.join(okt.nouns(post.content)) for post in posts] vectorizer = TfidfVectorizer(max_features=5000) tfidf_matrix = vectorizer.fit_transform(texts)
코사인 유사도 계산
from sklearn.metrics.pairwise import cosine_similarity similarities = cosine_similarity(tfidf_matrix)
병합 + 게이트
두 레이어의 결과를 합친다. Layer 1(TF-IDF)를 우선하고, Layer 0(택소노미)은 빈틈을 메우는 보완역으로 쓴다. 글 하나당 최대 5개의 추천만 남긴다 — 너무 많은 링크는 스팸처럼 보인다.
그리고 검증 게이트를 통과한다:
타깃 글이 실제로 publish 상태인가? 소스와 타깃 사이에 이미 링크가 존재하는가? (중복 방지) 타깃 글의 키워드가 소스 글 본문에 실제로 존재하는가? (자연스러운 링크가 가능한가)
수동으로 보면 당연한 검증이다. 하지만 이걸 자동화하지 않으면, “이미 링크되어 있는 글에 또 링크를 거는” 바보 같은 문제가 발생한다.
자동 적용
검증을 통과한 추천 중 상위 N개(현재 10개)를 자동으로 적용한다. 타깃 글의 제목에서 키워드를 추출하고, 소스 글 본문에서 그 키워드가 처음 등장하는 위치에 자연스럽게 링크를 삽입한다.
이게 중요하다. 임의의 텍스트를 추가하는 게 아니다. 본문에 이미 존재하는 키워드를 앵커로 쓴다. “Drewgent”라는 단어가 이미 소스 글에 있으면, 그 단어에 링크를 건다. 없는 단어를 억지로 끼워넣지 않는다. 링크는 자연스러워야 한다.
파이프라인 전체
cron (매일 06:00) └─ content_graph_builder.py ├─ build: WordPress REST API → 모든 글 가져오기 │ ├─ Layer 0: 택소노미 유사도 (Jaccard) │ ├─ Layer 1: TF-IDF 의미 유사도 (코사인) │ └─ 병합 + 검증 게이트 → content-graph.json 저장 └─ apply: 상위 10개 링크 자동 적용 ├─ 키워드 중첩 감지 ├─ 앵커 텍스트 결정 └─ WordPress REST API PATCH → 본문 업데이트
설계 결정들
결정선택이유
링크 추천 엔진TF-IDF (No LLM)결정적 결과. 비용 $0. 30개 글 기준 2초 소요 한국어 토크나이저konlpy.Okt명사 추출 정확도 높음. Mecab보다 설치 간편 최대 링크 수글당 5개, 실행당 10개과도한 링크는 스팸. 점진적 적용이 더 자연스러움 앵커 텍스트본문 기존 키워드 재사용억지 키워드 삽입 금지. 없는 건 링크 걸지 않는다 실행 주기매일 06:00새 글이 올라온 후 한 번만. 과도한 업데이트는 불필요 Content Quicksand와 분리CGE = 본문 속 링크, CQ = 하단 박스역할 분리. 같은 글에 같은 링크를 두 번 걸지 않는다
실제 결과
2026년 6월 27일 오전 6시 첫 실행. 30개 글에서 139개의 후보 추천을 생성했고, 상위 10개를 자동 적용했다.
예를 들면:
“14개에서 6개로…” (v0.8 compression) → “덜어낼수록 강해진다” (contraction 철학) — score 0.97. 같은 패턴을 다른 각도에서 다룬 글 “AI에게 주도권을…” (자동화 실패) → “토큰을 더 쓰는 시대는 끝났다” — score 1.0. 둘 다 시스템 설계에서 “덜어내기”를 다룸 “Claude Code 게임 개발…” (CCGS) → “주목받는 건 모델이 아니라 구조다” — score 0.92. 같은 현상의 서로 다른 분석
모든 링크가 완벽하지는 않다. TF-IDF는 의미를 이해하는 게 아니라 단어 빈도를 비교할 뿐이다. 그래서 검증 게이트가 중요하다. “타깃의 키워드가 소스 본문에 실제로 있는가” — 이 한 줄이 쓰레기 링크를 대부분 걸러낸다.
이게 왜 의미 있는가
Drewgent를 만들면서 계속 반복되는 패턴이 있다. 자동화할 가치가 있는 문제인지 판단하는 것이 자동화 자체보다 어렵다.
내부 링크 구축은 그 판단의 좋은 사례다:
LLM이 필요 없다. 통계로 충분하다. 비용 $0. 자연스러움을 해치면 안 된다. 없는 키워드는 링크를 걸지 않는다. 과하게 하지 않는다. 하루에 10개. 점진적으로 쌓는다. Content Quicksand와 레이어를 나눈다. 본문 속 링크와 하단 박스는 다른 문제다.
SEO 전문가들은 내부 링크의 중요성을 강조하지만, 그걸 실제로 시스템화한 블로그는 드물다. 대부분 “글 쓸 때 수동으로 추가하세요”에서 멈춘다. 나는 그걸 cron job 하나로 해결했다.
30개 글의 그래프는 앞으로 매일 조금씩 더 촘촘해질 거다. 새 글이 올라오면, 엔진은 자동으로 그 글이 기존 글들과 어떻게 연결되는지 찾아낸다. 내가 할 일은 없다.
잘 만든 자동화는 존재감이 없다. 그냥 조용히 돌면서, 결과만 쌓인다.