X 아티클 퍼블리셔은(는) 무엇을 하나요?
Markdown 콘텐츠를 X (Twitter) 아티클 편집기에 리치 텍스트 변환과 함께 서식을 유지하여 발행합니다.
사전 요구 사항
- Playwright MCP (브라우저 자동화)
- Premium Plus 구독으로 X에 로그인한 사용자
- Python 3.9+ 및 종속성:
- macOS:
pip install Pillow pyobjc-framework-Cocoa - Windows:
pip install Pillow pywin32 clip-util
- macOS:
- Mermaid 다이어그램:
npm install -g @mermaid-js/mermaid-cli
스크립트
~/.claude/skills/x-article-publisher/scripts/에 위치:
parse_markdown.py
Markdown을 파싱하고 구조화된 데이터를 추출합니다:
python parse_markdown.py <markdown_file> [--output json|html] [--html-only]
반환되는 JSON: title, cover_image, content_images, dividers (block_index 포함), html, total_blocks
copy_to_clipboard.py
이미지 또는 HTML을 시스템 클립보드에 복사합니다 (크로스 플랫폼):
# 이미지 복사 (선택적 압축)
python copy_to_clipboard.py image /path/to/image.jpg [--quality 80]
# 리치 텍스트 붙여넣기를 위한 HTML 복사
python copy_to_clipboard.py html --file /path/to/content.html
table_to_image.py
Markdown 표를 PNG 이미지로 변환합니다:
python table_to_image.py <input.md> <output.png> [--scale 2]
X 아티클이 기본 표 렌더링을 지원하지 않거나 일관된 스타일링이 필요할 때 사용합니다.
전처리 (선택 사항)
발행 전에 변환이 필요한 요소를 Markdown에서 스캔합니다:
표 → PNG
# 표를 임시 파일로 추출한 후 변환
python ~/.claude/skills/x-article-publisher/scripts/table_to_image.py /tmp/table.md /tmp/table.png
# Markdown의 표를 다음으로 대체: 
Mermaid 다이어그램 → PNG
# mermaid 블록을 .mmd 파일로 추출한 후 변환
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.png -b white -s 2
# mermaid 블록을 다음으로 대체: 
구분선 (---)
구분선은 parse_markdown.py에 의해 자동으로 감지되어 dividers 배열에 출력됩니다. X 아티클의 Insert > Divider 메뉴를 통해 삽입해야 합니다 (HTML <hr> 태그는 X에서 무시됨).
워크플로우
전략: "본문 우선, 이미지 다음, 구분선 마지막"
이미지와 구분선이 있는 문서의 경우, 먼저 모든 텍스트 콘텐츠를 붙여넣은 다음 block_index를 사용하여 올바른 위치에 이미지와 구분선을 삽입합니다.
- (선택 사항) 전처리: 표/mermaid를 이미지로 변환
- Python 스크립트로 Markdown 파싱 → title, images, dividers (block_index 포함), HTML 획득
- X 아티클 편집기로 이동
- 커버 이미지 업로드 (첫 번째 이미지)
- 제목 입력
- HTML을 클립보드에 복사 (Python) → Cmd+V로 붙여넣기
- block_index로 지정된 위치에 내용 이미지 삽입
- block_index로 지정된 위치에 구분선 삽입 (Insert > Divider 메뉴를 통해)
- 초안으로 저장 (절대 자동 발행하지 않음)
효율성 지침
목표: 작업 간 대기 시간을 최소화하여 원활한 자동화 경험을 제공합니다.
1. 불필요한 browser_snapshot 피하기
대부분의 브라우저 작업 (click, type, press_key 등)은 결과에 페이지 상태를 포함합니다. 매 작업 후 browser_snapshot을 별도로 호출하지 말고, 작업 반환 결과의 페이지 상태를 직접 사용하세요.
❌ 잘못된 방법:
browser_click → browser_snapshot → 분석 → browser_click → browser_snapshot → ...
✅ 올바른 방법:
browser_click → 반환 결과에서 페이지 상태 획득 → browser_click → ...
2. 불필요한 browser_wait_for 피하기
다음 경우에만 browser_wait_for를 사용하세요:
- 이미지 업로드 완료 대기 (
textGone="미디어 업로드 중") - 페이지 초기 로드 대기 (극히 드문 경우)
browser_wait_for를 사용하여 버튼이나 입력 필드가 나타날 때까지 기다리지 마세요 - 페이지 로드 완료 후 즉시 사용할 수 있습니다.
3. 독립적인 작업 병렬 실행
두 작업에 종속성이 없을 때, 동일한 메시지에서 여러 도구를 병렬로 호출할 수 있습니다:
✅ 병렬 가능:
- 제목 입력 (browser_type) + 클립보드에 HTML 복사 (Bash)
- Markdown 파싱하여 JSON 생성 + HTML 파일 생성
❌ 병렬 불가능 (종속성 있음):
- 커버 이미지를 업로드하려면 먼저 create를 클릭해야 함
- 이미지를 삽입하려면 먼저 콘텐츠를 붙여넣어야 함
4. 브라우저 작업 연속 실행
각 브라우저 작업이 반환하는 페이지 상태에는 필요한 모든 요소 참조가 포함됩니다. 이러한 참조를 사용하여 다음 작업을 직접 수행하세요:
# 이상적인 흐름 (각 단계를 직접 실행, 추가 대기 없음):
browser_navigate → 반환 상태에서 create 버튼 찾기 → browser_click(create)
→ 반환 상태에서 업로드 버튼 찾기 → browser_click(업로드) → browser_file_upload
→ 반환 상태에서 적용 버튼 찾기 → browser_click(적용)
→ 반환 상태에서 제목 상자 찾기 → browser_type(제목)
→ 편집기 클릭 → browser_press_key(Meta+v)
→ ...
5. 사전 작업 미리 수행
브라우저 작업을 시작하기 전에 모든 사전 작업을 완료하세요:
- Markdown 파싱하여 JSON 데이터 획득
- HTML 파일을 /tmp/에 생성
- title, cover_image, content_images 등의 정보 기록
이렇게 하면 브라우저 작업 단계를 연속적으로 실행할 수 있으며, 중간에 데이터 처리를 위해 멈출 필요가 없습니다.
1단계: Markdown 파싱 (Python)
parse_markdown.py를 사용하여 모든 구조화된 데이터를 추출합니다:
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md
출력 JSON:
{
"title": "기사 제목",
"cover_image": "/path/to/first-image.jpg",
"cover_exists": true,
"content_images": [
{"path": "/path/to/img2.jpg", "original_path": "/md/dir/assets/img2.jpg", "exists": true, "block_index": 5, "after_text": "컨텍스트..."},
{"path": "/path/to/img3.jpg", "original_path": "/md/dir/assets/img3.jpg", "exists": true, "block_index": 12, "after_text": "다른..."}
],
"html": "<p>콘텐츠...</p><h2>섹션</h2>...",
"total_blocks": 45,
"missing_images": 0
}
주요 필드:
block_index: 이 인덱스 (0부터 시작) 다음 블록 요소 뒤에 이미지를 삽입해야 함total_blocks: HTML의 전체 블록 요소 수after_text: 참조/디버깅용으로만 유지, 위치 지정에는 사용하지 않음exists: 이미지 파일이 발견되었는지 여부 (false인 경우 업로드 실패)original_path: Markdown에서 확인된 경로 (자동 검색 전)path: 사용할 실제 경로 (자동 검색 시 original_path와 다를 수 있음)missing_images: 어디에서도 찾을 수 없는 이미지 수
클립보드용으로 HTML을 임시 파일에 저장합니다:
python parse_markdown.py article.md --html-only > /tmp/article_html.html
2단계: X 아티클 편집기 열기
브라우저 오류 처리
Error: Browser is already in use 오류가 발생하는 경우:
# 방법1: 먼저 브라우저를 닫고 다시 열기
browser_close
browser_navigate: https://x.com/compose/articles
# 방법2: browser_close가 작동하지 않으면 (잠김), 사용자에게 Chrome을 수동으로 닫도록 요청
# 방법3: 기존 탭 사용, 직접 이동
browser_tabs action=list # 기존 탭 확인
browser_navigate: https://x.com/compose/articles # 현재 탭에서 이동
모범 사례: 매번 시작하기 전에 browser_tabs action=list로 상태를 확인하여 불필요한 빈 탭 생성을 방지합니다.
편집기로 이동
browser_navigate: https://x.com/compose/articles
중요: 페이지가 로드되면 초안 목록이 표시되며 편집기가 아닙니다. 다음이 필요합니다:
- 페이지 로드 완료 대기:
browser_snapshot으로 페이지 상태 확인 - 즉시 "create" 버튼 클릭: "제목 추가"와 같은 편집기 요소는 create를 클릭한 후에만 나타나므로 기다리지 마세요
- 편집기 로드 대기: create 클릭 후 편집기 요소가 나타날 때까지 기다립니다
# 1. 페이지로 이동
browser_navigate: https://x.com/compose/articles
# 2. 페이지 스냅샷을 가져와 create 버튼 찾기
browser_snapshot
# 3. create 버튼 클릭 (일반적으로 ref가 "create"이거나 create 레이블이 있음)
browser_click: element="create button", ref=<create_button_ref>
# 4. 이제 편집기가 열렸으므로 커버 이미지 업로드 등을 계속 진행할 수 있습니다
참고: browser_wait_for text="제목 추가"를 사용하여 페이지를 기다리지 마세요. 이 텍스트는 create를 클릭한 후에만 나타나므로 시간 초과가 발생합니다.
로그인이 필요한 경우 사용자에게 수동으로 로그인하도록 요청합니다.
3단계: 커버 이미지 업로드
- "사진 또는 비디오 추가" 버튼 클릭
- 커버 이미지 경로로 browser_file_upload 사용 (JSON 출력에서)
- 이미지 업로드 확인
4단계: 제목 입력
- "제목 추가" 플레이스홀더가 있는 텍스트 상자 찾기
- browser_type을 사용하여 제목 입력 (JSON 출력에서)
5단계: 텍스트 콘텐츠 붙여넣기 (Python 클립보드)
Python을 사용하여 HTML을 시스템 클립보드에 복사한 다음 붙여넣습니다:
# 클립보드에 HTML 복사
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html
그런 다음 브라우저에서:
browser_click on editor textbox
browser_press_key: Meta+v
이렇게 하면 모든 리치 텍스트 서식 (H2, 굵게, 링크, 목록)이 유지됩니다.
6단계: 내용 이미지 삽입 (텍스트 검색 위치 지정)
권장 방법: after_text 텍스트 검색을 사용한 위치 지정이 block_index보다 더 직관적이고 안정적입니다.
위치 지정 원리
각 이미지의 after_text 필드는 이전 단락의 끝 텍스트 (최대 80자)를 기록합니다. 편집기에서 해당 텍스트가 포함된 단락을 검색하고 클릭한 후 이미지를 삽입합니다.
작업 단계
각 내용 이미지 (content_images 배열)에 대해 block_index가 큰 순서대로:
# 1. 이미지를 클립보드에 복사 (압축 포함)
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py image /path/to/img.jpg --quality 85
# 2. browser_snapshot에서 after_text를 포함하는 단락 검색
# 해당 단락의 ref 찾기
# 3. after_text를 포함하는 단락 클릭
browser_click: element="target text가 있는 단락", ref=<paragraph_ref>
# 4. **중요 단계**: End 키를 눌러 커서를 줄 끝으로 이동
# 이 단계는 매우 중요합니다! 단락 내 링크를 클릭하여 위치가 이동하는 것을 방지합니다
browser_press_key: End
# 5. 이미지 붙여넣기
browser_press_key: Meta+v
# 6. 업로드 대기 (textGone만 사용, 시간 매개변수 없음)
browser_wait_for textGone="미디어 업로드 중"
End 키가 필요한 이유는?
문제: 단락에 링크가 포함된 경우 (예: [링크 텍스트](https://raw.githubusercontent.com/wshuyi/x-article-publisher-skill/main/url)), 단락을 클릭하면 다음과 같은 문제가 발생할 수 있습니다:
- 링크 편집 팝업이 트리거됨
- 단락 끝이 아닌 링크 내부에 커서가 위치함
해결책: 단락을 클릭한 후 즉시 End 키를 누릅니다:
- 커서가 단락 끝으로 이동하도록 보장
- 링크 간섭 방지
- 이미지가 해당 단락 뒤에 올바르게 삽입됨
위치 지정 전략
browser_snapshot에서 반환된 구조 내에서 after_text의 키워드를 검색합니다:
textbox [ref=editor]:
generic [ref=p1]:
- StaticText: "연말연시에 집에서 휴대폰 앨범을 뒤적였는데..." # after_text가 이 텍스트를 포함하면 p1 클릭
heading [ref=h1]:
- StaticText: "데모"
generic [ref=p2]:
- StaticText: "이게 얼마나 간편한지 알아볼까요?"
- link [ref=link1]: "Claude Code" # 참고: 단락에 링크가 포함될 수 있음
...
역순 삽입 예시
block_index가 각각 5, 12, 27인 이미지 3개가 있는 경우:
- 먼저 block_index=27인 이미지 삽입 (after_text 검색 + End + 붙여넣기)
- 그런 다음 block_index=12인 이미지 삽입
- 마지막으로 block_index=5인 이미지 삽입
큰 인덱스부터 작은 순서로 삽입하면 위치 오프셋 문제를 방지할 수 있습니다.
6.5단계: 구분선 삽입 (메뉴를 통해)
중요: Markdown의 --- 구분선은 HTML <hr> 태그를 통해 붙여넣을 수 없습니다 (X 아티클에서 무시됨). X 아티클의 Insert 메뉴를 통해 삽입해야 합니다.
작업 단계
dividers 배열의 각 구분선에 대해 block_index 역순으로:
# 1. block_index 위치의 블록 요소 클릭
browser_click on the element at position block_index in the editor
# 2. Insert 메뉴 열기 (미디어 추가 버튼)
browser_click on "Insert" or "미디어 추가" button
# 3. Divider 메뉴 항목 클릭
browser_click on "Divider" or "구분선" menuitem
# 구분선이 커서 위치에 삽입됩니다
이미지와의 삽입 순서
모든 이미지를 먼저 삽입한 다음 모든 구분선을 삽입하는 것이 좋습니다. 둘 다 block_index 큰 순서대로 진행합니다:
- 모든 이미지 삽입 (가장 큰 block_index부터 시작)
- 모든 구분선 삽입 (가장 큰 block_index부터 시작)
7단계: 초안 저장
- 콘텐츠가 붙여넣어졌는지 확인 (단어 수 표시기 확인)
- 초안은 자동 저장되거나, 필요한 경우 저장 버튼 클릭
- "미리보기"를 클릭하여 서식 확인
- 보고: "초안이 저장되었습니다. 수동으로 검토하고 발행해 주세요."
중요 규칙
- 절대 발행하지 않음 - 초안만 저장
- 첫 번째 이미지 = 커버 - 첫 번째 이미지를 커버 이미지로 업로드
- 리치 텍스트 변환 - 붙여넣기 전에 항상 Markdown을 HTML로 변환
- 클립보드 API 사용 - 올바른 서식을 위해 클립보드를 통해 붙여넣기
- Block index 위치 지정 - 정확한 이미지/구분선 배치를 위해 block_index 사용
- 역순 삽입 - block_index가 높은 것부터 낮은 순서로 이미지와 구분선 삽입
- H1 제목 처리 - H1은 제목으로만 사용되며 본문에 포함되지 않음
- 메뉴를 통한 구분선 - Markdown
---는 Insert > Divider 메뉴를 통해 삽입해야 함 (HTML<hr>은 무시됨)
지원되는 서식
| 요소 | 지원 | 참고 |
|---|---|---|
H2 (##) | 네이티브 | 섹션 제목 |
굵게 (**) | 네이티브 | 강조 |
기울임 (*) | 네이티브 | 강조 |
링크 ([](url)) | 네이티브 | 하이퍼링크 |
| 순서 있는 목록 | 네이티브 | 1. 2. 3. |
| 순서 없는 목록 | 네이티브 | - 글머리 기호 |
인용문 (>) | 네이티브 | 인용 텍스트 |
| 코드 블록 | 변환됨 | → 인용문 |
| 표 | 변환됨 | → PNG 이미지 (table_to_image.py 사용) |
| Mermaid | 변환됨 | → PNG 이미지 (mmdc 사용) |
구분선 (---) | 메뉴 삽입 | → Insert > Divider |
예시 흐름
사용자: "/path/to/article.md를 X에 발행해 줘"
# 1단계: Markdown 파싱
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md > /tmp/article.json
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md --html-only > /tmp/article_html.html
- https://x.com/compose/articles로 이동
- 커버 이미지 업로드 (커버만 browser_file_upload)
- 제목 입력 (JSON에서:
title) - HTML 복사 및 붙여넣기:
그런 다음: browser_press_key Meta+vpython ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html - 각 내용 이미지에 대해 block_index 역순으로:
python copy_to_clipboard.py image /path/to/img.jpg --quality 85block_index위치의 블록 요소 클릭- browser_press_key Meta+v
- 업로드가 완료될 때까지 대기
- 미리보기에서 확인
- "초안이 저장되었습니다. 수동으로 검토하고 발행해 주세요."
모범 사례
block_index를 사용하는 이유 (텍스트 일치 대신)?
- 정확한 위치 지정: 텍스트 콘텐츠에 의존하지 않으므로 여러 곳에 유사한 텍스트가 있어도 올바르게 위치 지정 가능
- 신뢰성: 인덱스는 결정적이므로 텍스트 유사성으로 인한 혼동이 없음
- 디버깅 용이:
after_text는 수동 확인을 위해 계속 유지됨
Python을 사용하는 이유 (브라우저 내 JavaScript 대신)?
- 로컬 처리의 신뢰성: Python은 브라우저 샌드박스 제한 없이 운영 체제 클립보드를 직접 조작
- 이미지 압축: 업로드 전 이미지 압축 (--quality 85)으로 업로드 시간 단축
- 코드 재사용: 스크립트가 고정되어 있어 매번 변환 로직을 다시 작성할 필요 없음
- 디버깅 용이: 스크립트를 개별적으로 테스트할 수 있어 문제를 쉽게 찾을 수 있음
대기 전략
중요 발견: Playwright MCP의 browser_wait_for 실제 동작은 먼저 time초를 기다린 후 조건을 확인하는 방식이며, 폴링 방식이 아닙니다!
// 실제 실행 코드:
await new Promise(f => setTimeout(f, time * 1000)); // 먼저 고정 대기
await page.getByText("xxx").waitFor({ state: 'hidden' }); // 그런 다음 확인
올바른 사용법:
- ✅
textGone만 사용,time설정 안 함: Playwright가 자체적으로 폴링하며 대기 - ✅
time만 사용: 지정된 시간만큼 고정 대기 - ❌
textGone+time동시 사용: 먼저 time초를 기다린 후 확인하므로 시간 낭비
# 권장: textGone만 사용, 조건이 충족될 때까지 자동 대기
browser_wait_for textGone="미디어 업로드 중"
# 또는: browser_snapshot으로 상태를 폴링하여 확인
# 각 작업 후 반환된 페이지 상태를 확인하면 추가 대기 불필요
이미지 삽입 효율성
각 이미지의 브라우저 작업이 5단계에서 2단계로 줄었습니다:
- 이전: 클릭 → 미디어 추가 → 미디어 → 사진 추가 → file_upload
- 새 방식: 단락 클릭 → Meta+v
커버 이미지 vs 내용 이미지
- 커버 이미지: browser_file_upload 사용 (전용 업로드 버튼이 있으므로)
- 내용 이미지: Python 클립보드 + 붙여넣기 사용 (더 효율적)
문제 해결
MCP 연결 문제
Playwright MCP 도구를 사용할 수 없는 경우 (No such tool available 또는 Not connected 오류):
방법1: MCP 재연결 (권장)
/mcp 명령 실행, playwright 선택, Restart 선택
방법2: 잔여 프로세스 정리 후 재연결
# 모든 잔여 playwright 프로세스 종료
pkill -f "mcp-server-playwright"
pkill -f "@playwright/mcp"
# 그런 다음 /mcp를 실행하여 재연결
설정 파일 위치: ~/.claude/mcp_servers.json
브라우저 오류 처리
Error: Browser is already in use 오류가 발생하는 경우:
# 방법1: 먼저 브라우저를 닫고 다시 열기
browser_close
browser_navigate: https://x.com/compose/articles
# 방법2: Chrome 프로세스 종료
pkill -f "Chrome.*--remote-debugging"
# 그런 다음 다시 navigate
이미지 위치 오프셋
이미지 삽입 위치가 올바르지 않은 경우 (특히 링크가 포함된 단락을 클릭할 때):
원인: 단락 클릭 시 링크를 잘못 클릭하여 커서 위치가 잘못됨
해결책: 클릭 후 반드시 End 키를 눌러 커서를 줄 끝으로 이동
# 올바른 흐름
1. browser_click으로 대상 단락 클릭
2. browser_press_key: End # 중요 단계!
3. browser_press_key: Meta+v # 이미지 붙여넣기
4. browser_wait_for textGone="미디어 업로드 중"
이미지 경로를 찾을 수 없음
Markdown의 상대 경로 이미지를 찾을 수 없는 경우 (예: ./assets/image.png가 실제로 다른 위치에 있는 경우):
자동 검색: parse_markdown.py는 다음 위치에서 이미지를 검색합니다:
- 원래 Markdown 파일 기준 상대 경로
- Markdown 파일의 디렉토리
- 절대 경로가 지정된 경우 그대로 사용
수동 확인: missing_images 필드를 확인하여 찾을 수 없는 이미지가 있는지 확인합니다.
해결책: 누락된 이미지의 경로를 수동으로 확인하고 Markdown 파일을 업데이트합니다.

