트러블슈팅

  • WPCode로 보안 헤더 추가하다 코드가 깨진 이유

    WPCode로 보안 헤더 추가하다 코드가 깨진 이유

    SEO 감사 결과를 받아들고 액션 아이템 목록을 훑었다. 생각보다 할 게 많았다. 그중에서 오늘 건드린 건 두 가지다.

    SEO 감사에서 나온 보안 헤더 추가, 왜 하려고 했나?

    감사 리포트에 보안 헤더(HTTP Security Header, 브라우저에게 보안 정책을 알려주는 응답값)가 빠져 있다고 나왔다. 구체적으로는 세 가지였다. HSTS(HTTP Strict Transport Security, HTTPS 연결만 허용하도록 강제하는 헤더), X-Content-Type-Options(브라우저가 파일 형식을 멋대로 해석하지 못하게 막는 헤더), Referrer-Policy(다른 사이트로 이동할 때 내 사이트 주소를 얼마나 보낼지 제어하는 헤더). 검색엔진 점수에 직접 영향을 주는 항목은 아니지만, 빠져 있으면 감사 점수가 깎인다.

    여기에 하나 더 추가하고 싶은 게 있었다. llms.txt다. llms.txt는 AI 크롤러(ChatGPT, Claude 같은 AI가 사이트 정보를 수집할 때 읽어가는 파일)가 내 사이트 구조를 파악할 수 있도록 만들어두는 텍스트 파일이다. robots.txt(검색엔진 크롤러에게 크롤링 규칙을 알려주는 파일)의 AI 버전이라고 보면 된다.

    문제는 서버 접근 방법이었다. FTP(File Transfer Protocol, 서버에 파일을 올리고 내리는 방식)도 없고 SSH(Secure Shell, 서버에 원격으로 접속하는 방식)도 없었다. 서버에 직접 파일을 올릴 수 있는 방법이 아예 없는 환경이었다. 그래서 선택한 게 WPCode다. WPCode는 워드프레스 관리자 화면에서 PHP 스니펫(snippet, 짧은 코드 조각)을 직접 추가하고 실행할 수 있는 플러그인(plugin, 워드프레스에 기능을 추가하는 확장 프로그램)이다.

    WPCode로 보안 헤더·llms.txt 추가하다 겪은 삽질기 스크린샷 1

    위는 WPCode 플러그인 편집 화면이다. PHP 코드를 여기에 붙여넣고 활성화하면 워드프레스가 코드를 실행한다.

    CodeMirror 자동완성이 PHP 코드를 깨뜨린 문제

    WPCode의 코드 에디터(code editor, 코드를 입력하고 수정하는 편집 창)는 CodeMirror라는 라이브러리(library, 특정 기능을 묶어놓은 코드 모음)로 만들어져 있다. 편집기 기능이 꽤 풍부한데, 그중에 자동 괄호 완성 기능이 있다. 괄호를 하나 열면 닫는 괄호를 자동으로 붙여주는 기능이다.

    이게 문제였다. Claude Code로 에디터에 타이핑을 시켰더니, 자동완성이 끼어들면서 코드 중간에 괄호가 두 개씩 들어가거나 엉뚱한 위치에 삽입되는 일이 계속 생겼다. PHP 코드는 괄호 하나만 틀려도 에러가 난다. 별로였다.

    그러다가 예상치 못한 확인창이 떴다. 뭘 클릭한 건지도 모르겠는 상태에서 페이지가 about:blank로 넘어갔다. about:blank는 아무것도 없는 빈 페이지다. 입력하던 코드가 전부 날아갔다. 저장도 안 된 상태로.

    타이핑 대신 setValue()로 코드를 통째로 밀어넣다

    다시 페이지를 열었다. 이번엔 방식을 바꿨다.

    CodeMirror는 자바스크립트(JavaScript, 웹 브라우저에서 실행되는 프로그래밍 언어)로 제어할 수 있다. 에디터 인스턴스(instance, 실행 중인 특정 객체)에 setValue()라는 메서드(method, 객체가 수행할 수 있는 기능)를 호출하면, 타이핑 없이 코드를 통째로 집어넣을 수 있다. 자동완성이 개입할 여지가 없다. 한 번에 전체 내용이 들어가니까.

    브라우저 콘솔(console, 개발자 도구에서 자바스크립트를 직접 실행할 수 있는 창)에서 아래처럼 실행했다.

    document.querySelector('.CodeMirror').CodeMirror.setValue(`여기에 PHP 코드 전체`)

    코드가 깨끗하게 들어갔다. 자동완성 간섭 없이. 저장하고 활성화한 뒤 curl(curl, 터미널에서 HTTP 요청을 보내는 명령어 도구)로 확인했더니 세 헤더가 전부 응답에 포함돼 있었다. HSTS, X-Content-Type-Options, Referrer-Policy. 다 떴다.

    WPCode로 보안 헤더·llms.txt 추가하다 겪은 삽질기 스크린샷 2

    curl 응답에서 세 보안 헤더가 실제로 찍힌 결과다. 헤더가 응답에 없으면 아무리 코드를 넣어도 동작하지 않은 것이니, 이 확인 단계는 건너뛰면 안 된다.

    보안 헤더는 성공했는데 llms.txt는 404가 뜬 이유

    같은 방식으로 llms.txt도 만들었다. WPCode에 스니펫을 하나 더 추가해서, 특정 URL로 접근하면 llms.txt 내용을 텍스트로 응답하도록 했다. setValue()로 코드를 넣었으니 입력 과정은 문제없었다.

    그런데 curl로 확인하니 이상했다. 본문 내용은 정확하게 나왔다. llms.txt에 넣으려던 텍스트가 그대로 출력됐다. 그런데 HTTP 상태 코드(HTTP Status Code, 서버가 요청에 대해 응답할 때 같이 보내는 숫자 코드)가 404였다.

    404는 “없는 페이지”라는 뜻이다. 내용은 있는데 없는 페이지라고 응답하는 이상한 상태였다.

    상태 코드를 명시적으로 지정해야 했던 워드프레스의 특성

    워드프레스는 URL을 처리하는 방식이 독특하다. 등록된 페이지나 포스트가 아닌 URL로 요청이 들어오면, 워드프레스는 그 URL을 404로 먼저 처리해버린다. llms.txt는 워드프레스에 등록된 페이지가 아니다. 그러니까 워드프레스 입장에서는 없는 URL이다.

    PHP 코드로 본문 내용을 echo(echo, PHP에서 텍스트를 출력하는 명령어)해도, 워드프레스가 이미 404로 결정한 상태 코드는 바뀌지 않는다. 내용은 출력되지만 상태 코드는 404 그대로 남는 거다. 검색엔진이나 AI 크롤러는 상태 코드를 보고 판단한다. 404면 없는 페이지다. 내용이 아무리 정확해도 무시된다.

    해결 방법은 status_header(200)을 코드에 추가하는 거였다. status_header()는 워드프레스에서 HTTP 상태 코드를 강제로 지정하는 함수(function, 특정 작업을 수행하는 코드 묶음)다. 이걸 echo 앞에 넣어서 200으로 바꿔줬다.

    그래도 처음엔 안 됐다. 라이트스피드 캐시(LiteSpeed Cache, 웹사이트 속도를 높이기 위해 페이지를 미리 저장해두는 플러그인)가 이전 404 응답을 저장해두고 있었다. 캐시 퍼지(purge, 저장된 캐시를 강제로 삭제하는 작업)를 하고 나서야 curl에서 200이 확인됐다.

    WPCode로 보안 헤더·llms.txt 추가하다 겪은 삽질기 스크린샷 3

    캐시 퍼지 후 curl로 다시 확인한 결과다. 상태 코드가 200으로 바뀐 걸 확인할 수 있다. 이 단계 없이는 제대로 됐는지 알 수 없다.

    코드 에디터 자동화와 HTTP 헤더를 다루면서 배운 두 가지

    이번 작업에서 정리된 게 두 가지다.

    첫 번째. 자동화 도구로 코드 에디터를 다룰 땐 타이핑을 시키면 안 된다. 자동완성, 자동 들여쓰기, 단축키 처리 같은 에디터 기능이 전부 방해 요소가 된다. 에디터 인스턴스에 직접 값을 주입하는 방식을 써야 한다. CodeMirror라면 setValue()가 그 방법이다. 타이핑은 사람이 할 때만 자연스럽다.

    두 번째. 워드프레스에서 커스텀 URL(custom URL, 워드프레스에 등록되지 않은 주소)을 서빙할 땐 본문 내용만 맞춰선 끝이 아니다. HTTP 상태 코드를 직접 200으로 지정해야 한다. 안 하면 워드프레스가 404를 내보낸다. 크롤러는 그 페이지를 없는 페이지로 처리한다. llms.txt를 만들어도 아무도 읽지 않는 상황이 된다.

    다음엔 llms.txt 내용을 실제로 AI 크롤러가 제대로 읽어가는지 확인해볼 생각이다. 200이 뜨는 건 확인했는데, 실제로 색인(index, 검색엔진이나 AI가 페이지 내용을 자신의 데이터베이스에 등록하는 것)이 되는지는 별개 문제다. 아직 모른다.

  • 애드센스가 말한 ‘가치 없는 콘텐츠’의 진짜 원인, 블로그를 직접 진단해봤다

    애드센스가 말한 ‘가치 없는 콘텐츠’의 진짜 원인, 블로그를 직접 진단해봤다

    이 블로그는 파이프라인으로 돌아간다. 글도, 이미지도, 업로드도 자동화돼 있다. 근데 이 파이프라인 자체가 제대로 굴러가는지 스스로 점검해본 적은 없었다. 애드센스 심사 결과가 그 계기가 됐다.

    애드센스 심사 결과를 열어봤더니 딱 두 줄이 있었다. ‘주의 필요’, 그리고 ‘가치가 별로 없는 콘텐츠’. 2026년 9월 29일 오전 5:31 JST 기준으로 업데이트된 결과였다. 한마디로 가치 없는 콘텐츠라는 판정이었다.

    뭔가 더 구체적인 설명이 있을 줄 알았다. 없었다. 그냥 저 두 줄이 전부였다.

    애드센스가 보여준 거절 화면, ‘가치가 별로 없는 콘텐츠’는 뭐 때문?

    애드센스 사이트 관리 화면에 직접 들어가서 확인한 내용이다. 승인 상태가 ‘주의 필요’로 표시돼 있고, 그 아래 상태 세부정보에 ‘가치가 별로 없는 콘텐츠’라고 적혀 있었다.

    애드센스가 ujinify.com을 '가치 없는 콘텐츠'로 거절한 진짜 이유 — 색인 문제인 줄 알았는데 아니었다 스크린샷 1

    위 화면이 실제로 본 거절 메시지다. 저게 전부다. 애드센스는 왜 가치가 없는지, 어떤 기준으로 판단했는지 아무것도 알려주지 않는다. 이 화면 하나만 놓고 뭐가 문제인지 파악해야 한다. 불친절하다.

    그래서 직접 뜯어보기로 했다. 가능한 원인을 하나씩 짚어보는 방식으로.

    첫 번째 의심: 옛날 도메인 이력이 구글을 헷갈리게 한 건 아닐까

    ujinify.com은 재활용 도메인(다른 사람이 쓰다가 만료된 도메인을 다시 등록해서 사용하는 것)이다. 이 도메인은 예전에 건강·영양제 주제의 블로그로 쓰였던 이력이 있다. 그걸 알고 있었다.

    첫 번째 가설은 이거였다. 구글이 아직도 이 사이트를 옛날 건강 블로그로 색인(인덱싱, index , 구글이 웹사이트 내용을 읽고 검색 결과에 등록하는 것)하고 있어서, 애드센스 심사봇이 지금 블로그와 전혀 다른 주제의 콘텐츠를 보고 오해한 게 아닐까 하는 것이었다.

    그럴듯했다. 실제로 재활용 도메인에서 종종 생기는 문제이기도 하다.

    Site: 검색과 구글 서치 콘솔로 직접 확인한 결과

    가설을 검증하는 가장 빠른 방법은 직접 구글에서 검색해보는 거다. site:ujinify.com이라고 치면 구글이 현재 이 도메인에서 색인한 페이지 목록이 나온다.

    결과는 예상과 달랐다.

    타이틀도, 설명도 전부 지금 블로그 내용 그대로였다. ‘AgentLog – Claude Code로 만든 실전 자동화 이야기’. 건강이나 영양제 관련 흔적은 하나도 없었다. 첫 번째 가설은 틀렸다.

    애드센스가 ujinify.com을 '가치 없는 콘텐츠'로 거절한 진짜 이유 ,  색인 문제인 줄 알았는데 아니었다 스크린샷 2

    site: 검색 결과만으로는 부족할 수 있어서 구글 서치 콘솔(GSC, Google Search Console , 구글이 내 사이트를 어떻게 보고 있는지 확인할 수 있는 무료 도구)도 열었다.

    색인 생성 보고서를 보니 숫자가 이랬다.

    • 색인된 페이지: 11개
    • 색인 안 된 페이지: 56개 (사유 6가지)

    56개 중 가장 큰 덩어리가 눈에 들어왔다. 23개가 ‘NOINDEX 태그에 의해 제외됨’이었다.

    애드센스가 ujinify.com을 '가치 없는 콘텐츠'로 거절한 진짜 이유 ,  색인 문제인 줄 알았는데 아니었다 스크린샷 3

    클릭해서 목록을 열어봤다. URL을 보는 순간 바로 이해됐다. /tag/영양제-생체-이용률-차이, /tag/만성피로영양제추천, /tag/모낭세포재생비결, /tag/신경건강, /tag/근력약화. 전부 예전 건강 블로그가 남긴 태그 아카이브(archive, 특정 태그나 카테고리에 속한 글들을 모아서 보여주는 페이지) 페이지였다. 최종 크롤링 날짜도 2026년 2월~5월로, 현재 블로그 글들이 시작된 9월보다 몇 달 앞선다.

    애드센스가 ujinify.com을 '가치 없는 콘텐츠'로 거절한 진짜 이유 ,  색인 문제인 줄 알았는데 아니었다 스크린샷 4

    옛날 흔적이 맞긴 했다. 단지 예상한 형태가 아니었다.

    NOINDEX 태그 페이지들은 실제 문제가 아니었던 이유

    23개짜리 유령 태그 페이지들을 발견했을 때 ‘이게 원인이구나’라고 생각할 수도 있다. 아니다. 정확히 반대다.

    이 페이지들은 이미 NOINDEX(노인덱스 , 구글에게 이 페이지는 검색 결과에 올리지 말라고 지시하는 태그) 처리가 돼 있다. 그래서 site: 검색에서 전혀 안 나오는 거다. SEO 플러그인(검색엔진 최적화를 도와주는 워드프레스 확장 프로그램)이 연결된 글이 없는 빈 태그 아카이브를 noindex로 처리하는 것은 구글 공식 문서에도 나오는 정상 동작이다.

    즉, 실제 검색 노출에는 아무 영향도 없다. GSC 보고서 안에만 잔여 데이터로 남아 있을 뿐이다. 이걸 문제라고 잡으러 갔다가는 시간만 낭비한다. 예전에 랭크매스 무료 기능으로 고아 포스트를 찾아냈을 때도 비슷했다 , 진짜 문제는 항상 보고서 첫 줄이 아니라 몇 단계 더 들어간 곳에 있었다.

    진짜 문제는 다른 곳에 있었다.

    발견된 또 다른 버그: ads.txt 파일 누락 (그리고 고치는 법)

    같은 애드센스 사이트 관리 화면에서 Ads.txt 상태가 ‘찾을 수 없음’으로 표시된 걸 발견했다. 콘텐츠 문제 조사하다가 옆에서 발견한 거다.

    ads.txt(광고 판매 권한을 가진 업체를 선언하는 텍스트 파일)는 사이트 루트(root, 도메인 주소 바로 뒤에 오는 최상위 경로)에 있어야 한다. https://ujinify.com/ads.txt로 접근했을 때 파일이 있어야 한다는 뜻이다.

    curl(컬 , 터미널에서 URL에 직접 요청을 보내 응답을 확인하는 명령어)로 직접 확인해봤더니 404였다. 진짜로 없었다.

    고치는 법은 간단하다.

    • WP 파일 관리자(워드프레스 대시보드에서 서버 파일을 직접 편집할 수 있는 플러그인)를 열어서 사이트 루트 폴더에 ads.txt 파일을 새로 만든다.
    • 파일 내용으로 이 한 줄을 그대로 넣는다: google.com, pub-[본인의 퍼블리셔 ID], DIRECT, f08c47fec0942fa0
    • 저장하고 curl 또는 브라우저로 https://[본인도메인]/ads.txt를 열어서 200 응답이 오는지 확인한다.

    이렇게 했더니 바로 200으로 정상 응답했다.

    ads.txt가 없으면 애드센스 승인 이후 광고 수익에도 영향을 줄 수 있다. 콘텐츠 문제와 별개로 반드시 고쳐야 하는 설정이다. 가치 없는 콘텐츠 판정과는 무관한 별도 버그였지만, 콘텐츠 쌓느라 이걸 놓치면 나중에 더 귀찮아진다.

    결론: 색인이 아니라 ‘콘텐츠를 더 쌓아야 한다’는 뜻

    조사 결과는 명확했다. 도메인 이력이 문제가 아니다. 유령 태그 페이지들이 문제가 아니다. 애드센스 화면에 적힌 그대로다. ‘가치가 별로 없는 콘텐츠’ , 지금 발행된 글이 11개 안팎이라 아직 부족하다고 본 거다. 가치 없는 콘텐츠라는 판정을 뒤집으려면 결국 콘텐츠를 더 쌓는 수밖에 없다.

    ‘가치 없는 콘텐츠’라는 판정 앞에서는 어떤 가설도 일단 의심부터 해야 했다. 돌아보면 색인 문제일 수도 있다는 가설은 그냥 불안해서 만들어낸 거였다. 뭔가 기술적인 버그가 원인이었으면 고치고 빨리 재신청할 수 있을 테니까. 그게 더 편한 시나리오였다.

    현실은 더 단순하고 느리다. 글을 쌓아야 한다. 25개 정도까지 채운 뒤에 재신청하기로 했다. ads.txt는 이미 고쳤으니 그건 됐다.

    서두르지 않는다. 어차피 콘텐츠가 없으면 재신청해도 결과는 같다.

    이 블로그를 돌리는 파이프라인 자체를 점검해본 첫 사례였다. 스스로 진단하고 고칠 수 있는 건 바로 고치고, 나머지는 시간이 해결한다.

  • 자막이 카드 텍스트와 겹쳤다, y=h*0.8로 좌표 하나 바꿔 고친 이야기

    자막이 카드 텍스트와 겹쳤다, y=h*0.8로 좌표 하나 바꿔 고친 이야기

    버그는 항상 예상 못 한 타이밍에 나온다. 영상을 재생했더니 자막이 카드 문구 위에 그냥 올라가 있었다. 전형적인 자막 좌표 버그였다. 원인은 좌표 하나였다. 그런데 그 좌표를 고친 뒤 확인하는 과정이 더 흥미로웠다.

    aidojin-shorts-generator는 어떤 프로젝트인가?

    인스타그램 릴스 같은 숏폼 플랫폼에는 AI 도구 관련 주장이 넘쳐난다. “Claude Code로 이걸 했다”, “GPT로 저걸 만들었다” — 검증 없이 퍼진다. aidojin-shorts-generator는 그런 주장을 팩트체크하고, 결과를 숏폼 영상으로 자동 제작하는 파이프라인(pipeline, 여러 작업 단계를 자동으로 연결한 처리 흐름)이다.

    처음부터 만든 건 아니다. 건강 채널용 영상을 자동화하려고 만들어뒀던 shorts-generator를 포크(fork, 기존 코드를 복사해서 새 프로젝트로 분리하는 것)했다. Whisper(오디오에서 자막 타임스탬프를 추출하는 AI 모델) 기반 자막, Ken Burns 효과(이미지를 천천히 확대하거나 이동시키는 영상 기법), FFmpeg(영상·음성 합성에 쓰는 오픈소스 도구) 합성 , 이 핵심 로직은 그대로 가져왔다.

    첫 테스트 주제로 선택한 건 릴스에서 본 주장이었다. “Claude Code에 GitHub 저장소(repository, 코드를 저장하는 온라인 공간) 7개를 깔아뒀다.” 그중 하나가 anthropics/claude-code였다. GitHub API로 스타 수까지 직접 대조해서 검증했다. 영상으로 만들어보려고 실행했더니 문제가 생겼다.

    자막 좌표 버그가 나온 곳

    완성된 영상을 재생했다. 자막이 카드 헤드라인 문구 위에 그대로 올라가 있었다. 두 텍스트가 같은 자리에서 겹쳐 보였다. 읽을 수가 없었다.

    원인은 단순했다. 두 요소가 같은 y좌표(화면에서 세로 위치를 나타내는 값)를 가리키고 있었다. agents/videoEditor.ts의 자막 번인(burn-in, 자막을 영상에 직접 새겨넣는 처리) 필터는 자막을 y=(h-text_h)/2로 그리고 있었다. 화면 세로 정중앙이다. agents/evidenceCard.ts의 renderHookCard()는 카드 헤드라인 텍스트를 startY = HEIGHT/2 - blockHeight/2로 배치하고 있었다. 이것도 세로 정중앙이다.

    1080×1920 세로 영상에서 두 요소가 같은 y축을 놓고 부딪힌 거다. 수식만 달랐지 결과는 같은 위치였다. 버그의 원인치고는 웃겼다.

    같은 y좌표를 바라본 두 코드를 찾은 방법

    겹쳐 보인다는 건 알았다. 어느 파일에서 좌표를 건드려야 하는지가 문제였다.

    파이프라인에 파일이 여럿 있었다. 자막을 다루는 코드와 카드를 다루는 코드가 별도 에이전트 파일로 나뉘어 있었다. Claude Code에 상황을 설명하고 어느 파일에서 충돌이 발생하는지 찾아달라고 했다. videoEditor.ts와 evidenceCard.ts 두 파일을 바로 짚어줬다. 각각의 수식을 나란히 보여주니 충돌 구조가 한눈에 보였다.

    카드 위치는 건드리지 않기로 했다. 카드는 화면 중앙에 있어야 시각적으로 자연스럽다. 자막 좌표 버그를 없애려면 움직여야 하는 건 자막 쪽이었다.

    자막 위치를 옮겨 고치기, y=h*0.8-text_h/2의 의미

    videoEditor.ts의 자막 y좌표를 하나만 바꿨다.

    • 수정 전: y=(h-text_h)/2 , 화면 세로 정중앙
    • 수정 후: y=h*0.8-text_h/2 , 화면 세로 80% 지점

    h*0.8은 화면 전체 높이의 80% 위치다. 거기서 텍스트 높이의 절반(text_h/2)을 빼면 자막의 중심선이 정확히 80% 지점에 온다. 화면 아래쪽이다. x좌표(가로 위치)인 x=(w-text_w)/2는 가로 중앙이라 그대로 뒀다.

    수식 자체는 짧다. 이 한 줄이 자막을 화면 중앙에서 화면 하단으로 내려보냈다.

    자막 좌표 버그 수정 후 영상 프레임 ,  자막이 화면 하단에 위치해 카드 헤드라인과 겹치지 않는다
    수정 후 영상 프레임 ,  다른 장면에서도 자막이 카드 텍스트 아래쪽에 안전하게 배치됐다

    수정 후 실제로 뽑아본 영상 프레임 두 장이다. 장면이 바뀌어도 자막은 화면 하단에 고정돼 있고, 카드 헤드라인과 더 이상 겹치지 않는다.

    유료 API를 다시 부르지 않은 이유: 캐시의 힘

    좌표를 고쳤다. 이제 확인해야 한다. 영상을 다시 만들려면 처음부터 전부 돌려야 할까?

    그럴 필요가 없었다. 파이프라인이 이미 중간 산출물(결과물)을 output 폴더에 저장해두고 있었다. narration.mp3(ElevenLabs TTS 결과), script.json(Claude가 생성한 대본), narration.srt(Whisper가 뽑아낸 자막 타임스탬프 파일), 카드 PNG 이미지들 , 전부 있었다.

    임시 검증 스크립트를 하나 만들었다. 이 스크립트는 저장된 파일들만 읽어들이고, SRT(자막 정보와 시간 정보를 담은 텍스트 파일 형식)를 다시 파싱해서 FFmpeg 합성 단계인 assembleVideo와 burnNarrationSubtitles만 로컬에서 재실행했다. API(Application Programming Interface, 외부 서비스와 통신하는 창구) 호출이 하나도 없었다. 몇 초 만에 끝났다. 자막이 카드 텍스트를 더 이상 가리지 않는 걸 확인했다. 스크립트는 확인 후 바로 지웠다.

    전에도 비용을 아끼려고 AI 대신 코드로 썸네일을 자동 생성해서 비용을 0원으로 만든 적이 있다. 이번에도 같은 방향이었다. Claude 대본 생성, ElevenLabs TTS, Whisper 전사 , 이 세 단계는 돈이 나간다. 자막 좌표 수정은 돈이 안 나간다. 비싼 단계를 다시 밟을 이유가 없었다.

    렌더링 파이프라인을 나누면 반복 검증이 빨라진다

    이번 자막 좌표 버그를 고치면서 구조적으로 확인한 게 있다. 파이프라인에서 비용이 드는 단계와 그렇지 않은 단계가 명확히 분리돼 있다는 거다.

    단계 비용 재실행 필요 여부
    대본 생성 (Claude) 유료 API 대본이 바뀔 때만
    TTS 음성 생성 (ElevenLabs) 유료 API 음성이 바뀔 때만
    자막 타임스탬프 추출 (Whisper) 유료 API 음성이 바뀔 때만
    카드 합성, 자막 위치, Ken Burns 렌더링 무료 (로컬) 화면 배치가 바뀔 때마다 자유롭게

    화면 배치만 수정하는 작업이라면 비싼 단계를 건드릴 필요가 없다. 캐시된 중간 산출물만으로 몇 번이고 반복 검증할 수 있다. 자막 좌표를 바꾸거나, 카드 위치를 조정하거나, 렌더링 순서를 바꾸는 작업 모두 마찬가지다.

    파이프라인을 처음 설계할 때 이걸 의도했는지는 솔직히 기억이 잘 안 난다. 결과적으로 잘 나뉘어 있었다. 덕분에 이번 버그 수정이 빨리 끝났다.

    다음에는 자막 스타일 자체를 건드려볼 생각이다. 폰트 크기, 줄바꿈 처리, 글자 그림자 , 이것들도 전부 로컬 렌더링 단계라 캐시만 있으면 자유롭게 반복할 수 있다. 그쪽을 좀 더 다듬어야 영상이 볼 만해진다. 아직 거칠다.

  • 워드프레스 SEO 플러그인 충돌 피하는 법, 켜야 할 것과 꺼야 할 것

    워드프레스 SEO 플러그인 충돌 피하는 법, 켜야 할 것과 꺼야 할 것

    워드프레스 새로 만들면 플러그인 목록 바로 확인하는 사람이 얼마나 될까. 나는 안 했다. 그냥 랭크매스 설치하고 활성화했다가 워드프레스 SEO 플러그인 충돌을 직접 겪었다. 그게 실수였다.

    호스팅사가 몰래 깔아주는 SEO 플러그인, 확인하는 법

    호스팅어(Hostinger)에서 워드프레스를 새로 만들면 플러그인이 이미 몇 개 깔려 있다. 기본 번들이다. 문제는 그 안에 All in One SEO가 활성화된 채로 포함돼 있다는 거다.

    처음엔 몰랐다. 랭크매스를 쓰기로 마음먹고 설치부터 했다. 설치 자체는 문제없이 됐다. 그런데 활성화하고 나서 플러그인 목록을 열어봤더니 — All in One SEO가 이미 켜져 있었다. 두 개가 동시에 활성 상태였다.

    확인 방법은 간단하다. 워드프레스 관리자 화면에서 플러그인 → 설치된 플러그인으로 들어간다. 목록 상단에 ‘전체 | 활성 | 비활성’ 탭이 보인다. 활성 탭을 클릭해서 지금 켜진 플러그인만 따로 보는 게 제일 확실하다. 전체 목록에서 눈으로 훑는 건 놓치기 쉽다.

    워드프레스 새로 설치할 때 SEO 플러그인 충돌 피하는 법 ,  켜야 할 것, 꺼야 할 것 스크린샷 1

    위 화면처럼 활성 플러그인 목록을 확인하면 워드프레스 SEO 플러그인 충돌 여부를 바로 알 수 있다. 자세한 확인 방법은 워드프레스 공식 플러그인 관리 문서에도 나온다. 여기서 두 개 이상 보이면 즉시 하나를 꺼야 한다.

    호스팅어뿐 아니라 다른 호스팅사도 비슷하게 기본 플러그인을 깔아두는 경우가 있다. 새 워드프레스를 만들었다면 플러그인 목록 확인이 첫 번째 할 일이다.

    SEO 플러그인을 두 개 켜면 안 되는 이유 (메타박스, 스키마 마크업 충돌)

    워드프레스 SEO 플러그인 충돌이 실제로 왜 문제인지 구체적으로 알아야 경각심이 생긴다.

    첫 번째 문제는 메타박스(Meta Box, 글 편집 화면 아래쪽에 뜨는 SEO 입력 칸) 중복이다. 글을 쓰다 보면 편집 화면 아래에 SEO 제목, 설명, 키워드를 입력하는 칸이 뜬다. SEO 플러그인이 두 개면 이 칸이 두 세트가 된다. 어느 쪽 설정이 실제로 적용되는지 알 수 없다. 헷갈린다. 별로다.

    두 번째 문제가 더 심각하다. 스키마 마크업(Schema Markup, 구글이 검색 결과에 별점·날짜 같은 정보를 표시할 때 읽는 구조화 데이터) 충돌이다. 두 플러그인이 각자 스키마 마크업을 만들어낸다. 구글 입장에서는 같은 페이지에 두 개의 서로 다른 구조 데이터가 존재하는 셈이다. 어느 걸 믿어야 할지 모른다. 검색 엔진 최적화(SEO)가 목적인데 오히려 역효과다.

    워드프레스 SEO 플러그인 충돌을 막으려면 SEO 플러그인은 하나만 켜야 한다. 예외는 없다.

    All in One SEO 비활성화할 때 자주 실패하는 이유

    비활성화 버튼을 눌렀다. 그런데 안 꺼졌다. 이건 좀 웃겼다.

    나중에 이유를 알았다. All in One SEO를 비활성화하려고 버튼을 클릭한 순간, 화면에 설문 팝업이 떴다. 플러그인을 끄기 전에 이유를 물어보는 팝업이다. 그 팝업이 클릭을 가로채고 있었던 거다. 버튼을 눌렀다고 생각했는데 팝업 영역을 누른 셈이었다.

    팝업이 떠 있으면 반드시 팝업을 먼저 처리해야 한다. 닫든 제출하든 상관없다. 그다음에 비활성화 버튼을 다시 눌러야 실제로 꺼진다.

    비활성화 후 확인도 직접 해야 한다. 플러그인 목록 상단에 이런 식으로 표시된다.

    • 전체 (13) | 활성 (9) | 비활성 (4)

    비활성화 전후로 활성 숫자가 하나 줄었는지 확인하는 게 제일 확실하다. 숫자가 안 줄었으면 비활성화가 실제로 안 된 거다. 다시 해야 한다.

    워드프레스 새로 설치할 때 SEO 플러그인 충돌 피하는 법 ,  켜야 할 것, 꺼야 할 것 스크린샷 2

    비활성화가 제대로 됐으면 플러그인 목록에서 이 플러그인의 링크가 ‘비활성화’가 아니라 ‘활성화’로 바뀌어 있다. 위 화면이 그 상태다 , All in One SEO 옆에 ‘활성화’ 링크가 뜬다는 건 지금 꺼져 있다는 뜻이다.

    호스팅어 기본 플러그인 중 뭘 켜두고 뭘 꺼야 할까?

    호스팅어 기본 번들 플러그인을 전부 쓸 필요는 없다. 실제로 정리해봤다.

    플러그인 이름 처리 이유
    All in One SEO 비활성화 랭크매스를 메인으로 쓰므로 충돌 방지를 위해 꺼야 함
    Hostinger Reach 비활성화 SEO·콘텐츠 작업에 불필요
    Hostinger Tools 비활성화 SEO·콘텐츠 작업에 불필요
    호스팅어 간편한 온보딩 비활성화 초기 설정 이후 불필요
    LiteSpeed Cache 유지 캐시(Cache, 페이지를 미리 저장해 속도를 높이는 기능)로 사이트 속도 향상에 기여
    Site Kit by Google 유지 구글 서치콘솔·애널리틱스 연동에 계속 필요

    정리하면 호스팅어 번들 중 실제로 쓸 것은 LiteSpeed Cache와 Site Kit by Google 두 개다. 나머지는 꺼도 된다.

    충돌 없이 안전하게 설치해도 되는 플러그인 5가지

    SEO 플러그인처럼 서로 충돌하는 플러그인이 있는 반면, 그냥 설치하면 되는 것들도 있다. 직접 설치해서 쓰고 있는 5개다.

    • WP File Manager (비트 파일 관리자) , 워드프레스 관리자 화면에서 직접 서버 파일을 관리할 수 있다. FTP(File Transfer Protocol, 파일을 서버에 올리고 내리는 프로토콜) 없이도 파일을 수정할 수 있어서 편하다.
    • Converter for Media , 업로드하는 이미지를 WebP(웹피, 구글이 만든 이미지 형식으로 용량이 작아 로딩이 빠름) 형식으로 자동 변환해준다. 따로 손댈 게 없다.
    • Head & Footer Code , 헤더와 푸터에 직접 코드를 삽입할 수 있다. 구글 태그 같은 걸 넣을 때 쓴다.
    • Instant Indexing , 글을 발행하면 구글에 즉시 색인 요청을 자동으로 보내준다.
    • 럭키WP 목차 , 글 안에 목차를 자동으로 만들어준다. 긴 글일수록 유용하다.

    이 5개는 SEO 플러그인처럼 같은 기능을 두 개가 동시에 담당하는 구조가 아니다. 설치하고 활성화하면 된다.

    플러그인 활성화 후 반드시 해야 할 확인 작업

    플러그인 설치하고 활성화했다고 끝이 아니다. 확인이 남아 있다.

    워드프레스 새로 설치할 때 SEO 플러그인 충돌 피하는 법 ,  켜야 할 것, 꺼야 할 것 스크린샷 3

    반대로 랭크매스는 옆에 ‘비활성화’ 링크가 뜬다 , 이건 지금 켜져 있다는 뜻이다. 이렇게 SEO 플러그인 중 딱 하나만 ‘비활성화’ 링크가 보이는 상태가 맞는 상태다.

    확인 순서는 이렇다.

    • 활성 탭을 클릭해서 현재 켜진 플러그인 목록만 본다.
    • SEO 플러그인이 하나인지 반드시 확인한다. 두 개 이상이면 하나를 당장 꺼야 한다.
    • 비활성화한 플러그인은 비활성 탭에서 숫자가 늘었는지 확인한다.
    • 새로 설치한 플러그인이 활성 탭에 정상 표시되는지 본다.

    이걸 한 번만 제대로 해두면 나중에 ‘왜 SEO 설정이 안 먹히지?’ 같은 상황을 방지할 수 있다.

    워드프레스 SEO 플러그인 충돌 문제를 해결했으니 이제 랭크매스 초기 설정을 해야 한다. 설치만 하면 자동으로 최적화되는 게 아니다. 랭크매스 설정 마법사에서 직접 사이트 정보를 입력해야 제대로 작동한다. 포커스 키워드가 REST로 안 먹힐 때 직접 넣는 방법은 이전 글에 정리해뒀다.

    같은 워드프레스 주제로 이어서 읽으면 좋은 글: WPCode로 보안 헤더 추가하다 코드가 깨진 이유, 대표 이미지가 잘리는 이유 , 템플릿 비율 3:2→16:9 차이, 워드프레스 SEO 버그 5개, curl로 찾은 공통 원인.

  • 데몬 버전 불일치로 CLI가 안 됐던 이야기 (v1.8.7→1.8.8)

    데몬 버전 불일치로 CLI가 안 됐던 이야기 (v1.8.7→1.8.8)

    인스타그램 릴스를 보다가 CLI(Command Line Interface, 명령어로 프로그램을 조작하는 도구) 하나를 알게 됐다. 근데 알고 보니 이미 설치돼 있었다. 업데이트하다가는 데몬 버전 불일치라는 문제를 만났다. 데몬(daemon, 백그라운드에서 몰래 돌아가는 상주 프로세스)이 CLI 프로그램과 서로 다른 버전으로 어긋난 상태였다.

    이 글은 그 과정을 그대로 기록한 것이다.

    인스타 릴스에서 본 CLI 도구, 이미 깔려 있었다

    릴스 하나가 올라왔다. 유튜브, 인스타그램, 트위터, GitHub, 링크드인을 CLI 하나로 읽을 수 있다는 내용이었다. 유료 API 키(API key, 유료 서비스를 이용할 때 발급받는 인증 열쇠) 없이 돌아간다고 했고, 레딧이나 빌리빌리처럼 막히던 곳도 지원한다고 했다.

    릴스 화면에 GitHub 레포(repository, 코드를 저장하는 폴더) 이름이 잠깐 스쳤다. Panniantong/Agent-Reach였고, 별(star, GitHub에서 좋아요 같은 개념) 85.5k, 포크 7.5k라고 적혀 있었다.

    GitHub API로 직접 조회했다. 별 85,710개, 포크 7,537개, MIT 라이선스. 릴스 숫자와 거의 일치했다. 설명은 “에이전트에게 인터넷 전체를 보는 눈을 준다”였다. 저장소 설명과 똑같았다.

    설치하려고 했는데 이미 깔려 있었다. agent-reach라는 이름으로 스킬이 존재했고, 버전은 v1.5.0이었다. 릴스를 보기 전까지는 이 도구가 정확히 뭘 하는지 몰랐다. 이건 좀 웃겼다.

    CLI는 최신인데 왜 자꾸 구버전이라고 나올까?

    도구가 실제로 작동하는지 확인하기 위해 agent-reach doctor를 돌렸다. doctor 명령은 각 플랫폼 연결 상태를 한 번에 점검해주는 진단 도구다.

    결과는 이랬다. 유튜브와 빌리빌리는 바로 작동했다. 트위터, 레딧, 인스타그램, 페이스북, 샤오홍슈는 브라우저 확장인 OpenCLI 파일은 있는데 연결됐는지 확인이 안 된 상태, 즉 warn(경고)으로 나왔다.

    이유가 있었다. agent-reach 쪽 점검 도구는 일부러 브라우저 확장 상태를 자동으로 확인하지 않는다. 브라우저 쿠키를 건드릴 수 있어서다. 그래서 opencli doctor라는 별도 명령을 직접 실행해야 한다.

    돌려봤다. 데몬 실행 중, 확장 연결됨, 연결 확인 완료. 5.8초 만에 전부 통과했다.

    인스타 릴스에서 본 CLI, 알고 보니 이미 깔려 있었다 — 업데이트하다 만난 데몬 버전 불일치 스크린샷 1

    위 화면이 opencli doctor가 통과된 직후 상태다. 그런데 화면 끝에 업데이트 안내가 떴다. v1.8.7에서 v1.8.8로 올리라는 내용이었다.

    업데이트 명령은 간단했다.

    npm install -g @jackwener/opencli

    설치가 끝나고 opencli --version을 확인했다. 1.8.8로 바뀌어 있었다. 여기까지는 문제없었다.

    그런데 다시 opencli doctor를 돌렸더니 데몬은 여전히 v1.8.7이라고 나왔다. CLI 프로그램 자체는 업데이트됐는데, 백그라운드에서 계속 떠 있던 데몬 프로세스는 예전 버전 그대로 살아있던 거다.

    인스타 릴스에서 본 CLI, 알고 보니 이미 깔려 있었다 ,  업데이트하다 만난 데몬 버전 불일치 스크린샷 2

    위 화면이 그 상태다. CLI는 1.8.8, 데몬은 1.8.7. 버전이 다르다.

    Stale Daemon의 정의와 문제가 되는 이유

    opencli doctor는 이 상황을 정확히 짚었다. “stale daemon”이라는 표현으로.

    stale은 “오래돼서 유효하지 않은”이라는 뜻이다. stale daemon(낡은 데몬, 업데이트 이후에도 종료되지 않고 구버전 상태로 계속 떠 있는 백그라운드 프로세스)이 문제가 되는 이유는 단순하다. CLI가 데몬에게 명령을 보낼 때, 데몬이 구버전이면 새 기능이나 수정된 동작을 처리하지 못한다. 버전이 다르면 의사소통이 제대로 안 된다고 보면 된다.

    npm으로 패키지를 업데이트하면 파일은 교체된다. 그런데 이미 메모리에 올라가서 돌아가고 있는 데몬 프로세스는 파일을 바꿨다고 자동으로 재시작되지 않는다. 직접 껐다 켜야 한다.

    doctor가 이걸 감지하고 해결책까지 알려줬다. opencli daemon restart를 실행하라고.

    데몬 버전 불일치, 재시작으로 해결하기

    opencli daemon restart를 실행했다. 먼저 경고가 떴다. 브라우저 프로필 연결이 끊긴다는 내용이었다. 확장이 자동으로 다시 붙을 거라고 했다.

    데몬은 포트 19825에서 v1.8.8로 재시작됐다. 그 순간에는 확장이 아직 연결 안 된 상태로 표시됐다. 5초 기다렸다. 그리고 다시 opencli doctor를 돌렸다.

    데몬 v1.8.8, 확장 연결됨, 연결 확인 완료. 데몬 버전 불일치가 풀리고 전부 정상이었다.

    인스타 릴스에서 본 CLI, 알고 보니 이미 깔려 있었다 ,  업데이트하다 만난 데몬 버전 불일치 스크린샷 3

    위 화면이 재시작 후 정상 상태다. 재시작 직후에 바로 doctor를 돌리면 아직 확장이 붙기 전이라 실패로 나올 수 있다. 5초 정도는 기다려야 한다.

    그리고 별개 문제가 하나 더 있었다. GitHub 쪽은 gh CLI(GitHub 공식 명령줄 도구)는 설치돼 있는데 로그인이 안 된 상태였다. gh auth status를 돌려보니 어떤 GitHub 계정에도 로그인돼 있지 않다고 나왔다. 이건 내가 대신 처리할 수 없다. 사용자가 직접 gh auth login을 실행해야 한다.

    CLI 업데이트할 때 꼭 확인해야 할 것들

    데몬 버전 불일치를 겪고 나서 정리한 확인 목록이다. 짧게 정리한다.

    • 업데이트 후 반드시 버전을 두 군데 다 확인해라. CLI 버전(opencli --version)만 보지 말고, 데몬 버전도 doctor 명령으로 따로 확인해야 한다.
    • stale daemon 경고가 뜨면 바로 재시작해라. 무시하면 CLI는 최신인데 실제 동작은 구버전 그대로인 상태가 된다.
    • 데몬 재시작 직후 바로 doctor를 돌리지 마라. 확장이 다시 붙는 데 수 초가 걸린다. 5초 이상 기다리고 확인해야 한다.
    • doctor 결과에서 warn은 그냥 두지 마라. warn은 파일은 있는데 연결이 확인 안 된 상태다. 별도 명령으로 직접 확인해야 한다.
    • 데몬을 재시작하면 브라우저 프로필 연결이 끊긴다. 자동으로 다시 붙지만, 연결이 필요한 작업 중이라면 타이밍을 골라야 한다.
    • gh auth login은 직접 해야 한다. CLI 도구가 대신 처리해줄 수 없다. GitHub 연동이 필요하면 이 단계를 빠뜨리지 마라.

    이 CLI는 opencli 공식 저장소에서 소스를 볼 수 있다. 도구를 설치했다가 이미 깔려 있던 걸 알게 된 경험은 MCP 도구를 찾다가 이미 설치돼 있었던 글에도 비슷하게 적어뒀다.

    다음에 할 일은 agent-reach v1.5.0이 실제로 뭘 할 수 있는지 직접 써보는 것이다. doctor 점검만 했고 아직 실제로 콘텐츠를 읽어보지 않았다. 트위터와 레딧이 연결 확인 상태에서 실제로 작동하는지가 궁금하다. 별 85k짜리 도구가 말한 대로 되는지는 써봐야 안다.

  • CLAUDE.md 정리하다 발견한 중복 지침

    CLAUDE.md 정리하다 발견한 중복 지침

    CLAUDE.md 정리를 한 번도 안 했다면 점검해볼 만하다. Claude Code(AI 코딩 도우미)를 쓰다 보면 어느 순간 CLAUDE.md 파일이 여러 개가 된다. 내 환경에는 세 개가 있었다. 그냥 두면 되는 줄 알았는데 점검해보니 아니었다.

    CLAUDE.md가 뭐고 왜 여러 개일까?

    CLAUDE.md는 Claude Code가 세션(session, 작업을 시작하고 끝내는 한 번의 대화 구간)을 시작할 때 자동으로 읽는 설정 파일이다. “나는 이렇게 일하는 사람이야, 이 규칙을 따라줘”라고 클로드에게 미리 알려두는 메모다.

    문제는 이 파일이 한 군데만 있는 게 아니라는 점이다. Claude Code는 공식 문서에 나온 것처럼 여러 위치에서 CLAUDE.md를 찾는다. 홈 폴더(~/CLAUDE.md), 전역 설정 폴더(~/.claude/CLAUDE.md), 그리고 각 프로젝트 폴더 안의 CLAUDE.md까지.

    내 세 파일은 ~/CLAUDE.md, ~/.claude/CLAUDE.md, 자동화 프로젝트 폴더의 CLAUDE.md였다. 그래서 클로드에게 직접 점검을 시켰다. 파일 내용을 실제 폴더와 대조해서 읽고, 수정하거나 지워도 되는 부분이 있으면 알려달라고 했다.

    CLAUDE.md 정리 전 점검: 세 파일에서 나온 문제

    세 파일 모두 문제가 있었다. 종류는 달랐다.

    ~/CLAUDE.md는 131줄짜리 파일이었다. 클로드가 읽고 나서 이상한 걸 발견했다. 1번 줄과 67번 줄에 똑같이 # CLAUDE.md라는 제목이 있었다. 1~65줄과 67~131줄을 비교했더니 완전히 같은 내용이었다. 같은 지침 블록이 두 번 들어가 있었다는 뜻이다.

    CLAUDE.md에 같은 지침이 두 번 들어 있었다 — 3개 파일 점검하고 정리한 후기 스크린샷 1

    위는 정리 전 131줄, 정리 후 65줄과 두 블록 비교 결과를 찍은 화면이다. 왜 두 번 들어갔는지 원인은 지금도 모른다.

    자동화 프로젝트 CLAUDE.md에는 다른 문제가 있었다. workflows/ 폴더를 전제로 한 규칙이 세 곳에 흩어져 있었다. “Workflow는 내 허락 없이 새로 만들거나 삭제하지 마”, 에러 처리 절차의 ‘Workflow 업데이트’, 보안 절의 ‘Workflow’ 표현. 그런데 workflows/ 폴더가 실제로 없었다. 폴더는 사라졌는데 규칙은 그대로 남아 있었다.

    CLAUDE.md 정리 중 만난 NotebookLM 지시 문제

    ~/.claude/CLAUDE.md는 또 다른 문제였다. 세션 시작 때 NotebookLM(구글의 AI 노트 정리 도구) 노트북을 먼저 검색하라고 되어 있었다. 그런데 NotebookLM 연결은 로그인 감지 버그와 구글 장애로 2026-09-01부터 막혀 있었다. 매 세션마다 실패할 게 뻔한 명령이 설정에 그냥 살아 있었던 거다.

    CLAUDE.md의 NotebookLM 검색 지시를 보류 중으로 바꾼 변경 내용

    위는 전역 CLAUDE.md의 NotebookLM 지시를 고친 변경 부분이다.

    그 외에 자동화 프로젝트 CLAUDE.md에 ‘실제 사용 중인 블로그는 tistory 1개뿐’이라고 적혀 있었다. 지금 이 블로그, ujinify.com이 훨씬 더 활발히 쓰는 프로젝트인데도 설정에는 반영이 안 되어 있었다.

    CLAUDE.md 정리 첫 단계: 중복 찾는 쉘 명령어

    쉘(shell, 명령어를 입력해서 컴퓨터에게 일을 시키는 창)에서 두 가지 명령어로 확인할 수 있다.

    먼저 파일이 몇 줄인지 센다.

    • wc -l ~/CLAUDE.md , 줄 수를 보여준다. wc는 ‘word count’의 줄임말이다.
    • grep -n '^# ' ~/CLAUDE.md , 파일 안에서 #으로 시작하는 제목 줄을 찾아서 몇 번째 줄인지 함께 보여준다. grep은 파일 안에서 특정 단어나 패턴을 찾는 명령어다.

    내 경우에는 grep 결과에 # CLAUDE.md가 1번 줄과 67번 줄, 두 군데 나왔다. 같은 제목이 두 번 뜨면 중복을 의심해야 한다. 그다음엔 두 블록의 내용이 실제로 같은지 확인해야 한다.

    앞 블록과 뒤 블록을 비교하는 짧은 스크립트(script, 명령어를 순서대로 묶어둔 파일)를 돌렸다. 완전히 같다는 결과가 나왔고, 그래서 뒤 블록을 지웠다.

    자기 파일에서 확인하려면 이 순서로 해보면 된다. wc -l로 줄 수 확인 → grep -n '^# '로 제목 위치 확인 → 같은 제목이 두 군데 이상 있으면 두 블록 내용 비교. 간단하다.

    CLAUDE.md 정리에서 없는 폴더 규칙 지우기

    workflows/ 폴더가 실제로 없다는 걸 어떻게 확인했냐고 하면, ls 명령어로 봤다. ls는 폴더 안에 뭐가 있는지 목록으로 보여주는 명령어다. 없으면 없다고 나온다.

    있어야 할 폴더가 없으면 그 폴더를 전제로 한 규칙은 전부 의미가 없다. 클로드가 “Workflow 삭제 전 허락을 받아야 한다”는 규칙을 읽어도, 지킬 대상이 없다. 그냥 노이즈다.

    workflows 폴더가 없는데 CLAUDE.md에 Workflow 규칙이 남아 있던 확인 결과

    위는 workflows 폴더가 없다는 결과와 정리 전후 ‘Workflow’ 문구 검색 결과다.

    수정 전에 파일을 /tmp에 백업했다. /tmp는 임시 파일을 저장하는 폴더다. 백업 없이 수정하지 마라. 잘못 지우면 돌아올 방법이 없다.

    백업 뒤에 workflows/ 관련 문구를 세 곳 모두 지웠다. 113줄이던 파일이 106줄이 됐다.

    파일 구조를 설명하는 절도 실제와 달랐다. tools/ 폴더에 파이썬(.py)과 타입스크립트(.ts) 파일이 섞여 있고, 새 코드는 타입스크립트로 작성된다는 실제 구조로 고쳤다. ujinify를 활성 블로그 목록에 올리고, 폐기된 프로젝트 목록은 4줄로 줄였다. 보관 방침은 그대로 뒀다.

    실제 안 쓰는 도구 지시는 지우기로 했다

    NotebookLM 지시는 지울지 남길지 갈렸다. 연결이 복구되면 다시 켜야 해서, 재개 규칙까지 통째로 지우면 나중에 처음부터 다시 써야 한다.

    그래서 지우지 않고 ‘보류 중’으로 바꿨다. 지시 자체는 남겨두고, 현재 상태를 주석처럼 표시한 거다. 연결이 복구되면 ‘보류 중’ 표시만 지우면 된다. 재개 규칙은 그대로 살아 있다.

    이건 판단의 문제다. 완전히 폐기된 도구라면 지우는 게 맞다. 일시 중단 상태라면 보류 표시를 남겨두는 쪽이 낫다. 어느 쪽이든 원칙은 하나다. 지금 작동하지 않는 도구에 대한 지시가 설정에 살아 있으면 클로드는 매 세션마다 실패할 명령을 실행하려 한다. 그 부분만큼은 반드시 정리해야 한다.

    ujinify 전용 CLAUDE.md를 새로 만든 이유

    점검하면서 ujinify 블로그 파이프라인 폴더에 전용 CLAUDE.md를 새로 만들기도 했다. 여기저기 흩어져 있던 규칙들, 글 내용 규칙, 이미지 캡처 규칙, 랭크매스 점수 규칙, 릴스·링크 처리 순서를 한 파일에 모았다.

    알려진 문제도 적었다. 이미지 캡션과 사실을 지어내는 문제, 랭크매스 포커스 키워드가 REST로 저장되지 않는 문제. 고친 게 아니라 기록한 거다. 기록해두지 않으면 클로드도 나도 같은 실수를 반복한다.

    정리 후 나한테 달라진 점, 너도 확인해보는 방법

    솔직하게 말하겠다. CLAUDE.md 정리 후 확인한 건 줄 수뿐이다. ~/CLAUDE.md가 131줄에서 65줄이 됐다. 프로젝트 CLAUDE.md가 113줄에서 106줄이 됐다. 그게 다다.

    응답이 빨라졌는지, 컨텍스트가 몇 토큰 줄었는지는 측정하지 않았다. 그래서 효과는 “읽는 분량이 줄었다”까지만 말할 수 있다. 더 좋아졌다고 단정하지 않겠다.

    내 환경의 CLAUDE.md 정리 상태 확인하는 법

    자기 환경에서 확인하고 싶다면 세 가지만 해보면 된다.

    • wc -l로 CLAUDE.md 줄 수를 세고, grep -n '^# '로 같은 제목이 반복되는지 확인한다.
    • CLAUDE.md 안에 언급된 폴더 이름을 찾아서 ls로 실제로 있는지 확인한다.
    • 세션 시작 때 실행하라고 적어둔 도구가 지금도 작동하는지 직접 돌려본다.

    나는 이 세 가지에서 각각 문제가 나왔다. 처음 점검해보는 사람이라면 하나쯤은 걸릴 거라고 본다. 오래된 설정일수록 더 그렇다.

    스킬과 메모리 폴더를 정리한 이야기는 Claude Code 스킬 정리 후기에 적어뒀다.

    다음엔 새로 만든 ujinify 전용 CLAUDE.md가 실제로 잘 작동하는지 확인할 차례다. 규칙을 모아두는 것과 클로드가 그 규칙을 제대로 따르는 것은 다른 얘기다. 그 부분은 좀 더 써봐야 안다.

  • 랭크매스 포커스 키워드, wp.data로 직접 넣는 법

    랭크매스 포커스 키워드, wp.data로 직접 넣는 법

    ujinify.com 업로드 스크립트가 REST API(Application Programming Interface, 외부에서 워드프레스 데이터를 주고받는 통로)로 랭크매스 포커스 키워드를 보내고 있었다. 응답도 200이 왔다. 에러도 없었다. 그런데 글을 열어보면 포커스 키워드 칸이 비어 있다.

    조용히 무시당한 거다. 한동안 이걸 몰랐고, 그동안은 글마다 에디터를 열어서 손으로 키워드를 입력했다. 글이 늘수록 이게 제일 귀찮은 수작업이었다.

    이 글은 그 문제를 해결한 방법을 정리한다. wp.data라는 워드프레스 내부 저장소(앱 안에서 데이터를 관리하는 공간)를 직접 건드리는 방식이다.

    REST API가 랭크매스 포커스 키워드를 무시하는 이유

    REST API로 글을 저장할 때 meta 필드에 rank_math_focus_keyword를 넣으면 된다고 생각했다. 실제로 그렇게 했다. 그리고 저장은 됐다. 응답 코드도 200이었다.

    랭크매스가 이 값을 받아도 포커스 키워드로 반영하지 않는다. 에러를 주지도 않는다. 그냥 없는 것처럼 동작한다.

    정확한 내부 원인까지는 확인하지 못했다. 확인한 건 하나다. 랭크매스의 점수 계산 엔진(SEO 점수를 계산하는 프로그램)이 브라우저 자바스크립트(JavaScript, 웹 브라우저에서 실행되는 프로그래밍 언어)로 돌아가서, 에디터 화면을 열었을 때만 점수가 계산된다. 키워드도 그 화면 안에서 넣으면 들어갔다.

    스크립트가 이걸 감지해서 경고는 띄워주게 됐지만, 근본 해결은 아니었다. 에디터를 열고 직접 넣는 수밖에 없었다. 그러다 자동화할 방법을 찾기 시작했다.

    wp.data 저장소에서 랭크매스 액션 찾기

    wp-admin(워드프레스 관리자 화면) 글 에디터를 열어둔 상태에서 시작했다.

    이 글의 코드는 브라우저 개발자 도구 콘솔(Console, 코드를 직접 실행할 수 있는 창)에 그대로 붙여 넣어도 돌아가는 코드다. 나는 같은 코드를 Playwright(브라우저 자동 조종 도구)로 실행했다. 워드프레스 블록 에디터는 wp.data라는 내부 저장소 시스템을 쓴다. 워드프레스 공식 문서에도 나오는 방식이다. 플러그인도 여기에 자기 저장소를 등록할 수 있다. 랭크매스도 그렇게 했다.

    콘솔에 이렇게 입력하면 랭크매스 저장소에 접근할 수 있다.

    wp.data.select('rank-math')

    그리고 액션(Action, 저장소에 변경을 가하는 명령)을 확인하려면 이렇게 입력한다.

    wp.data.dispatch('rank-math')

    여기서 액션이 87개나 나왔다. 이름을 하나씩 보다가 바로 눈에 띄는 게 있었다. updateKeywords, updateDescription, updateSerpDescription, updateTitle. 이름만 봐도 뭘 하는지 명확했다.

    아래 스크린샷은 이 방식으로 값을 넣은 글의 에디터 화면이다. 오른쪽 랭크매스 패널에 포커스 키워드와 점수(78/100)가 들어가 있다.

    랭크매스 포커스 키워드가 REST로 안 들어갈 때, wp.data로 직접 넣는 법 스크린샷 1

    액션 이름은 랭크매스 버전에 따라 바뀔 수 있다. 지금 여기서 나열하는 이름들은 현재 ujinify.com에 설치된 랭크매스에서 직접 확인한 것들이다. 버전이 다르면 이름이 달라질 수 있으니 반드시 직접 확인해야 한다.

    updateKeywords로 포커스 키워드 직접 입력하기

    확인했으니 바로 써봤다. 콘솔에서 이렇게 입력했다.

    wp.data.dispatch('rank-math').updateKeywords('세션 ID')

    포커스 키워드 칸에 바로 들어갔다. 4초쯤 기다렸더니 랭크매스 점수가 22점에서 71점으로 뛰었다.

    이게 맞는 방법이었다. REST API를 우회해서 에디터가 직접 받아서 처리하는 경로로 넣는 거다.

    저장까지 마치려면 core/editor(워드프레스 에디터의 핵심 저장소)의 savePost()를 써야 한다. 비동기 함수라서 await를 붙여서 호출했다.

    await wp.data.dispatch('core/editor').savePost()

    내가 쓴 순서는 이렇다. updateKeywords를 먼저 호출하고, 점수 갱신을 잠깐 기다린 뒤, savePost()로 저장했다.

    updateDescription, updateTitle로 다른 메타데이터 채우기

    포커스 키워드만 넣었는데 점수가 71점이었다. 메타 설명(검색 결과에 표시되는 글 요약문)이 비어 있었다.

    updateDescription으로 메타 설명을 넣었더니 73점이 됐다.

    wp.data.dispatch('rank-math').updateDescription('여기에 메타 설명 텍스트를 넣는다')

    같은 방식으로 SEO 제목도 바꿀 수 있다.

    wp.data.dispatch('rank-math').updateTitle('여기에 SEO 제목을 넣는다')

    updateSerpDescription도 있었는데, SERP에 표시되는 설명을 따로 관리하는 액션이다. 저장소에 이름이 있으니 확인해두면 된다.

    이후 본문 첫 10% 안에 키워드가 없다는 항목이 걸려 있어서 첫 문단에 문장을 하나 추가했다. 긴 문단을 쪼개고, 제목이 키워드로 시작하게 순서를 바꿨다. 첫 번째 글은 최종 76점까지 올라갔다. 두 번째 글은 70점에서 시작해서 본문에 실제 캡처 이미지 2장을 추가했더니 78점이 됐다.

    아래 스크린샷은 랭크매스 패널의 항목 목록이다. 통과 항목은 초록, 감점 항목은 빨강이다. URL 항목이 빨간색으로 남아 있다.

    랭크매스 포커스 키워드가 REST로 안 들어갈 때, wp.data로 직접 넣는 법 스크린샷 2

    점수 갱신이 늦거나 예전 값이 나올 때 확인법

    헷갈리는 지점이 있었다. window.rankMathEditor.resultManager.getResults()로 테스트 통과 여부를 읽었는데, 키워드가 제목에도 본문에도 분명히 있는데 전부 false로 나왔다.

    화면 항목 목록에는 ‘제목에 포커스 키워드를 사용하고 있습니다’가 통과로 표시돼 있었다. 이 값은 갱신이 늦거나 예전 스냅샷(특정 시점의 데이터 복사본)을 돌려주는 거였다. 그래서 getResults() 반환값은 신뢰하지 않기로 했다. 화면에 보이는 항목 목록을 기준으로 삼는다.

    또 하나. 통과(has: true)로 표시돼도 점수가 다 들어오지 않는 항목이 있다. 이미지 항목은 1/6점, 본문 길이는 2/8점처럼 부분 점수로 처리되는 경우가 있었다. 점수가 기대보다 낮게 나오면 이걸 먼저 확인해야 한다. 통과 여부만 보고 끝내면 안 된다.

    화면에 뜬 점수와 데이터베이스에 저장된 점수가 다를 수도 있다. 저장 후에는 REST API로 get-post-seo-meta를 조회해서 DB에 실제로 반영된 점수를 따로 확인해야 한다. 이 단계를 빼면 안 된다. 화면 숫자만 믿다가 나중에 데이터가 다른 걸 발견하면 그때는 더 번거롭다.

    이 방법의 한계와 실제 적용 시 주의점

    이 방법에는 명확한 한계가 있다. 숨기지 않겠다.

    • 브라우저 에디터를 열어야 한다. REST API나 MCP(Model Context Protocol, AI 도구가 외부 서비스에 명령을 보내는 통신 방식)만으로는 동작하지 않는다. 점수 계산 엔진이 브라우저 자바스크립트라서, 에디터 화면이 열려 있어야 실행된다.
    • 액션 이름이 버전마다 다를 수 있다. updateKeywords, updateDescription 같은 이름은 현재 내 사이트 랭크매스에서 직접 확인한 것들이다. 업데이트 후에 이름이 바뀌면 동작하지 않는다. 사용 전에 반드시 wp.data.dispatch('rank-math')를 콘솔에서 직접 실행해서 목록을 확인해라.
    • 저장 후 DB 값을 반드시 확인해야 한다. 화면 점수와 실제 DB에 저장된 점수가 다를 수 있다. REST API로 get-post-seo-meta를 조회하는 단계를 절대 생략하지 마라.
    • 올릴 수 없는 점수가 존재한다. URL에 한국어 키워드가 없는 항목(5점), 유료 Content AI 항목(5점)은 구조적으로 올리기 어렵거나 올리지 않을 항목이다. 이 점수를 제외하면 실제 달성 가능한 최대치는 처음부터 그만큼 낮다.

    수작업으로 키워드를 하나씩 입력하는 것보다는 낫다. 하지만 완전 자동화는 아니다. 에디터 화면을 열고 콘솔에서 코드를 실행하는 단계는 여전히 남는다.

    지금은 이 정도로 쓰고 있다.

    랭크매스 점수가 왜 에디터를 열어야 계산되는지는 랭크매스 SEO 16점→76점 글에, 같은 결의 버그 다섯 개는 워드프레스 SEO 버그 5개 총정리에 정리해뒀다.

    같은 랭크매스 주제로 이어서 읽으면 좋은 글: 랭크매스 무료 기능으로 고아 포스트 2개 찾아낸 이야기, 메타 설명 오류 — 색인됐는데 검색 안 되던 이유.

  • Claude 5시간 한도 72% 찍힌 이유는 세션 길이였다

    Claude 5시간 한도 72% 찍힌 이유는 세션 길이였다

    블로그 예약 발행 캡처 규칙을 막 정리했을 때였다. 뭔가 마무리된 느낌이 들어서 Claude 사용량 한도를 열어봤다.

    5시간 한도 72%. 주간 한도 50%.

    잠깐, 이게 벌써?

    사용량 계기판에서 72%를 발견한 순간

    Claude Code에는 사용량 계기판(usage dashboard, 내가 얼마나 썼는지 보여주는 화면)이 있다. 5시간 단위로 사용량을 제한하는 방식인데, 확인했더니 이미 72%가 찍혀 있었다.

    이해가 안 됐다. 딱히 엄청난 작업을 한 것도 아니었다. 블로그 글 몇 편 만들고, 이것저것 설정 잡은 게 전부였다. 그런데 72%라니.

    그래서 Claude한테 직접 물어봤다. 왜 이렇게 됐냐고.

    돌아온 답이 좀 뜻밖이었다.

    Claude 사용량 한도, 세션을 208MB까지 키운 게 진짜 범인이었다

    세션(session, 대화창 하나를 열고 나서 닫기 전까지의 전체 대화 묶음)이 208MB, 줄 수로는 23,829줄까지 쌓여 있었다.

    하루 종일 같은 대화창을 끊지 않고 계속 이어서 썼다. 작업이 바뀌어도 그냥 계속 입력했다. 새 창을 열 생각을 못 했다.

    문제는 Claude가 대화하는 방식에 있다. 매 턴(turn, 내가 한 번 입력하고 Claude가 한 번 답하는 한 쌍)마다 지금까지 나눈 대화 전체를 처음부터 다시 읽는다. 토큰(token, AI가 글을 읽고 쓸 때 세는 단위. 대략 글자 두세 개 분량)으로 환산해서 전부 처리한다는 뜻이다.

    세션 초반에는 괜찮다. 대화가 짧으니까. 그런데 세션이 쌓일수록, 한마디 할 때마다 처리해야 하는 양이 눈덩이처럼 불어난다. 나는 그 세션을 208MB까지 키웠다. 한마디 할 때마다 208MB짜리 대화를 통째로 들고 시작한 셈이다.

    Claude 사용량 한도 초과 원인을 직접 확인한 터미널 화면 — 세션 파일 wc -l 23829줄, du -h 208M, grep -c image 1420건

    직접 명령을 돌려서 확인한 숫자다. 줄 수 23,829줄, 용량 208MB, 그 안에 이미지가 들어간 메시지만 1,420건. 이게 근본 원인이었다.

    이미지 토큰(페이지 스냅샷)은 텍스트보다 훨씬 비싸다

    두 번째 원인은 Playwright(플레이라이트, 브라우저를 코드로 자동 조작하는 도구)였다.

    랭크매스 점수를 확인하려고 워드프레스 편집기를 열었다 닫았다를 반복했다. 포스트 하나 점수 보는 데 편집기를 6번 넘게 열었다.

    문제는 브라우저를 열 때마다 딸려오는 것이 있다는 점이다. 페이지 스냅샷이 자동으로 첨부된다. Claude가 화면 상태를 파악하려면 이게 필요하다.

    이미지 토큰은 텍스트 토큰보다 훨씬 무겁다. 글자 몇 백 개짜리 텍스트와, 화면 전체 이미지 한 장은 비교가 안 된다. 그걸 6번 넘게 반복했다. 거기에 208MB짜리 세션 문맥까지 매번 붙었다.

    단순 계산이다. 비쌀 수밖에 없었다.

    이 글을 올리는 과정에서도 똑같은 일이 반복됐다. REST API로 랭크매스 키워드를 저장했는데, 응답을 확인해보니 값이 안 찍혀 있었다. 저장이 씹힌 거다. 결국 편집기를 다시 열어서 UI에 직접 입력하고 저장 버튼을 눌러야 했다.

    Claude 사용량 한도와 관련된 랭크매스 REST 메타 저장 씹힘을 직접 확인하고 에디터 UI로 우회한 터미널 화면

    REST로 넣은 값은 저장 응답에 안 찍히는데, 편집기에서 직접 입력하고 저장하면 “글 업데이트 되었습니다” 토스트가 뜨고 새로고침해도 유지됐다. 편집기를 또 열어야 했다는 뜻이다. 방금 원인으로 지목한 그 습관을, 그 글을 쓰면서 또 반복한 셈이다.

    Opus 모델을 너무 많이 쓴 것도 한몫했지만

    세 번째는 모델 선택이었다. 그날 모델을 여러 번 바꿨는데, Opus(오퍼스, Claude 모델 중 가장 고성능이지만 가장 무거운 버전)를 꽤 자주 썼다.

    Opus는 Sonnet(소네트, Opus보다 가볍고 빠른 중간 성능 모델)보다 5시간 한도를 훨씬 빨리 깎아먹는다. 같은 작업을 해도 Opus로 하면 한도 소모가 더 크다.

    이것도 분명히 한몫했다. 하지만 솔직히 말하면, 세션 길이 앞에서는 부차적인 문제였다. 세션이 짧았다면 Opus를 좀 썼어도 이 정도는 아니었을 것이다.

    블로그 생성 파이프라인은 별개였다, 한도 안 깎았음

    처음엔 블로그 글 여러 편 만든 게 원인이라고 생각했다. 틀렸다.

    블로그 글을 만드는 파이프라인은 API 키로 따로 돌아간다. 종량제(사용한 만큼만 별도로 돈 내는 방식) 결제라서 Claude Code의 5시간 사용량 한도와는 완전히 별개다.

    글 열 편을 만들어도 이 계기판 숫자는 안 움직인다. 계기판에 찍힌 72%는 순전히 대화 자체에서 나왔다. 파이프라인 탓이 아니었다.

    이건 좀 중요한 포인트다. 자동화 작업과 대화 사용량은 별도로 추적해야 한다. 같은 숫자로 묶어서 보면 원인을 잘못 짚는다.

    지금부터 /clear로 세션을 끝내기로 결심한 이유

    해결책은 단순하다. /clear를 쓰면 된다.

    /clear는 Claude Code에서 현재 세션을 완전히 초기화하는 명령어다. 대화 기록을 싹 지우고 새 세션으로 시작한다. 208MB짜리 짐을 내려놓고 가볍게 다시 출발하는 것이다. 공식 문서에도 세션이 길어질수록 토큰 사용량이 늘어난다고 나와 있다.

    왜 지금까지 안 썼냐면, 솔직히 필요성을 몰랐다. 대화가 길어지면 Claude가 문맥을 더 잘 기억하니까 오히려 좋은 거 아닌가 싶었다. 착각이었다.

    문맥이 길어지면 Claude가 더 많이 읽어야 한다. 비용이 는다. 속도도 느려진다. 문맥을 오래 유지하는 건 장점만 있는 게 아니다.

    사용량 한도는 어디서 확인하나

    Claude Code에서 /usage를 입력하면 사용량 화면이 열린다. 여기서 Current session의 사용률과 리셋 시각을 볼 수 있다. Current week(모든 모델)의 사용률과 리셋 시각도 함께 나온다.

    나는 이 화면에서 사용률이 17%, 28%, 36%로 올라가는 것을 직접 확인했다. 화면 아래에는 사용량이 많았던 이유도 나온다. 내 경우에는 150k 넘는 긴 문맥이 가장 큰 비중이었고, Playwright 같은 MCP 서버도 13~16%를 차지했다고 나왔다.

    사용량 초기화는 언제 되나

    /usage 화면에는 Current session 옆에 리셋 시각이 찍힌다. 내 계정에서는 Asia/Tokyo 기준 11:19pm이었다.

    같은 화면을 두 번 열어봤다. 두 번째는 몇 분 뒤였고, 사이에 대화를 몇 번 주고받았다. 그래도 사용률 17%와 리셋 11:19pm이 그대로였다. 리셋 시각이 메시지를 보낼 때마다 밀리지는 않았다.

    첫 사용 시점과 맞춰 보면

    이 세션 기록에서 오늘 첫 활동은 6시 25분 무렵이다. 화면의 리셋 11:19pm에서 5시간을 빼면 6시 19분이라 6분 차이가 난다. 같은 계정으로 열어 둔 다른 창이 6시 19분쯤 먼저 쓰였을 수도 있다. 그래서 “처음 사용한 시점부터 5시간”이라고 단정하지는 않는다.

    확인하지 못한 것도 있다. Pro 요금제의 리셋 규칙은 공식 문서에서 찾지 못했다. 위 내용은 내 화면에서 본 관찰이고, 다른 날 같은 시각에 리셋되는지는 아직 보지 못했다.

    이제 작업 하나가 끝나면 /clear를 친다. 새 작업은 새 세션에서 시작한다. 이걸 습관으로 만들어야 한다. 모르고 넘어갔다면 72%가 아니라 100%를 진작에 찍었을 것이다.

    계기판을 더 자주 봐야겠다는 생각도 든다. 이상한 걸 발견했을 때 그냥 넘기지 않고 물어본 덕분에 이 구조를 이해했다. 눈치채지 못했으면 다음 번에도 같은 실수를 반복했을 것이다.

  • 세션 ID 문제, Claude Code에서 MCP가 안 보이는 이유

    세션 ID 문제, Claude Code에서 MCP가 안 보이는 이유

    ujinify.com 워드프레스에 MCP Adapter가 코어에 내장돼 있다는 걸 알게 됐다. 나중에 밝혀졌지만, 이 이야기의 결론은 결국 세션 ID 하나였다.

    거기에 랭크매스(워드프레스 SEO 플러그인)가 ability(어빌리티, 서버가 외부에 제공하는 기능 단위)를 25개나 등록해뒀다. get-seo-scores로 여러 글 점수를 한 번에 조회하거나, audit-site-seo/fix-site-seo로 사이트 전체를 진단하고 자동 수정하거나, set-homepage-seo로 설정까지 바꿀 수 있는 것들이었다.

    그전까지는 SEO 점수 하나 확인하려면 Playwright(플레이라이트, 브라우저를 자동으로 조종하는 도구)로 wp-admin 에디터를 직접 열어야 했다. 이걸 Claude Code에 MCP 서버로 등록하면 API 호출 하나로 끝낼 수 있겠다 싶었다. 그래서 등록했다. 그리고 아무것도 안 됐다.

    문제: MCP가 ‘연결됨’인데 tool이 하나도 안 보여요

    등록 자체는 간단했다.

    claude mcp add --transport http ujinify-wp <MCP 서버 URL> --header "Authorization: Basic ..."

    claude mcp list를 돌리면 “✔ Connected”가 떴다. 연결됐다는 뜻이다. 그런데 ToolSearch(툴서치, Claude Code에서 사용 가능한 도구를 찾는 기능)로 아무리 찾아봐도 랭크매스 tool(툴, 도구)이 하나도 잡히지 않았다. 매번 “No matching deferred tools found”였다. 세션을 새로 시작해봤다. 재시작도 해봤다. 결과는 똑같았다.

    서버는 연결됐다는데, 실제로 쓸 수 있는 tool은 하나도 없는 상태. 이건 좀 웃겼다.

    curl로 직접 테스트해보니 세션 ID가 필요했다

    Claude Code를 믿지 않기로 했다. curl(컬, 터미널에서 HTTP 요청을 직접 보내는 도구)로 MCP 서버에 직접 말을 걸어보기로 했다.

    먼저 initialize(초기화 요청, MCP 서버와 처음 악수하는 단계)를 호출했다. 서버는 정상 응답했다. 그런데 응답 헤더(header, 서버가 응답과 함께 보내는 부가 정보)를 보니 mcp-session-id라는 값이 딸려 내려왔다. 이 서버는 Streamable HTTP(스트리머블 HTTP, 요청마다 세션 ID를 주고받는 MCP 통신 방식)를 쓰는 타입이었다.

    그 상태에서 세션 ID 없이 바로 tools/list를 호출했다. 결과는 “Missing Mcp-Session-Id header” 에러였다. 반대로, initialize에서 받은 세션 ID를 Mcp-Session-Id 헤더에 실어서 tools/list를 다시 호출했더니 — 랭크매스가 등록한 tool 3개의 스키마(schema, 도구의 구조와 사용법을 정의한 명세)가 정상으로 내려왔다.

    서버는 100% 정상이었다. 문제는 Claude Code 쪽이었다.

    왜 Claude Code는 세션 ID를 못 챙겼을까

    MCP 통신은 핸드셰이크(handshake, 통신 시작 전에 서로 규칙을 맞추는 절차)가 필요하다. MCP 공식 스펙 문서에도 명시돼 있는 내용인데, 순서는 이렇다.

    • initialize 요청을 보낸다.
    • 서버가 응답 헤더로 세션 ID를 내려준다.
    • 이후 모든 요청에 그 세션 ID를 Mcp-Session-Id 헤더에 실어서 보낸다.

    Claude Code의 HTTP MCP 클라이언트가 이 두 번째와 세 번째 단계를 처리하지 못하고 있었다. initialize는 성공했지만, 거기서 받은 세션 ID를 이후 요청에 다시 실어 보내지 않는 것이다.

    claude mcp list의 “Connected” 표시는 initialize 성공만 확인한다. 그러니 이 핸드셰이크 실패를 잡아내지 못하고, 겉으로는 정상처럼 보인다. 연결은 됐지만 실제로 아무것도 못 쓰는 상태가 “Connected”로 표시되는 것이다. 별로였다.

    해결책: 직접 만든 함수로 세션 핸드셰이크 처리하기

    MCP tool 등록에 기대는 걸 포기했다. 대신 이 핸드셰이크 절차를 그대로 코드로 옮겼다.

    scripts/rankmath-mcp.ts에 callRankMathAbility 함수를 만들었다. 이 함수는 호출될 때마다 아래 순서를 자동으로 처리한다.

    • initialize 요청을 보내서 세션 ID를 받는다.
    • 받은 세션 ID를 헤더에 실어서 tools/call을 호출한다.
    • 결과를 반환한다.

    CLI(커맨드 라인 인터페이스, 터미널에서 명령어로 프로그램을 실행하는 방식)에서 바로 쓸 수 있게 npm run rankmath -- <ability-name> '<json-params>' 형태로도 만들었다. 기존 upload.ts의 getWpConfig()를 그대로 재사용해서 인증 정보를 따로 관리할 필요도 없게 했다.

    Claude Code가 못 하는 걸 직접 구현한 것이다. 우아하진 않지만 동작한다.

    실제로 점수를 가져왔을 때 검증 결과

    get-seo-scores ability(어빌리티, 여기서는 랭크매스가 MCP를 통해 제공하는 기능)를 호출해서 여러 글의 점수를 한 번에 받아봤다. 이전에 Playwright로 wp-admin 에디터를 직접 열어서 확인했던 점수와 정확히 일치했다. 132번 글 81점, 109번 글 83점, 42번 글 79점. 숫자가 맞았다.

    그다음엔 실제로 사이트 전체를 점검했다. seo_filter를 bad(50점 이하), empty-fk(포커스 키워드 없음), unscored(채점 안 됨)로 각각 돌렸다. 결과는 깔끔했다. 문제되는 글은 하나도 없었다. 포커스 키워드가 없는 건 애초에 필요 없는 고정 페이지 3개뿐이었다.

    브라우저를 한 번도 열지 않았다. 몇 초 만에 끝났다.

    이 방법의 한계, 읽기만 되고 쓰기는 안 된다

    명확히 해둬야 할 게 있다.

    get-seo-scores는 데이터베이스에 이미 저장된 점수를 ‘읽기’만 한다. 랭크매스의 점수 계산 엔진 자체는 여전히 브라우저, 구체적으로는 wp-admin 에디터의 자바스크립트에서만 돌아간다. 새 글을 올리거나 콘텐츠를 수정한 직후에 점수를 새로 계산하려면 여전히 에디터를 열어야 한다. REST나 MCP로는 재계산을 트리거할 방법이 없다.

    analyze-post-content라는 ability도 있다. 이건 점수를 주지 않는다. 원본 콘텐츠와 24개 테스트 목록, 채점 기준 문서를 던져주고 AI가 직접 채점하라는 방식이다. 손이 많이 가서 아직 시도하지 않았다.

    결국 이 방법으로 해결된 건 ‘이미 채점된 점수를 빠르게 읽는 것’이다. 채점 자체는 여전히 브라우저 의존이다. 한계를 알고 쓰면 된다. 모르고 쓰면 나중에 헷갈린다.

    다음에는 analyze-post-content를 실제로 써볼 생각이다. AI가 직접 채점한 결과가 랭크매스 공식 점수와 얼마나 차이 나는지 비교해보면 쓸만한지 아닌지 판단할 수 있을 것이다.

    “설정 화면에 값이 있다고 실제로 작동한다는 보장은 없다”는 패턴은 이번에도 반복됐다. 워드프레스 SEO 버그 5개 총정리 글에서 다룬 다섯 가지 사례와 정확히 같은 결이다. 이번엔 랭크매스 설정이 아니라 Claude Code의 MCP 연결 상태였다는 것만 다르다.

    세션 쪽에서 겪은 또 다른 문제는 Claude 5시간 한도 72% 찍힌 이유는 세션 길이였다에도 있다.

  • Claude Code 스킬 정리 후기, 죽은 폴더를 가리키던 스킬 1개와 메모리 4개

    Claude Code 스킬 정리 후기, 죽은 폴더를 가리키던 스킬 1개와 메모리 4개

    블로그 예약 발행 작업을 다 끝내고 나서 문득 이런 생각이 들었다. 계속 추가만 했지, 한 번도 정리를 안 했다는 걸. 그래서 Claude Code 스킬 정리를 처음으로 해봤다.

    Claude Code 스킬 정리를 해야겠다고 생각한 이유

    Claude Code에는 스킬(skill)과 메모리(memory)라는 개념이 있다. 스킬은 “블로그 포스팅해줘”처럼 특정 말을 하면 자동으로 실행되는 루틴이다. 메모리는 Claude가 작업할 때 참고하는 기록 파일들이다. 둘 다 쓸수록 쌓인다.

    나는 작업할 때마다 스킬을 추가하고, 메모리를 저장하고, 그걸 다시 꺼내 쓰는 방식으로 Claude를 활용해왔다. 근데 돌이켜보니 한 번도 뒤를 돌아본 적이 없었다. 쌓기만 했다.

    그래서 물어봤다. “지금 메모리랑 스킬 중에 삭제해도 무관한 거 있어?”

    오래 안 쓴 것을 찾는 대신 ‘없는 것’을 찾기로 한 결정

    처음 접근은 단순했다. 마지막으로 쓴 날짜 순으로 정렬해서 오래된 것부터 지우면 될 거라고 생각했다.

    근데 그게 안 됐다.

    세션(session, 한 번의 작업 대화 기록) 기록이 3주치밖에 남아 있지 않았고, 그 안에서 스킬이 실제로 호출된 횟수는 전부 합쳐서 20번이었다. 이 데이터를 기준으로 “안 쓴 것”을 추리면 멀쩡하게 살아 있는 스킬까지 다 걸린다. 판단 기준으로 쓸 수가 없었다.

    그래서 기준을 바꿨다. Claude Code 스킬 정리의 기준을 “오래 안 쓴 것”이 아니라 “없는 걸 가리키는 것”으로 잡았다. 존재하지 않는 폴더나 폐기된 프로젝트를 참조하고 있는 항목을 찾는 거다. 이건 기간 데이터가 없어도 확인할 수 있다.

    가장 자주 쓰는 말에 걸린 죽은 스킬 발견

    첫 번째로 나온 게 blog-automation이라는 스킬이었다.

    이 스킬의 트리거(trigger, 스킬을 작동시키는 특정 말이나 조건)는 “블로그 포스팅해줘”, “포스트 만들어줘” 같은 말이었다. 내가 가장 자주 쓰는 말이다. 그런데 이 스킬이 라우팅하는 대상이 blog-generator 폴더였고, 그 폴더는 이미 삭제된 지 오래였다. ai-blog-generator도 마찬가지였다. 폐기된 프로젝트였다.

    즉, “블로그 포스팅해줘”라고 하면 없는 폴더로 cd(폴더 이동 명령어) 하는 명령이 나올 수 있는 상태였다.

    언제부터 이랬는지는 모른다. 이 스킬을 만든 건 4월이고, 프로젝트가 폐기됐다고 기록해둔 건 8월 말이다. 그사이 어디쯤에서 깨졌을 텐데 정확한 시점을 알 방법이 없다. 깨진 걸 아무도 기록하지 않았으니까.

    조금 놀랐다. 그리고 열받았다. “왜 지금까지 나한테 말을 안 해줬을까”라는 생각이 먼저 들었는데, 내가 물어본 적이 없으니 말을 안 한 거다. 틀린 말도 아니다. 그게 더 답답했다.

    지금까지 Claude와 함께 만든 것들이 — 서브에이전트 7개로 만든 블로그 파이프라인 같은 것들이 , 이 스킬을 거쳐갔다면, 쏟아부은 노력에 구멍이 있었던 거다. 멀쩡히 작동하는 것처럼 보였을 뿐이고.

    메모리 파일에서 나온 4개의 좀비 참조들

    좀비 참조란 이미 삭제되거나 폐기된 대상을 가리키는 기록을 말한다. 실제로 없는데 있다고 믿고 있는 항목들이다.

    메모리 쪽에서는 네 개가 나왔다.

    • 폐기한 tech-blog-generator의 스킬 자동 업데이트 규칙
    • 폐기한 블로그의 글자 수 기준
    • 삭제된 blog-generator의 에이전트 구성 문서
    • 색인에 아예 등록되지 않은 고아 파일 하나

    전부 존재하지 않는 것을 가리키고 있었다.

    그중 세 번째가 특히 웃겼다. 삭제된 blog-generator의 에이전트 구성 문서는 그날 만든 거였다. 몇 시간 전에 세션 정리하면서 저장한 문서인데, 그 프로젝트가 이미 폐기됐다는 걸 모르고 저장한 거다. 만든 지 반나절도 안 된 메모리가 벌써 죽어 있었다.

    쌓이는 속도보다 썩는 속도가 더 빠를 수 있다는 걸 이때 처음 실감했다.

    Claude Code 스킬 정리 후 blog-automation 스킬 삭제, 메모리 57개→53개, MEMORY.md 끊긴 링크 0건을 터미널에서 직접 확인한 화면

    정리를 다 끝내고 나서 직접 확인한 화면이다. blog-automation은 더 이상 없고, 메모리는 53개로 줄었고, MEMORY.md 안에서 실제 파일을 못 찾는 링크는 하나도 없다.

    Claude Code 스킬 정리 결과와, 지우지 않은 것들

    정리 결과는 이렇다.

    항목 정리 전 정리 후
    메모리 파일 57개 53개
    색인 파일 줄 수 125줄 113줄
    스킬 blog-automation 포함 blog-automation 삭제

    고아 파일 하나는 지우지 않았다. 내용 자체는 쓸 만했기 때문에, 이미 있던 자율 처리 규칙 안으로 합쳤다. 내용은 버리지 않고 위치만 바꾼 거다.

    요즘 안 쓰는 스킬도 몇 개 더 있었는데, 그것들은 건드리지 않았다. 이유는 단순하다. 프로젝트 폴더가 다 살아 있었다. 깨진 게 아니라 지금 당장 안 하는 일일 뿐이다. “오래 안 썼다”는 이유 하나로 지우는 건 근거가 약하다. 지우지 않았다.

    앞으로 이런 문제를 줄이려면 어떻게 해야 할까

    Claude Code 스킬 정리를 매번 내가 물어봐서 하는 게 아니라, 이런 참조 오류를 수시로 체크해서 먼저 알려주는 Claude를 만들고 싶다. 근데 내 마음대로 안 된다. 그게 답답하다.

    한편으로는 나 자신이 문제였다는 것도 안다. 원하는 결과가 나오면 거기서 멈추는 습관. 더 이상 손대지 않으려 하는 것. 정리 같은 건 생각도 안 하는 것. 이번에 발견한 구멍은 그 습관이 만든 거다.

    당장 할 수 있는 건 하나다. 프로젝트를 폐기할 때 관련된 스킬과 메모리를 같이 정리하는 것. 지금은 추가할 때만 신경 쓰고 삭제할 때는 아무것도 안 한다. 그러니까 blog-automation처럼 언제 죽었는지도 모르는 스킬이 생기는 거다.

    폴더를 지울 때 그 폴더를 참조하는 스킬과 메모리도 같이 확인해야 한다. Claude Code 스킬 정리는 그때 같이 끝내는 게 맞다. 순서가 그래야 한다. 추가할 때 꼼꼼한 것보다, 삭제할 때 꼼꼼한 게 더 중요하다는 걸 이번에 배웠다.

    다음번에 또 이런 걸 발견하면, 그때도 내가 물어봐서 찾을 거다. 그게 지금은 좀 불만이지만, 적어도 묻는 습관은 들었다.

    CLAUDE.md 쪽에서 비슷하게 발견한 중복 지침은 CLAUDE.md 정리하다 발견한 중복 지침에 정리했다.