클로드 코드로 인스타그램 카드뉴스(캐러셀) 자동화 시스템 만들기
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
디자인 툴을 닫고, 코드로 카드뉴스를 찍기로 했다
클로드 코드로 카드뉴스 만들기에 빠진 뒤로, 디자인 툴을 한 번도 열지 않고 인스타그램 카드뉴스 한 게시물을 통째로 뽑는 시스템을 직접 만들어 운영하고 있습니다. 슬라이드 디자인용 HTML부터 캡처·발행 스크립트까지 전부 클로드 코드로 짰고, python export_slides.py 한 줄이면 10장짜리 인스타그램 캐러셀 이미지가 한 번에 나옵니다. 사람은 콘텐츠 기획에만 집중하고, 픽셀 작업은 코드에 위임하는 구조입니다.
아래에는 콘텐츠 설계부터 발행까지 클로드 코드로 짠 실제 코드와, 그 과정에서 직접 부딪힌 에러를 그대로 풀어놓았습니다. 미리캔버스나 Canva로 슬라이드를 한 장씩 만드는 데 지친 분, 인스타그램 봇 만들기를 고민 중인 분이 같은 걸 다시 만들 수 있도록 썼습니다.

왜 카드뉴스 생성을 자동화했나
인스타그램 정보성 계정을 운영하면 콘텐츠 자체보다 이미지 제작이 병목입니다. 한 게시물에 10장 안팎, 슬라이드마다 제목·본문·강조·진행 표시·로고를 일관된 톤으로 맞춰야 합니다. 디자인 툴로 한 장씩 만들면 게시물 하나에 30분에서 1시간이 그냥 사라지고, 디자인 일관성도 사람 손에 맡기는 순간 무너집니다.
그래서 접근을 바꿨습니다. 카드뉴스를 디자인 작업이 아니라 코드로 다뤄 보기로 한 겁니다. 슬라이드는 한 번 만들어 둔 HTML/CSS 템플릿으로 그리고, 콘텐츠만 갈아끼웁니다. 캡처와 발행은 파이썬이 결정론적으로 처리합니다. 사람은 문장 단위 기획에만 집중하고, 픽셀은 기계에 맡기는 구조입니다.
결과적으로 지금은 이런 흐름으로 돌아갑니다.
- 주제/문장(클로드와 함께 작성) →
carousel.html(슬라이드 HTML) python export_slides.py carousel.html 10→ 슬라이드 PNG 일괄 캡처- 캡션·해시태그·DM 트리거 문구 작성(
캡션.md) python post_carousel.py 01→ 인스타그램 캐러셀 발행 + ManyChat으로 댓글-DM 자동화
예전엔 게시물 하나에 한 시간 가까이 붙잡혀 있었는데, 지금은 몇 분이면 끝납니다.
전체 아키텍처 — 디자인·캡처·발행의 분리
파이프라인은 3개 레이어로 나뉩니다.
[1] 디자인
주제/문장 → carousel.html
(HTML/CSS 슬라이드)
│
▼
[2] 캡처
export_slides.py (Playwright)
→ slide_1~10.png (1080×1350)
│
▼
[3] 발행
post_carousel.py
→ PNG→JPEG → GitHub 호스팅
→ graph.instagram.com (캐러셀)
+ ManyChat (댓글→DM)
핵심은 디자인(HTML)과 출력(코드)을 분리한 것입니다. 슬라이드 모양은 HTML/CSS가, 픽셀과 발행은 파이썬이 담당합니다. 디자인을 바꾸고 싶으면 HTML만, 콘텐츠를 바꾸고 싶으면 텍스트만 고치면 됩니다. 한 번 만들어두면 주제만 갈아끼우며 무한히 재사용할 수 있는 자산이 됩니다.
핵심 설계 결정: 420px 뷰포트 + device_scale_factor 2.5714
가장 먼저 부딪힌 게 해상도였습니다. 인스타그램 권장 캐러셀 해상도는 1080×1350(4:5)입니다. 그런데 처음에 브라우저 뷰포트를 1080×1350으로 직접 잡고 캡처했더니, 미리보기로 보던 화면과 폰트 크기·줄바꿈·간격이 전부 달라졌습니다. 뷰포트를 키우면 CSS가 다시 리플로우되기 때문입니다.
그래서 방식을 바꿨습니다. 디자인은 420×525(4:5) 뷰포트에서 그대로 두고, 출력만 device_scale_factor로 2.5714배(=1080/420) 키워 캡처합니다.
- 뷰포트는 420×525로 고정 → 미리보기 = 결과물(리플로우 없음)
device_scale_factor = 1080 / 420 ≈ 2.5714→ 캡처 시 1080×1350으로 고해상도 렌더링- 폰트·간격은 420 기준 그대로라 화면에서 본 그대로 선명하게 떨어짐
이미 만들어진 이미지를 억지로 키우는 게 아니라, 브라우저가 처음부터 고해상도로 렌더링해 찍어 주는 방식입니다. 작은 뷰포트로 디자인하고 큰 배율로 출력하는 셈입니다.
1단계: carousel.html — 슬라이드를 HTML로 디자인
슬라이드는 한 장의 HTML 파일로 만듭니다. .carousel-track 안에 슬라이드 div를 가로로 늘어놓고, 캡처 단계에서 translateX로 한 장씩 이동시키며 찍는 구조입니다.
<div class="ig-frame">
<div class="carousel-viewport">
<div class="carousel-track">
<section class="slide cover">
<h1>복붙하면 끝나는<br>클로드 프롬프트 10개</h1>
<p class="sub">직장인용 실전 모음</p>
</section>
<section class="slide">
<h2>1. 회의록 요약</h2>
<p>녹취 텍스트를 붙이고 <b>핵심 결정·액션아이템·담당자</b>만 표로 뽑아달라고 한다.</p>
</section>
</div>
</div>
</div>
이 구조에서 신경 쓴 지점들입니다.
- 슬라이드는
하나 = 한 장. 표지(cover)와 본문을 클래스로 구분해 톤을 다르게 줍니다. - 색·여백·폰트는 CSS 변수로 한 곳에 모읍니다. 시리즈 톤을 바꿀 때 변수만 고치면 전 슬라이드에 일괄 반영됩니다.
- 강조는 그냥
로. 디자인 툴의 수작업 대신 마크업으로 강조를 표현합니다.
이 HTML을 만드는 단계가 클로드를 가장 많이 쓰는 지점입니다. "이 주제로 10장짜리 카드뉴스 만들 건데, 이 템플릿 구조로 슬라이드 채워줘"라고 시키면 슬라이드 구성·강조·줄바꿈까지 한 번에 나옵니다. 사람은 톤과 사실관계만 검수합니다.
2단계: export_slides.py — Playwright로 캡처
렌더링은 Playwright로 처리합니다. 헤드리스 Chromium이 carousel.html을 열고, 슬라이드를 한 장씩 이동시키며 PNG로 캡처합니다. 핵심은 앞서 말한 뷰포트 고정 + 배율 캡처입니다.
VIEW_W, VIEW_H = 420, 525
SCALE = 1080 / 420 # = 2.5714... -> 출력 1080x1350
page = await browser.new_page(
viewport={"width": VIEW_W, "height": VIEW_H},
device_scale_factor=SCALE,
)
await page.set_content(html_content, wait_until="networkidle")
await page.wait_for_timeout(3000) # Google Fonts 로드 대기
for i in range(TOTAL_SLIDES):
await page.evaluate("""(idx) => {
const track = document.querySelector('.carousel-track');
track.style.transition = 'none';
track.style.transform = 'translateX(' + (-idx * 420) + 'px)';
}""", i)
await page.wait_for_timeout(400)
await page.screenshot(
path=f"slides/slide_{i+1}.png",
clip={"x": 0, "y": 0, "width": VIEW_W, "height": VIEW_H},
)
몇 가지 실전 디테일이 있습니다.
wait_until="networkidle"+ 3초 대기: 웹폰트(Google Fonts)가 로드되기 전에 캡처하면 폰트가 깨진 채로 찍힙니다. 네트워크가 멎고도 한 박자 더 기다립니다.- 캡처 전 인스타 UI 크롬 숨김: HTML에 미리보기용으로 넣어 둔 헤더·점·좋아요·캡션(
.ig-header,.ig-dots,.ig-actions,.ig-caption)을display:none으로 끄고 프레임을 정확히 420×525로 고정한 뒤 찍습니다. clip으로 영역 고정: 슬라이드 한 장 영역만 잘라 1080×1350으로 떨어지게 합니다.
실행은 이렇게 끝납니다.
pip install playwright && playwright install chromium
python export_slides.py carousel.html 10
# -> slides/slide_1.png ... slide_10.png (각 1080x1350)

[참고] 빌드 팁: 클라우드 동기화 폴더에서 직접 대량 이미지를 쓰면 빠른 연속 저장 때 파일이 유실됩니다. 저는 /tmp에서 캡처한 뒤 결과만 폴더로 복사합니다. 동기화 폴더는 원본(HTML·캡션) 보관용, 빌드는 로컬 임시 폴더에서 하는 원칙입니다.
3단계: post_carousel.py — 인스타그램 발행 + ManyChat
이미지가 나오면 발행입니다. 여기서는 비공식 스크래핑을 피하고 공식 인스타그램 그래프 API(graph.instagram.com)를 씁니다. post_carousel.py는 편 폴더에서 PNG 10장과 캡션.md를 읽어 다음 순서로 게시합니다.
- PNG를 JPEG로 변환해 GitHub에 업로드: 인스타 콘텐츠 API는 공개 URL만 받으므로, 이미지를 GitHub 저장소(
blog-images)에 올려 공개 raw/CDN URL을 확보합니다. - 각 이미지를 캐러셀 아이템 컨테이너로 생성:
POST /{ig-user-id}/media에image_url과is_carousel_item=true를 보내 컨테이너 ID를 받습니다. - 캐러셀 컨테이너로 묶기:
media_type=CAROUSEL,children=[컨테이너 ID들],caption=캡션으로 부모 컨테이너를 만듭니다. - 발행:
POST /{ig-user-id}/media_publish에 부모 컨테이너 ID를 보내면 게시됩니다.
python post_carousel.py 01 --dry-run # 호스팅·캡션까지만 확인
python post_carousel.py 01 # 실제 발행
토큰 같은 비밀값은 클라우드 드라이브가 아니라 로컬(~/.insta-publisher/config.json)에만 둡니다. 페이스북 페이지 없이 인스타그램 로그인 방식으로도 발행할 수 있습니다.
발행 이후는 ManyChat이 받습니다. 캡션의 DM 키워드(예: 프롬프트)를 댓글 자동화 트리거로 걸어두면, 누군가 그 댓글을 달았을 때 자동으로 DM이 나갑니다. 저장+댓글+DM 루프가 인스타그램 알고리즘이 좋아하는 인게이지먼트 신호라 도달이 붙습니다. 키워드는 게시물별로 'specific post'에 격리해 다른 콘텐츠와 안 꼬이게 합니다.

자주 만나는 에러 3가지 (직접 겪은 것)
만들면서 실제로 막혔던 지점입니다. 같은 길을 가는 분이라면 거의 다 만나게 됩니다.
캡처본이 미리보기와 다르게 깨진다
처음엔 브라우저 뷰포트를 곧장 1080×1350으로 잡고 캡처했습니다. 그랬더니 디자인할 때 보던 화면과 폰트 크기·줄바꿈·여백이 전부 달라졌습니다. 뷰포트를 키우면 CSS가 다시 리플로우되기 때문입니다. 해결은 뷰포트를 420×525로 고정한 채 device_scale_factor=2.5714로 캡처하는 것이었습니다. 미리보기와 결과물이 정확히 일치합니다. 추가로 웹폰트 로드를 기다리지 않으면 폰트가 깨진 채 찍히므로 networkidle 뒤에도 몇 초 더 대기해야 합니다.
인스타 API가 PNG 업로드를 거부한다 (에러 9004 / 2207052)
캡처한 PNG를 그대로 호스팅해 발행하려 하자 인스타 콘텐츠 API가 9004, 2207052 에러로 계속 거부했습니다. 원인은 포맷이었습니다. 인스타 콘텐츠 게시 API는 JPEG만 안정적으로 받습니다. 그래서 업로드 직전에 Image.open(p).convert("RGB") 후 save(..., format="JPEG", quality=90)로 변환해 .jpg로 올리도록 바꿨더니 해결됐습니다.
AI가 잡아 준 폰트가 어딘가 어색하다
클로드가 처음 만든 HTML은 본문 폰트로 Noto Serif·Pretendard를 잡아 줬는데, 막상 캡처해 보니 한글 카드뉴스 톤과 미묘하게 안 맞았습니다. CSS 폰트 스택을 애플 시스템 폰트(SF Pro + Apple SD Gothic Neo)로 교체하니 훨씬 깔끔해졌습니다. 디자인 디테일은 결국 사람이 한 번 잡아 줘야 하는 영역이라는 걸 배웠습니다.
마치며 — 어디까지를 사람이 정하고 어디부터 기계에 넘길까
문장 하나로 카드뉴스 한 게시물이 나오는 구조는, 결국 디자인(사람·LLM)과 출력(결정론적 코드)의 경계를 잘 긋는 문제였습니다. 만들면서 몸으로 익힌 게 몇 가지 있습니다.
가장 컸던 건 카드뉴스를 HTML로 보기 시작한 순간 자동화가 풀렸다는 점입니다. 디자인 툴을 코드로 흉내 내려 하지 않고, 한 번 만든 템플릿에 콘텐츠만 갈아끼우는 쪽으로 방향을 잡으니 나머지가 따라왔습니다. 캡처 단계에서는 뷰포트를 420으로 고정하고 배율로 키우는 방식이 "미리보기 = 결과물"을 보장했고, 뷰포트를 키우는 순간 리플로우로 다 무너졌습니다.
- 인스타 API는 JPEG·공개 URL을 요구합니다. 이 전제를 호스팅 단계에 미리 끼워 넣어야 막히지 않습니다.
- 콘텐츠와 폰트 톤처럼 감이 필요한 부분은 여전히 사람과 LLM이 잡아 주고, 캡처와 발행처럼 매번 똑같은 일은 코드에 맡깁니다. 이 경계를 지키니 비용과 품질 편차가 같이 잡혔습니다.
같은 원리로 유튜브 쇼츠, 회의록, 뉴스 큐레이션도 자동화해두고 쓰고 있습니다.
자주 묻는 질문
Q. 코딩을 몰라도 만들 수 있나요?
A. 핵심 흐름은 "슬라이드를 HTML 템플릿으로 한 번 만들어 두고, 콘텐츠만 클로드로 채운 뒤 파이썬으로 캡처·발행"하는 것입니다. 템플릿은 한 번 갖추면 재사용하므로 매번 코딩할 필요는 없습니다. 처음 한 번은 클로드에게 "이 구조로 캐러셀 HTML 만들어줘"라고 시켜 골격을 받으면 됩니다.
Q. 미리캔버스·Canva보다 정말 빠른가요?
A. 게시물 하나(10장)를 손으로 만들면 30분~1시간이지만, HTML에 텍스트만 바꾸면 캡처는 수 초입니다. 디자인 자유도는 디자인 툴이 높지만, "같은 포맷의 시리즈를 반복 발행"하는 경우 자동화 이득이 가장 큽니다.
Q. 인스타그램 자동 발행은 계정 정지 위험이 없나요?
A. 비공식 스크래핑이 아니라 메타 공식 인스타그램 그래프 API를 쓰면 정책 위반이 아닙니다. 다만 비즈니스/크리에이터 계정이 필요하고, 이미지는 JPEG로 변환해 공개 URL로 올려야 합니다.
Q. 왜 Pillow로 직접 그리지 않고 HTML로 하나요?
A. 처음엔 Pillow로 픽셀을 직접 그리는 방식도 썼지만, 그림자·그라데이션·웹폰트·줄바꿈을 코드로 일일이 맞추는 비용이 컸습니다. HTML/CSS는 디자인 표현력이 훨씬 좋고, 브라우저가 렌더링을 대신 해 주므로 Playwright 캡처 방식으로 정착했습니다.
이 글은 제가 실제 운영 중인 인스타그램 카드뉴스(캐러셀) 생성 시스템의 코드와 시행착오를 기준으로 정리했습니다. 캡처는 Playwright, 발행은 인스타그램 그래프 API 공식 경로를 사용합니다. (2026년 6월 기준)
같은 주제 다른 글 — Claude Code로 직접 만들기
- 클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)
- Claude Code로 만족도 설문 만들기 — 교육자료만 주고 30분 만에 맞춤 설문
- 캔바로 2주 걸릴 강의 슬라이드 60장, 클로드 코드로 하루 만에 만들었다
- AI 견적서 만들기 — 고객 문의 메일 + 원가표로 견적 초안 자동 생성
- Claude Code로 회사 홈페이지 만들기 — Next.js + 카페24 쇼핑몰 연동까지
- 클로드 스킬 만드는 법: 3C 분석 프레임워크를 재사용 가능한 Skill로 설계했습니다
- 유튜브 플레이리스트 웹사이트 만들기 — 손글씨 146곡 선물 (GitHub Pages)
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기