인스타 카드뉴스를 매번 같은 스타일로 뽑는 법 — style.md + HTML/CSS + Playwright
AI에 그냥 시키면 매번 다른 디자인이 나옵니다. 디자인을 파일로 고정하고 4:5 PNG로 자동 추출하는 순서.
카드뉴스를 몇 주만 연재해 보면 문제가 어디서 터지는지 알게 됩니다. 캔바 템플릿을 복제해 쓰면 처음 두세 장은 괜찮은데, 제목이 한 글자 길어지는 순간 줄바꿈이 어긋나고 여백이 무너집니다. 그걸 손으로 맞추다 보면 8장짜리 한 세트에 30분이 사라집니다. 그렇다고 AI에게 이미지를 통째로 만들어 달라고 하면 이번 주 결과물은 마음에 드는데, 다음 주에 같은 톤을 다시 못 뽑습니다. 프로필 그리드를 열어 보면 톤이 들쭉날쭉한 카드가 줄줄이 섞여 있죠.
원인은 모델 성능이 아니라 요청 안에 디자인 결정이 없다는 것입니다. 카드 한 장에는 배경색, 본문 크기, 자간, 줄간, 안전 여백, 강조색 개수, 번호 표기 방식처럼 최소 스무 개의 결정이 들어갑니다. 요청에 그 값이 없으면 모델은 매번 새로 고릅니다. 매번 다르게 나오는 게 당연합니다. 반대로 그 결정들을 파일 하나에 적어 두고 요청에는 “소재”만 넘기면, 같은 결과가 반복됩니다.
이 글은 그 방식을 순서대로 정리한 것입니다. 골격은 데키랩 채널의 카드뉴스 자동생산 영상(조회 2.2만)에서 제시된 워크플로우 — 디자인을 스타일 md로 고정하고, Claude Code가 HTML/CSS로 카드를 렌더한 뒤, Playwright 스크린샷으로 PNG를 뽑는 흐름 — 이고, 여기에 Playwright 공식 문서 기준의 구현 코드를 붙였습니다. 미리 밝히면 저희가 이 파이프라인으로 실제 계정에 장기 연재를 돌려본 기록은 아직 없습니다. 아래 내용은 영상에서 제시된 절차와 공개 문서를 근거로 재구성한 것이고, 저희 판단은 판단이라고 따로 표시했습니다.
무엇을 고정해야 하는가
“스타일을 고정한다”는 말이 추상적이라 자주 실패합니다. 실제로는 아래 항목이 숫자로 적혀 있어야 재현됩니다.
| 고정 항목 | 예시 값 | 안 적으면 생기는 일 |
|---|---|---|
| 캔버스 비율·크기 | 4:5 / 1080×1350px | 정사각형과 세로형이 섞여 그리드가 깨짐 |
| 안전 여백 | 상하 96px, 좌우 80px | 카드마다 텍스트 시작점이 달라짐 |
| 폰트·굵기 | Pretendard Variable / 700·400 | 시스템 폴백 폰트로 렌더돼 인상이 바뀜 |
| 글자 크기 단계 | 표지 84 / 제목 56 / 본문 34 | 카드마다 위계가 흔들림 |
| 자간·줄간 | -0.03em / 1.25 | 한글에서 특히 티가 남 |
| 강조색 사용 규칙 | 카드당 1곳 | 강조가 늘어나 강조가 아니게 됨 |
| 글자 수 상한 | 한 줄 18자, 제목 3줄 | 텍스트가 카드 밖으로 넘침 |
| 금지 목록 | 그림자·그라데이션·이모지 | 매번 새 장식이 붙음 |
비율을 4:5로 잡는 건 영상에서도 짚는 부분입니다. 인스타 피드에서 세로로 가장 크게 잡히는 비율이고, 1080×1350px로 두면 절반(540×675)으로도 딱 떨어져 계산이 편합니다.
폴더 구조
작업은 폴더 하나로 끝냅니다.
cardnews/
├── CLAUDE.md # 생산 절차 (새 세션 재현용)
├── style.md # 디자인 결정 (단일 진실 공급원)
├── input/ # 소재 원문
│ └── 2026-07-16-astro-blog.md
├── templates/
│ ├── card.css
│ └── card.html
├── build/ # 생성된 HTML (중간 산출물)
├── build.mjs # Playwright 스크린샷 스크립트
└── out/ # 최종 PNG
핵심은 style.md와 input/의 분리입니다. 디자인은 style.md에만, 내용은 input에만 둡니다. 이 경계가 무너지면(예: 요청 프롬프트에 “이번엔 파란색으로” 같은 말을 섞으면) 재현성이 그 자리에서 깨집니다.
1단계: style.md 만들기
영상에서 제시된 순서는 두 갈래입니다. 참고할 디자인이 있으면 **레퍼런스 캡처를 첨부해 “이 디자인을 재현 가능한 문서로 정리해 달라”**고 요청하고, 제로베이스면 스타일 3안을 추천받아 고르는 쪽입니다. 어느 쪽이든 결과물은 사람이 읽을 수 있는 md 한 장이어야 합니다.
# 카드뉴스 스타일 가이드
## 캔버스
- 비율 4:5 / 1080 x 1350 px
- 안전 여백: 상하 96px, 좌우 80px
- 배경은 단색만. 이미지 배경 사용 금지.
## 컬러
- 배경 #0E0E10 / 본문 #F5F5F5 / 보조 #9A9AA2 / 강조 #FFD400
- 강조색은 카드 한 장당 한 곳에만 쓴다.
## 타이포
- 폰트: Pretendard Variable
- 표지 제목 84px / 700 / 자간 -0.03em / 줄간 1.25
- 본문 제목 56px / 700 / 줄간 1.3
- 본문 34px / 400 / 줄간 1.6 / 색 #9A9AA2
- 한 줄 최대 18자, 제목 최대 3줄, 본문 최대 4줄
## 레이아웃
- 표지: 제목 하단 정렬, 좌상단에 시리즈 라벨
- 본문: [번호] → [제목] → [설명] 수직 스택, 간격 40px / 24px
- 마지막 장: 팔로우 유도 문구 + 계정 핸들
## 금지
- 그림자, 그라데이션, 이모지, 아이콘 임의 추가
- style.md에 없는 색상·크기 신규 생성
글자 수 상한을 규칙에 넣는 건 저희가 덧붙인 부분입니다. 이 한 줄이 있어야 뒤에 나올 넘침 검사와 짝이 맞고, 소재를 쓸 때부터 길이를 맞추게 됩니다.
2단계: Pillow가 아니라 HTML/CSS로 그린다
이미지 생성을 파이썬 Pillow로 하는 예제가 많은데, 영상에서도 짚듯이 카드뉴스에서는 HTML/CSS가 유리합니다. 이유는 세 가지입니다.
- 자간·줄간·줄바꿈·정렬을 CSS가 이미 해결해 둡니다. Pillow는 텍스트 폭을 재서 좌표를 직접 계산해야 하고, 한글 줄바꿈 규칙까지 손으로 짜야 합니다.
- 모델이 가장 많이 학습한 기술입니다. 같은 수정 요청(“제목을 조금만 내리고 본문 회색을 한 톤 밝게”)에 대해 CSS 쪽 성공률이 눈에 띄게 높습니다.
- 웹폰트를 그대로 씁니다. 브랜드 폰트를
@font-face로 물리면 웹사이트와 카드뉴스의 인상이 자동으로 붙습니다.
:root {
--w: 1080px;
--h: 1350px;
--pad-y: 96px;
--pad-x: 80px;
--bg: #0e0e10;
--fg: #f5f5f5;
--sub: #9a9aa2;
--accent: #ffd400;
}
@font-face {
font-family: 'Pretendard';
src: url('./fonts/PretendardVariable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: block;
}
.card {
width: var(--w);
height: var(--h);
padding: var(--pad-y) var(--pad-x);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 40px;
background: var(--bg);
color: var(--fg);
font-family: 'Pretendard', sans-serif;
overflow: hidden;
}
.card h2 {
font-size: 56px;
font-weight: 700;
line-height: 1.3;
letter-spacing: -0.03em;
text-wrap: balance; /* 제목 줄바꿈 균형 */
margin: 0;
}
.card p {
font-size: 34px;
line-height: 1.6;
color: var(--sub);
text-wrap: pretty;
margin: 0;
}
폰트 파일은 반드시 프로젝트 안에 두고 @font-face로 지정하세요. 헤드리스 브라우저는 시스템에 설치된 폰트에 의존하기 때문에, 폰트를 로컬 파일로 물리지 않으면 다른 PC나 CI에서 한글이 폴백 폰트로 렌더되어 자간과 줄바꿈이 전부 어긋납니다. 저희가 보기엔 이 파이프라인에서 가장 자주 사람을 당황하게 하는 지점입니다.
3단계: Playwright로 4:5 PNG 뽑기
렌더된 HTML을 이미지로 굳히는 단계입니다.
npm i -D playwright
npx playwright install chromium
// build.mjs
import { chromium } from 'playwright';
import { readdir, mkdir } from 'node:fs/promises';
import path from 'node:path';
const SRC = 'build';
const OUT = 'out';
await mkdir(OUT, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1080, height: 1350 },
deviceScaleFactor: 1,
});
const files = (await readdir(SRC)).filter((f) => f.endsWith('.html')).sort();
const overflow = [];
for (const file of files) {
await page.goto('file://' + path.resolve(SRC, file));
// 폰트 로딩 완료까지 대기 — 없으면 폴백 폰트 상태로 찍힘
await page.evaluate(() => document.fonts.ready);
// 텍스트 넘침 기계 검사
const bad = await page.$$eval('[data-fit]', (els) =>
els.filter((el) => el.scrollHeight > el.clientHeight + 1).map((el) => el.className)
);
if (bad.length) overflow.push(`${file}: ${bad.join(', ')}`);
await page.locator('.card').screenshot({
path: path.join(OUT, file.replace('.html', '.png')),
});
}
await browser.close();
if (overflow.length) {
console.error('텍스트 넘침 발생\n' + overflow.join('\n'));
process.exit(1);
}
console.log(`${files.length}장 생성 완료`);
몇 가지 짚을 점이 있습니다.
page.screenshot()대신locator('.card').screenshot()을 쓰면 뷰포트와 카드 크기가 1픽셀 어긋나도 잘리지 않습니다.- 더 선명한 원본이 필요하면 CSS 캔버스를 540×675로 절반 줄이고
deviceScaleFactor: 2를 주면 됩니다. 출력은 똑같이 1080×1350입니다. - 넘침이 발견되면
process.exit(1)로 실패시키는 게 중요합니다. Claude Code에게 빌드를 맡기면 종료 코드를 보고 스스로 다시 고치기 때문에, 성공으로 끝내 버리면 깨진 카드가 그대로 통과합니다.
4단계: 자가검수 루프
영상에서 강조되는 부분이 수정 루프입니다. 결과 이미지를 첨부해 피드백을 주거나, 아예 **“스스로 문제를 찾아 style.md까지 고쳐라”**고 맡기는 방식입니다. 저희는 여기에 검사를 두 층으로 나눌 것을 권합니다.
기계 검사는 위 스크립트가 담당합니다. 넘침, 글자 수 상한, 색상 값이 style.md 목록 밖인지 여부처럼 판정이 명확한 것들입니다. 눈 검사는 모델에게 PNG를 다시 읽히는 쪽입니다.
out/ 의 PNG 8장을 열어 style.md와 대조하세요.
1) 규칙 위반을 카드 번호와 함께 목록으로 적으세요. 위반이 없으면 없다고 쓰세요.
2) 원인이 소재 텍스트면 input/을, 템플릿이면 templates/를,
규칙 자체가 모호하면 style.md를 고치세요.
3) 무엇을 왜 고쳤는지 마지막에 3줄로 요약하세요.
한 가지 주의가 있습니다. 자가검수에 style.md 수정 권한까지 주면, 모델이 위반을 고치는 대신 규칙 쪽을 느슨하게 바꿔서 통과시키는 경우가 생깁니다. 저희 판단으로는 루프를 2회로 제한하고, style.md 변경은 반드시 별도 커밋으로 남겨 git diff로 확인하는 편이 안전합니다.
5단계: CLAUDE.md로 재현성 잠그기
여기까지가 세팅이고, 마지막 한 단계가 이 방식의 실제 이득을 만듭니다. 절차를 CLAUDE.md에 적어 두면 새 세션에서 한 줄 요청으로 같은 결과가 나옵니다.
# 카드뉴스 생산 규칙
디자인 결정은 전부 `style.md`에 있다. 대화 중 지시와 충돌하면 style.md가 우선한다.
## 절차
1. `input/`의 최신 md를 읽는다.
2. 5~10장으로 나눈다. 첫 장은 표지, 마지막 장은 팔로우 유도.
3. `templates/card.html`을 기준으로 `build/`에 장별 HTML을 만든다.
4. `node build.mjs` 를 실행한다. 실패하면 원인을 고치고 다시 실행한다.
5. `out/`의 PNG를 style.md와 대조해 검수한다. (최대 2회)
## 금지
- style.md에 없는 색·폰트 크기를 새로 만들지 않는다.
- 소재에 없는 수치나 사례를 지어내지 않는다.
- input/ 원문의 사실관계를 바꾸지 않는다.
이 상태가 되면 매주 하는 일은 input/에 소재 md 하나를 넣고 “이번 주 소재로 카드뉴스”라고 치는 것뿐입니다. 영상에서 “주제 한 줄 입력”이라고 부르는 지점이 여기입니다.
대안과 비교
| 캔바 템플릿 | 피그마 + 데이터 플러그인 | 이 파이프라인 | |
|---|---|---|---|
| 첫 세팅 | 30분 | 반나절 | 반나절~하루 |
| 8장 반복 생산 | 20~30분 | 5~10분 | 수 분 (검수 포함) |
| 텍스트 길이 대응 | 수동 | 부분 자동 | 자동 + 넘침 검사 |
| 디자인 자유도 | 템플릿 범위 | 높음 | 텍스트·도형은 높음, 자유형 그래픽은 약함 |
| 비개발자 단독 운영 | 가능 | 가능 | 어려움 |
| 변경 이력 추적 | 없음 | 버전 히스토리 | git diff |
점수: 8.2
좋은 점
- 반복 비용이 사실상 사라집니다. 세팅 이후에는 소재를 넣는 일만 남습니다.
- 디자인 결정이 텍스트 파일로 남아 검토·인수인계가 됩니다. “왜 이 여백인가”를 물으면 파일을 열어 보면 됩니다.
- 스타일 변경이
git diff로 추적됩니다. 어느 시점에 톤이 바뀌었는지 되짚을 수 있습니다. - 텍스트 넘침을 사람 눈이 아니라 코드가 잡습니다. 8장 중 한 장만 깨지는 사고가 줄어듭니다.
- 고객사별로 style.md만 갈아끼우면 브랜드가 분리됩니다. 유지보수 계약에 “카드뉴스 월 N회”를 붙이기 쉬운 구조입니다.
아쉬운 점
- 첫 세팅 비용이 큽니다. style.md가 확정될 때까지 수정 루프를 여러 번 돌아야 하고, 카드 두세 장짜리 일회성 작업이라면 캔바가 훨씬 빠릅니다.
- Node, Playwright, CSS를 최소한 읽을 줄 알아야 합니다. 비개발자 담당자가 혼자 굴리기는 어렵습니다.
- 헤드리스 폰트 문제 때문에 PC나 CI 환경이 바뀌면 결과가 달라질 수 있습니다. 폰트를 프로젝트에 동봉하지 않으면 언젠가 겪습니다.
- 일러스트, 사진 합성, 곡선 마스크 같은 자유형 그래픽은 여전히 손이 갑니다. 이 방식은 텍스트와 도형 중심 카드에 강합니다.
- 자가검수에 규칙 수정 권한을 주면 규칙이 조용히 느슨해질 수 있습니다. 커밋으로 감시해야 합니다.
이럴 때 쓰세요: 같은 포맷의 카드뉴스를 최소 월 4회, 반년 이상 계속 낼 계획이고, HTML/CSS를 읽을 수 있는 사람이 팀에 한 명은 있을 때.
확인하지 못한 것
- 저희가 이 파이프라인으로 실제 계정에 장기간 연재를 돌린 기록은 없습니다. 위 점수는 공개된 워크플로우와 각 도구의 공식 사양을 근거로 한 판단입니다.
- 인스타그램 업로드 자동화(그래프 API 연동)는 이 글 범위 밖입니다. 여기서 나오는 결과물은
out/의 PNG까지입니다. - 폰트는 상업적 사용 조건이 제각각입니다. Pretendard처럼 오픈 라이선스인 것도 있지만, 브랜드 폰트를 쓸 계획이라면 배포 조건을 공식 페이지에서 확인하세요.
- Playwright와 Chromium 버전에 따라
text-wrap: balance같은 최신 CSS 지원 여부가 달라질 수 있습니다. 렌더 결과로 확인하는 게 확실합니다.