김솔비 블로그
기술 블로그

오토포스트 + AI: 완전 자동화를 위해 AI를 어디에 붙이고 어디서 멈췄나

14분 읽기시리즈 3/3

TL;DR

  • 목표는 "상품만 등록하면 세 플랫폼 글이 저절로"였다. 그런데 글 전체를 AI에게 맡기지 않았다. 고정 문구는 템플릿, 상품마다 바뀌는 몇 줄만 AI가 쓴다.
  • AI 호출은 상품 하나에 한 번이다. 네이버 소개·인스타 첫 줄·인스타 소개·태그를 JSON 스키마로 한꺼번에 받고, URL을 지우고 길이를 자르는 가드레일을 거친다.
  • 이미지는 AI가 아니라 코드가 그린다. 매번 같은 품질이 나와야 하는 곳에는 확률적인 생성 모델을 쓰지 않았다.
  • 프롬피드 글은 정한 시각에 가져오기 → AI 문구 → 인스타 게시까지 사람 없이 돈다. 네이버만은 에디터를 채워 두고 발행 버튼은 사람이 누른다.

들어가며

첫 글에서 postflow를 만든 이야기를, 두 번째 글에서 열흘 동안 쓰며 고친 이야기를 했다. 이번 글은 그 사이에 계속 붙여 온 AI 부분만 따로 정리한다.

처음 목표는 단순했다. 카카오톡 테마 하나를 완성하면 네이버 블로그, 포스타입, 인스타그램에 같은 소식을 올려야 한다. 손으로 하면 한 시간이 넘는다. 이걸 "상품 등록 버튼 하나"로 줄이고 싶었다.

"AI한테 글 다 써 달라고 하면 되지 않나?" 처음엔 그렇게 생각했다. 결론부터 말하면 그렇게 하지 않았다. 이 글은 어디에 AI를 붙였고, 어디서 일부러 멈췄는지에 대한 기록이다.

전체 흐름: 누가 무엇을 하나

상품 하나가 세 플랫폼에 올라가기까지를 단계별로 나누면 이렇다.

단계하는 일담당
1. 테마 만들기캐릭터 그림 → 에셋 시트 → .ktheme이미지 AI + 사람 (ktheme-maker)
2. 상품 등록실기기 캡처, 캐릭터, 시리즈·버전 이름사람
3. 글 틀인사말, 구매 링크, 이용 안내, 태그 기본값템플릿 (Mustache)
4. 바뀌는 문구소개 문단, 인스타 첫 줄, 롱테일 태그AI (Gemini)
5. 이미지인스타 첫 장·안쪽 장·마지막 장, 블로그 첫 사진코드 (Java2D)
6. 인스타 게시캐러셀 업로드서버 (Graph API)
7. 네이버·포스타입에디터 열고 제목·본문·사진 채우기Mac 에이전트 (Playwright)
8. 발행마지막 확인 후 [발행]사람

AI가 맡는 건 4번 한 칸이다. 나머지는 템플릿, 코드, 에이전트가 정해진 대로 한다. 이렇게 나눈 이유를 하나씩 적어 본다.

1. 고정 문구는 템플릿, 바뀌는 문구만 AI

처음 시도: 글 전체를 AI에게

처음에는 상품 정보를 주고 블로그 글 전체를 써 달라고 했다. 결과물은 그럴듯했지만 쓸 수가 없었다.

  • 구매 링크와 이용 안내 문구가 매번 조금씩 달랐다. 이건 틀리면 안 되는 부분이다.
  • 가끔 사이트에 없는 기능이나 가격을 지어냈다.
  • 원래 내가 쓰던 글의 순서(인사 → 소개 → 미리보기 → 구매 링크 → 이용 안내)가 흐트러졌다.

바꾼 방식: AI 슬롯

글을 다시 뜯어보니 상품마다 실제로 바뀌는 건 몇 줄뿐이었다. 그래서 글 틀은 Mustache 템플릿으로 고정하고, 바뀌는 자리만 {{ai_intro}}, {{ai_caption_hook}} 같은 AI 슬롯으로 비워 뒀다.

슬롯들어가는 곳길이 상한
naver.intro네이버 블로그 소개 문단300자
instagram.caption_hook인스타 캡션 첫 줄 (시리즈 소개)50자
instagram.intro인스타 버전 소개 문단250자
common.tags추가 태그 (쉼표 구분)200자, 최대 10개

템플릿은 설정 화면에서 고칠 수 있고, 고친 기록이 남아 되돌릴 수도 있다. 링크, 가격, 이용 안내처럼 틀리면 안 되는 문장은 전부 템플릿 쪽에 있다. AI 문구는 그 사이에 끼워질 뿐이다.

오토포스트의 AI 문구 칸: 슬롯마다 [다시 쓰기]와 직접 고치기

이 구조 덕분에 AI가 이상한 문장을 써도 글 전체가 망가지지 않는다. 한 칸만 [다시 쓰기]하거나 직접 고치면 된다.

2. 상품 하나에 호출 한 번, JSON 스키마로

왜 한 번인가

슬롯이 네 개라고 네 번 부르면 비용과 지연이 네 배가 되고, 슬롯끼리 말투가 따로 놀았다. 그래서 상품 하나에 한 번만 부르고, 네 슬롯을 한 JSON으로 받는다. Gemini의 responseSchema를 쓰면 모양이 틀린 답이 오지 않는다.

java
private static final Map<String, Object> RESPONSE_SCHEMA = Map.of( "type", "OBJECT", "properties", Map.of( "naver", object(Map.of("intro", string()), "intro"), "instagram", object(Map.of("caption_hook", string(), "intro", string()), "caption_hook", "intro"), "tags", Map.of("type", "ARRAY", "items", string())), "required", List.of("naver", "instagram", "tags"));

무엇을 넣어 보내나

넣는 것이유
시스템 프롬프트말투(친근한 존댓말, 이모지는 문단마다 1~2개)와 금지 규칙
실제 게시글 예시내가 예전에 쓴 글. 말투를 흉내 내게
시리즈·버전·OS·컨셉, 버전 수"이번 시리즈는 총 N가지 버전" 같은 문장에 필요
첫 번째 사진 (512px로 줄여서)배경색·분위기를 글에 반영. 캐릭터 이름을 지어내지 않게 "색감과 분위기만 참고" 규칙
슬롯별 형식 지시예: "첫 줄은 '이번에는 ~ 테마로 준비해봤답니다.' 형식"

시스템 프롬프트의 규칙 중 가장 중요한 줄은 이것이다.

- URL, 가격, 이용 제한·재배포 관련 문구는 절대 쓰지 않는다(템플릿이 따로 넣는다).

AI가 쓰면 안 되는 것을 미리 템플릿으로 빼 두고, 프롬프트에서 "그건 네 일이 아니다"라고 못 박는 방식이다.

태그는 롱테일로

태그도 그냥 "관련 태그 써 줘"라고 하면 #카톡테마 같은 큰 태그만 나온다. 이런 태그는 이미 템플릿이 붙인다. 그래서 실제로 검색되는 형식을 예시로 줬다.

tags: 이 테마가 검색될 롱테일 태그 6~8개. 사람들이 실제로 검색하는 형식으로:
  '캐릭터+카톡테마'(시나모롤카톡테마), 'OS+캐릭터+카톡테마'(아이폰키티카톡테마),
  '스타일+카톡테마'(도트키티카톡테마) ...
  '카톡테마'처럼 캐릭터·스타일 없는 일반 태그는 쓰지 않는다(따로 붙는다)

3. AI 답을 그대로 믿지 않는다: 가드레일

프롬프트로 부탁해도 AI는 가끔 규칙을 어긴다. 그래서 받은 답을 저장하기 전에 코드로 한 번 더 거른다.

java
static String sanitize(AiSlot slot, String text) { String t = text.replaceAll("(https?://|www\\.)\\S+", "").strip(); // 링크는 템플릿만 넣는다 if (slot == AiSlot.TAGS) { return Tags.split(t).stream().distinct().limit(AiSlot.MAX_TAGS)...; // 중복 제거, 최대 10개 } // 소개 문단은 줄바꿈만 살리고 빈 줄은 없앤다 (빈 줄은 템플릿의 문단 간격과 겹친다) ... return t.codePointCount(0, t.length()) <= slot.maxLength() ? t : 잘라냄; }
가드레일막는 것
URL 지우기지어낸 링크, 이상한 사이트 링크
길이 자르기 (글자 수는 코드 포인트 기준)인스타 첫 줄이 길어져 "더 보기"에 묻히는 것. 이모지가 반으로 잘리지 않게
태그 정리중복, # 붙은 태그, 10개 초과
빈 줄 정리템플릿이 넣는 문단 간격과 겹쳐 빈 줄이 두 번 생기는 것

그리고 AI가 쓴 글이 화면에 HTML로 들어갈 때는 이스케이프한다. AI 문구나 남이 올린 프롬프트 제목에 <img onerror> 같은 게 섞여도 실행되지 않게 하려는 것이다. 이건 보안 점검 때 따로 고친 부분이다.

4. 사람이 고친 문장은 AI가 덮어쓰지 않는다

AI 문구가 마음에 안 들면 직접 고친다. 문제는 그다음이다. 사진을 바꾸거나 [고치지 않은 슬롯 모두 다시 쓰기]를 누르면 AI가 다시 돈다. 이때 내가 고친 문장이 날아가면 안 된다.

그래서 슬롯마다 edited 표시를 둔다.

java
for (AiSlot slot : targets) { AiDraft d = ...; boolean explicitlyAsked = forced != null && forced.contains(slot); if (d.isEdited() && !explicitlyAsked) { continue; // 직접 고친 슬롯은 건너뛴다 } d.generated(sanitize(slot, values.get(slot)), copy.model()); }
누른 버튼직접 고친 슬롯안 고친 슬롯
[고치지 않은 슬롯 모두 다시 쓰기]그대로새로 씀
그 슬롯의 [다시 쓰기]새로 씀 (콕 집어 요청했으니)새로 씀

AI가 쓰면 안 되는 슬롯도 있다

예전에 손으로 올린 글을 가져와 다시 올리면, 캡션이 예전 게시물과 달라지는 문제가 있었다. 그래서 "인스타 캡션 전체" 슬롯을 따로 만들었다. 이 칸에 글이 있으면 템플릿과 AI를 모두 건너뛰고 그대로 올린다. 예전 글을 가져올 때는 원래 캡션이 여기에 들어간다.

이 슬롯은 AI 생성 대상에서 빠져 있다(generated() == false). AI에게 맡기지 않는 칸을 코드로 명시해 둔 셈이다.

5. 기다리게 하지 않기: 비동기와 화면 갱신

AI 응답은 몇 초에서 길면 수십 초가 걸린다. 상품을 등록하는 순간 그걸 기다리게 하고 싶지 않았다.

  1. 상품을 저장하면 AI 상태를 PENDING으로 두고 바로 상품 화면으로 넘어간다.
  2. AI 호출은 @Async로 뒤에서 돈다. 상태는 RUNNING → DONE / FAILED / SKIPPED.
  3. 화면의 AI 칸은 상태가 RUNNING인 동안 htmx로 3초마다 상태를 묻는다.
  4. 끝나면 서버가 HX-Trigger: ai-done 헤더를 보내고, 화면은 AI 칸과 초안 미리보기만 다시 받아 바꾼다. 새로고침이 없다.

실패해도 글은 나간다

AI가 실패하면 슬롯이 빈 채로 템플릿만으로 초안이 만들어진다. 글이 조금 심심할 뿐 깨지지는 않는다. 실패 종류에 따라 처리가 다르다.

상황처리
API 키 없음SKIPPED, "템플릿만으로 초안을 만들었습니다" 안내
429(한도 초과)·503(과부하)설정한 다음 모델로 바로 넘어간다 (gemini-flash-latest → gemini-flash-lite-latest)
모든 모델이 429·50360초 쉬고 한 번 더
그 밖의 오류FAILED, 오류 메시지를 화면에 표시

모델을 쉼표로 여러 개 적을 수 있게 한 건 무료 티어 때문이다. 무료 한도는 모델마다 따로 잡혀서, 하나가 막혀도 다른 하나는 열려 있는 경우가 많았다. 생성기는 인터페이스(TextGenerator)로 감싸 두어서, 조건이 바뀌면 다른 모델로 갈아 끼울 수 있다.

6. 이미지는 AI가 아니라 코드가 그린다

글은 AI에게 맡겼지만, 이미지는 일부러 AI를 쓰지 않았다. 인스타 첫 장, 안쪽 테마 카드, 마지막 안내 장, 블로그 첫 사진은 모두 Java2D 코드(ThemeCover)가 그린다.

코드가 그린 인스타 마지막 장과 블로그 첫 사진 (조명을 든 한교동)

생성형 이미지 AI코드로 그리기
결과매번 다르다매번 같다
글자한글이 자주 깨진다글꼴 그대로 정확하다
실제 캡처다시 그리면서 조금씩 바뀐다원본 그대로 들어간다
비용·시간장마다 호출서버에서 바로

테마 판매 글에서 가장 중요한 건 실제 테마 화면이 정확히 보이는 것이다. 생성 모델이 캡처를 "예쁘게" 다시 그리면 그건 더 이상 실제 화면이 아니다. 그래서 디자인 규칙(캐릭터를 빈 곳에 놓기, 대표 색 뽑기, 프로필 그리드에서 안 잘리게 여백 두기)은 코드로 정하고, 사진은 원본을 그대로 쓴다.

대신 "자동으로 판단하는" 부분은 규칙으로 만들었다. 예를 들어 글자 색은 사진에서 가장 많이 쓰인 색 있는 색을 골라 정한다. 이 알고리즘은 따로 글을 쓸 만큼 시행착오가 많았다.

7. 프롬피드 글: 사람 없이 도는 파이프라인

카톡 테마 글은 사진을 사람이 올려야 시작된다. 반면 내가 운영하는 프롬프트 사이트 프롬피드의 소개 글은 재료가 이미 사이트에 있다. 그래서 여기는 끝까지 자동으로 돌게 만들었다.

순서하는 일
1매분 시계를 보고, 설정한 시각(예: 매일 오전 9시)이 지났는데 아직 안 했으면 시작한다
2프롬피드 최신 글 중 인스타에 아직 안 올린 첫 글을 고른다
3제목·요약·프롬프트·결과 이미지를 가져와 상품으로 만든다
4인스타 카드 3장(표지, 프롬프트 미리보기, 팔로우 안내)을 코드로 그린다
5AI 문구를 만든다. 최대 5분 기다리고, 실패하면 거기서 멈추고 알린다
6인스타에 캐러셀로 올린다
7네이버는 Mac 에이전트가 에디터를 채워 두고 "발행 대기" 알림을 띄운다

프롬피드 글에 자동으로 만들어진 인스타 카드 3장

프롬피드 글은 테마 글과 쓰는 내용이 달라서 요청문(brief)을 따로 둔다. 슬롯 이름은 같지만 "이 프롬프트가 무엇을 해 주는지, 왜 요즘 SNS에서 많이 하는지"를 쓰라고 한다. 프롬프트 원문은 800자까지만 참고로 넣고 "그대로 옮기지 않는다"고 적었다. 사이트에 가야 볼 수 있는 내용을 SNS 글에 다 풀면 안 되기 때문이다.

시스템 프롬프트에는 이런 규칙도 넣었다.

- 프롬프트가 실제로 하는 일만 쓴다. 결과를 직접 해 본 것처럼 꾸며 쓰지 않는다.
- 과장 광고 문구('역대급', '무조건')는 쓰지 않는다.

같은 시각에 두 번 돌지 않게

자동 발행은 "매분 확인"이라 서버가 재시작되거나 느려지면 같은 시각에 두 번 돌 수 있다. 그래서 시작하자마자 "이 시각은 했다"를 먼저 저장한다. 실패하더라도 같은 글을 두 번 올리는 것보다 한 번 빠지는 게 낫다고 판단했다. 그리고 예정 시각에서 2시간이 지나면 따라잡지 않는다. 밤새 꺼져 있던 서버가 아침에 켜지면서 어제 몫을 몰아서 올리는 일을 막기 위해서다.

8. 일부러 사람에게 남긴 것

완전 자동화를 목표로 했지만, 끝까지 사람에게 남긴 것이 있다.

남긴 것이유
네이버·포스타입 [발행] 버튼공식 API가 없어 브라우저 자동화로 채운다. 발행까지 자동으로 누르면 약관·계정 정지 위험이 있다. 그리고 마지막 화면을 한 번 보는 것이 가장 확실한 검수다
실기기 캡처테마가 실제 폰에서 어떻게 보이는지가 상품의 핵심이다
캐릭터(대표 오브젝트) 고르기어떤 캐릭터를 내세울지는 판매 판단이다

인스타는 공식 Graph API가 있어서 서버가 끝까지 올린다. 네이버는 에이전트가 제목, 본문, 사진, 태그까지 다 채워 두고 Mac 알림으로 "발행 대기"를 알린다. 나는 알림을 눌러 화면을 한 번 보고 [발행]만 누른다.

"완전 자동"을 고집했다면 이 마지막 한 번의 클릭도 없앴을 것이다. 하지만 한 사람이 운영하는 채널에서 계정 하나가 막히면 모든 게 끝난다. 클릭 한 번으로 그 위험을 지우는 건 아주 싼 값이라고 봤다.

9. 앞단의 AI: 테마 만들기

오토포스트가 "글"을 자동으로 만든다면, 그 앞의 테마 자체도 AI로 만들기 시작했다. ktheme-maker의 테마 자동 생성이다.

단계담당
캐릭터 그림을 참고해 에셋 시트(캐릭터, 꾸미기, 탭 아이콘) 그리기이미지 AI (ChatGPT 구독의 Codex, 또는 Gemini·OpenAI 이미지 API)
시트에서 요소를 잘라 번호를 붙인 모음 그림 만들기코드
번호마다 역할(캐릭터·꾸미기·탭 5개·안 씀) 정하기Claude 또는 Gemini (그림을 보고 판단)
색, 말풍선, 배경 적용 → .ktheme코드

여기서도 같은 원칙을 썼다. AI는 그림을 그리는 일과 그림을 보고 판단하는 일만 하고, 테마 파일을 조립하는 건 코드가 한다. 결과가 마음에 안 들면 역할만 고쳐서 다시 조립할 수 있다.

그래서 지금의 전체 흐름은 "캐릭터 그림 → (AI) 테마 → 실기기 캡처 → 오토포스트 (AI 문구 + 코드 이미지) → 세 플랫폼"이다. 사람이 하는 일은 캡처와 확인, 그리고 발행 클릭이다.

결과

10월 22일 기준으로 오토포스트 홈에 찍힌 숫자다.

항목값
이번 달 게시29개 (카톡 테마 12, 프롬피드 17)
이번 달 아낀 시간7시간 33분

"아낀 시간"은 플랫폼마다 손으로 할 때 걸리는 시간(설정값)에서 실제로 내가 손댄 시간을 뺀 값이다. 추정이 섞인 숫자라 정확한 측정값은 아니다. 다만 체감은 분명하다. 프롬피드 소개 글은 이제 아침에 알림을 받고, 네이버 화면을 한 번 보고, [발행]을 누르는 것으로 끝난다.

회고

잘한 것

  1. AI 범위를 좁게 잡았다. 글 전체가 아니라 슬롯 네 개. AI가 틀려도 피해 범위가 한 칸이다.
  2. 틀리면 안 되는 것을 AI 밖으로 뺐다. 링크, 가격, 이용 안내는 템플릿. 프롬프트로 "쓰지 마"라고 하고, 코드로 한 번 더 지운다.
  3. 사람의 수정을 존중했다. 직접 고친 문장은 AI가 다시 돌아도 남는다. 이게 없으면 결국 AI 기능을 끄게 된다.
  4. 이미지는 결정적으로 만들었다. 같은 입력이면 같은 결과가 나와서 버그를 재현하고 고칠 수 있었다.

아쉬운 것

  1. AI 문구의 품질을 숫자로 재지 못했다. 몇 번 [다시 쓰기]를 눌렀는지, 몇 번 직접 고쳤는지를 기록하면 프롬프트를 바꿀 때 비교할 수 있었을 것이다.
  2. 예전 글을 가져올 때 AI 슬롯이 비는 걸 늦게 알았다. 그 결과 캡션이 예전 게시물과 다르게 올라간 글이 두 개 생겼다. "AI가 쓰지 않는 칸"을 처음부터 생각했어야 했다.

다음

  • [다시 쓰기]·직접 고침 횟수를 슬롯별로 기록해서 프롬프트 개선의 근거로 쓰기
  • 테마 자동 생성과 오토포스트를 이어서, 테마 완성과 동시에 상품 초안까지 만들기
  • 포스타입도 네이버처럼 "발행 대기" 알림까지 한 흐름으로 묶기

마치며

AI를 붙이면서 가장 많이 한 고민은 "AI가 무엇을 할 수 있나"가 아니라 "AI가 틀렸을 때 무엇이 망가지나"였다. 그 답에 따라 AI 자리, 템플릿 자리, 코드 자리, 사람 자리가 정해졌다. 완전 자동화는 모든 걸 AI에게 맡기는 게 아니라, 틀려도 괜찮은 곳에만 AI를 두고 나머지를 단단하게 만드는 일이었다.