클로드코드로 X 자동 포스팅하는 방법 — 크롬 확장으로 글자수 분할부터 스레드 발행까지
목차
- 1. 크롬 연결하기
- 2. 스킬 폴더 만들기
- 3. 글자수 계산기 넣기
- 4. 분할기 이어 붙이기
- 5. SKILL.md 쓰기
- 6. 실행하기
- 브라우저 조작에서 걸리는 지점
- 안 될 때 확인할 것
- 더 붙일 만한 것
- 검증 환경
- 참고 링크
클로드코드에서 X(옛 트위터)에 글을 대신 올려 주는 스킬을 만들어 보겠습니다. 긴 글은 X 규격대로 잘려 스레드로 올라갑니다.
/login으로 로그인된 상태), 크롬 또는 크로미움 계열 브라우저, Claude in Chrome 확장 1.0.36 이상, Anthropic 직접 구독(Pro·Max·Team·Enterprise), Python 3, X에 로그인된 브라우저.claude setup-token으로 만든 토큰으로 인증한 세션에서는 크롬 연동이 켜지지 않습니다. --chrome을 붙여도 꺼진 채로 시작합니다.완성 파일 내려받기
아래 과정을 그대로 담은 스킬 폴더입니다. 압축을 풀어 ~/.claude/skills/ 에 넣으면 바로 씁니다.
unzip x-post-skill.zip
cp -r x-post ~/.claude/skills/SKILL.md · scripts/split_x.py · scripts/sample.md · README.md — 4개 파일, 4.7KB.
1. 크롬 연결하기
Playwright처럼 자동화 전용 브라우저를 따로 띄우는 방법도 있지만 그 브라우저는 프로필이 비어 있습니다. 실행할 때마다 X에 다시 로그인해야 하고 그러자면 아이디와 비밀번호를 어딘가에 넣어 둬야 합니다.
크롬 확장을 쓰면 그 과정이 통째로 없어집니다. 클로드는 쓰던 브라우저에서 새 탭을 열고 그 브라우저의 로그인 상태를 그대로 씁니다. 이미 X에 로그인돼 있으면 자격증명을 넘길 일이 없고, 로그인 화면이나 CAPTCHA를 만나면 클로드가 멈추고 직접 처리해 달라고 요청합니다.
--chrome 플래그로 클로드코드를 켠다.
claude --chrome/chrome으로 연결을 확인한다. Status: Enabled, Extension: Installed가 보이면 된다./chrome → Enabled by default를 고른다.--chrome으로 켜는 쪽이 가볍습니다.2. 스킬 폴더 만들기
개인 스킬은 ~/.claude/skills//SKILL.md에 둡니다. 모든 프로젝트에서 쓸 수 있습니다.
mkdir -p ~/.claude/skills/x-post/scripts완성 후 구조는 이렇습니다.
~/.claude/skills/x-post/
├── SKILL.md # 게시 절차
└── scripts/
└── split_x.py # 글자수 계산 + 분할3. 글자수 계산기 넣기
이 스킬의 절반은 글자수입니다. X는 문자 개수가 아니라 가중 길이로 셉니다. 규칙은 twitter-text v3 설정에 그대로 공개돼 있습니다.

| 대상 | 세는 값 |
|---|---|
| 라틴 문자·숫자·공백·개행·기본 문장부호 (코드포인트 0–4351 등) | 1자 |
| 한글·한자·가나·이모지 | 2자 |
| URL (길이·도메인 무관) | 23자 고정 |
~/.claude/skills/x-post/scripts/split_x.py를 만들고 아래를 넣습니다.
#!/usr/bin/env python3
"""X 본문 → 스레드 파트 분할기."""
import re, sys
SCALE, DEFAULT_WEIGHT, MAX_WEIGHTED, URL_LEN = 100, 200, 280, 23
LIGHT = ((0, 4351), (8192, 8205), (8208, 8223), (8242, 8247))
URL_RE = re.compile(r"https?://[^\s<>\"]+", re.I)
def weighted_len(text):
"""X 기준 가중 길이. 280이 상한."""
total, last = 0, 0
for m in URL_RE.finditer(text):
total += _plain(text[last:m.start()]) + URL_LEN * SCALE
last = m.end()
total += _plain(text[last:])
return -(-total // SCALE) # ceil
def _plain(s):
out = 0
for ch in s:
cp = ord(ch)
out += SCALE if any(lo <= cp <= hi for lo, hi in LIGHT) else DEFAULT_WEIGHT
return out세 가지만 확인하면 됩니다.
python3 -c "from split_x import weighted_len; print(weighted_len('가'*140), weighted_len('a'*280), weighted_len('https://harulogs.com'))"280 280 234. 분할기 이어 붙이기
같은 파일 아래에 이어서 붙입니다. 경계는 문단 → 문장 → 어절 순으로 낮춥니다. 문장 한복판을 자르지 않기 위해서입니다.
SENT_END = re.compile(r"(?<=[.!?…])\s+")
def _words(sent, limit):
"""상한을 넘는 한 문장을 어절 단위로 다시 자른다."""
out, cur = [], ""
for w in sent.split(" "):
cand = w if not cur else cur + " " + w
if weighted_len(cand) <= limit:
cur = cand
else:
if cur:
out.append(cur)
cur = w
if cur:
out.append(cur)
return out
def _pieces(text, limit):
"""(조각, 앞에 붙일 구분자) 목록."""
for i, para in enumerate(p.strip() for p in re.split(r"\n\s*\n", text.strip())):
if not para:
continue
head = "\n\n" if i else ""
for j, sent in enumerate(s for s in SENT_END.split(para) if s):
sep = head if j == 0 else " "
if weighted_len(sent) <= limit:
yield sent, sep
continue
for k, frag in enumerate(_words(sent, limit)):
yield frag, (sep if k == 0 else " ")
def _pack(pieces, limit):
parts, cur = [], ""
for text, sep in pieces:
cand = text if not cur else cur + sep + text
if weighted_len(cand) <= limit:
cur = cand
else:
if cur:
parts.append(cur)
cur = text
if cur:
parts.append(cur)
return parts
def split_x(text, limit=MAX_WEIGHTED):
"""번호(1/N) 길이까지 상한에 넣고 파트 수가 수렴할 때까지 재계산."""
for n in range(1, 100):
reserve = weighted_len(f"\n\n{n}/{n}")
parts = _pack(_pieces(text, limit - reserve), limit - reserve)
if len(parts) <= n:
total = len(parts)
return [f"{p}\n\n{i}/{total}" for i, p in enumerate(parts, 1)]
raise RuntimeError("분할 실패 — 본문을 줄이세요")
if __name__ == "__main__":
parts = split_x(open(sys.argv[1], encoding="utf-8").read())
over = 0
for i, p in enumerate(parts, 1):
w = weighted_len(p)
over += w > MAX_WEIGHTED
print(f"━━━ [{i}/{len(parts)}] {w}/{MAX_WEIGHTED}자")
print(p, end="\n\n")
print(f"총 {len(parts)}파트 · 초과 {over}건")
sys.exit(1 if over else 0)1/N 표기 자체도 글자수를 먹으므로 상한에 포함해 계산해야 합니다.원고 파일을 넣어 돌려 봅니다.
python3 ~/.claude/skills/x-post/scripts/split_x.py sample.md━━━ [1/2] 270/280자
X의 글자수는 문자 개수가 아니라 가중 길이로 센다. 한글은 한 글자가 2자다.
그래서 280자 상한은 한글로 쓰면 140자에서 끝난다. 눈으로 세다가는 절반쯤에서 게시 버튼이 잠긴다.
URL은 더 특이하다. 길든 짧든 무조건 23자로 계산된다. 도메인을 줄인다고 글자수가 줄지 않는다.
1/2
━━━ [2/2] 167/280자
이모지도 2자다. 👨👩👧 처럼 여러 조각이 붙은 것도 하나로 묶여 2자로 센다.
정리하면 규칙은 셋이다. 라틴 문자와 숫자는 1자, 한글과 이모지는 2자, URL은 23자 고정.
2/2
총 2파트 · 초과 0건종료 코드가 0이 아니면 초과한 파트가 있다는 뜻입니다. 그 상태로는 게시로 넘어가지 않습니다.
👨👩👧를 8자로 세지만 X는 2자로 셉니다. 조각이 붙은 이모지를 하나로 묶지 않기 때문입니다. 초과가 아니라 여유가 남는 쪽이라 게시는 막히지 않습니다. 정확히 세려면 ZWJ·피부톤·국기·키캡 시퀀스를 한 덩어리로 묶어야 합니다.5. SKILL.md 쓰기
스킬 본문은 클로드가 읽을 절차서입니다. name은 생략하면 폴더 이름이 쓰입니다. description은 클로드가 이 스킬을 언제 꺼낼지 판단하는 근거라서 언제 쓰는 스킬인지 먼저 적습니다.
---
name: x-post
description: "브라우저로 X에 글을 게시한다. 긴 글은 가중 글자수(한글 2자, URL 23자) 기준으로
잘라 스레드 체인으로 올린다. Trigger: /x-post, 'X에 올려', '트윗 써줘', 'post to X'"
---
# /x-post — X 게시
## 반드시 지킬 것
- 발행 전 분할된 전 파트를 글자수와 함께 보여주고 승인을 받는다. 승인 전에 게시 버튼을 누르지 않는다.
- 분할은 눈으로 세지 말고 scripts/split_x.py 로 한다. 종료 코드가 0이 아니면 게시하지 않는다.
- 비로그인이면 직접 로그인하지 않는다. 사용자에게 로그인을 요청한다.
- 같은 동작이 두세 번 실패하면 멈추고 화면을 보여준 뒤 묻는다.
- "모두 게시하기" 는 한 번만 누른다.
## 절차
1. python ~/.claude/skills/x-post/scripts/split_x.py <원고> 로 분할한다.
2. 전 파트를 글자수와 함께 출력하고 승인을 받는다.
3. 새 탭에서 https://x.com/compose/post 를 연다. 작성 모달이 바로 열린다.
4. 로그인 상태를 확인한다.
5. 본문 입력창을 클릭해 1번 파트를 입력한다.
6. "게시물 추가" 를 누르고 새로 생긴 블록을 클릭한 뒤 다음 파트를 입력한다. 파트 수만큼 반복.
7. 스크린샷으로 파트 수·순서·초과 경고 없음을 확인한다.
8. "모두 게시하기" 를 한 번 누른다.
9. 프로필에서 순서대로 올라갔는지 확인하고 첫 글 주소를 보고한다.6. 실행하기
클로드코드에 원고를 주고 시키면 됩니다.
/x-post 이 글 X에 올려줘스킬 이름을 몰라도 "X에 올려줘"처럼 description의 트리거에 걸리는 말이면 클로드가 이 스킬을 알아서 꺼냅니다. 클로드가 분할 결과와 함께 승인을 물어봅니다. 확인한 뒤 게시하라고 답하면 됩니다.
브라우저 조작에서 걸리는 지점
X 작성창은 일반적인 입력 폼과 다르게 움직이는 구석이 있습니다. 실제로 걸렸던 세 가지입니다.

| 증상 | 원인 | 조치 |
|---|---|---|
| 다음 파트를 입력했는데 통째로 사라짐 | "게시물 추가" 를 눌러도 포커스가 새 블록으로 따라오지 않는다 | 새 블록을 명시적으로 클릭한 뒤 입력한다 |
| 새로 생긴 빈 블록이 화면에 안 잡힘 | 1번 파트가 길면 모달의 보이는 영역 아래로 밀린다. 모달 위에서 스크롤해도 뒤쪽 타임라인만 움직인다 | 요소를 찾아 scroll_to로 끌어올린 뒤 스크린샷을 새로 찍어 그 좌표를 누른다 |
| 입력창을 누르려던 클릭에 작성 중이던 스레드가 통째로 날아감 | 블록이 늘어나면서 요소 참조가 어긋나 블록 삭제 ×나 모달 닫기에 꽂힌다 | 참조로 곧장 클릭하지 않는다. 방금 찍은 스크린샷의 좌표로 누르고, 블록 우측 끝에서 충분히 떨어진 지점을 고른다 |
cmd+a가 먹지 않습니다. 내용을 지우려면 백스페이스를 반복해야 하는데, 한글은 한 번에 한 글자(가중치로는 2자)씩 지워지므로 가중 글자수가 아니라 실제 글자 수만큼 눌러야 합니다.안 될 때 확인할 것
| 증상 | 조치 |
|---|---|
| 확장이 잡히지 않음 | chrome://extensions에서 설치·활성 확인 → claude --version으로 버전 확인 → /chrome에서 Reconnect extension. 그래도 안 되면 크롬과 클로드코드를 모두 재시작한다 |
| 처음 연결부터 확장이 안 잡힘 | 연동을 처음 켤 때 네이티브 메시징 호스트 설정 파일이 깔린다. 크롬은 이 파일을 시작할 때 읽으므로 크롬을 재시작한다 |
| "Browser extension is not connected" | 크롬과 클로드코드를 재시작한 뒤 /chrome으로 재연결 |
| "Receiving end does not exist" | 확장의 서비스 워커가 유휴 상태로 빠진 것. /chrome → Reconnect extension |
| 브라우저가 아무 반응 없음 | 페이지에 alert·confirm 같은 모달이 떠 있는지 본다. 자바스크립트 모달은 브라우저 이벤트를 막는다. 직접 닫은 뒤 이어서 진행한다 |
| 게시 버튼이 비활성 | 글자수 초과. 초과분이 빨간 배경으로 표시된다. 분할기를 다시 돌린다 |
| "모두 게시하기" 를 눌렀는데 화면 그대로 | 네트워크 지연일 수 있다. 다시 누르지 말고 프로필 타임라인부터 확인한다 |
더 붙일 만한 것
| 항목 | 내용 |
|---|---|
| Premium 계정 | 상한이 25,000자다. 작성창에 Premium 업그레이드 안내가 뜨거나 사이드바 Premium 링크가 가입 페이지로 가면 무료 계정이다. 애매하면 280자로 간다 — 짧게 나뉜 건 붙이면 되지만 초과는 게시 자체가 막힌다 |
| 이미지 첨부 | 게시물당 4장, JPG·PNG·WebP. 체인에서는 블록별로 붙으므로 원하는 파트의 블록에 들어갔는지 확인한다 |
| 예약 발행 | 작성창의 예약 게시물 버튼으로 X 자체 예약을 쓴다 |
| 링크 카드 | 마지막 파트에 URL을 두면 그 파트에 카드가 붙는다. 카드가 붙어도 글자수는 23자 그대로다 |
검증 환경
이 글의 코드와 출력은 아래 환경에서 직접 실행한 결과입니다.
| OS | macOS 26.3.1 |
|---|---|
| Python | 3.11.14 |
| Claude Code | 2.1.236 |
| 글자수 규격 | twitter-text v3 (config/v3.json) |
| 검증일 | 2026년 8월 24일 |
분할기는 무작위 원고 500건으로 돌려 상한 초과 0건, 내용 손실 0건을 확인했습니다.
