클로드코드로 X 자동 포스팅하는 방법 — 크롬 확장으로 글자수 분할부터 스레드 발행까지

2026년 8월 24일
조회수 6
코멘트0

목차

클로드코드에서 X(옛 트위터)에 글을 대신 올려 주는 스킬을 만들어 보겠습니다. 긴 글은 X 규격대로 잘려 스레드로 올라갑니다.

준비물 — 클로드코드(/login으로 로그인된 상태), 크롬 또는 크로미움 계열 브라우저, Claude in Chrome 확장 1.0.36 이상, Anthropic 직접 구독(Pro·Max·Team·Enterprise), Python 3, X에 로그인된 브라우저.
API 키나 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.

x-post-skill.zip 받기

1. 크롬 연결하기

Playwright처럼 자동화 전용 브라우저를 따로 띄우는 방법도 있지만 그 브라우저는 프로필이 비어 있습니다. 실행할 때마다 X에 다시 로그인해야 하고 그러자면 아이디와 비밀번호를 어딘가에 넣어 둬야 합니다.

크롬 확장을 쓰면 그 과정이 통째로 없어집니다. 클로드는 쓰던 브라우저에서 새 탭을 열고 그 브라우저의 로그인 상태를 그대로 씁니다. 이미 X에 로그인돼 있으면 자격증명을 넘길 일이 없고, 로그인 화면이나 CAPTCHA를 만나면 클로드가 멈추고 직접 처리해 달라고 요청합니다.

브라우저 동작은 눈에 보이는 크롬 창에서 실시간으로 일어납니다. 무엇을 누르는지 보면서 진행할 수 있고 이상하면 그 자리에서 끊으면 됩니다.
1.크롬 웹스토어에서 Claude in Chrome 확장을 설치한다.
2.--chrome 플래그로 클로드코드를 켠다.
claude --chrome
3.첫 실행이면 안내 창이 한 번 뜬다. Enter로 넘긴다.
4./chrome으로 연결을 확인한다. Status: Enabled, Extension: Installed가 보이면 된다.
5.매번 플래그를 붙이기 싫으면 /chromeEnabled 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 설정에 그대로 공개돼 있습니다.

X 가중 글자수 규칙 — 라틴 1자, 한글·이모지 2자, URL 23자
대상세는 값
라틴 문자·숫자·공백·개행·기본 문장부호 (코드포인트 0–4351 등)1자
한글·한자·가나·이모지2자
URL (길이·도메인 무관)23자 고정
상한 280자는 한글로 쓰면 140자입니다. 눈대중으로 자르면 전부 초과합니다. 도메인을 줄여도 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 23

4. 분할기 이어 붙이기

같은 파일 아래에 이어서 붙입니다. 경계는 문단 → 문장 → 어절 순으로 낮춥니다. 문장 한복판을 자르지 않기 위해서입니다.

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)
번호는 본문에 직접 넣습니다. X에는 파트 번호 기능이 없습니다. 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 작성창은 일반적인 입력 폼과 다르게 움직이는 구석이 있습니다. 실제로 걸렸던 세 가지입니다.

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자 그대로다

검증 환경

이 글의 코드와 출력은 아래 환경에서 직접 실행한 결과입니다.

OSmacOS 26.3.1
Python3.11.14
Claude Code2.1.236
글자수 규격twitter-text v3 (config/v3.json)
검증일2026년 8월 24일

분할기는 무작위 원고 500건으로 돌려 상한 초과 0건, 내용 손실 0건을 확인했습니다.

참고 링크

첨부파일
조회 통계 (최근 30일)
PV 6UV 6
이 글이 도움이 되셨나요? 의견을 들려주세요!
지금까지 0명이 의견을 남겼어요
아직 댓글이 없어요. 첫 댓글을 남겨보세요!