인스타그램 자동 발행, 본인 계정이면 무료였습니다
저는 카드뉴스를 종종 만듭니다. 글 한 편을 몇 장의 이미지로 압축해 인스타그램에 올리는 그 형식 말입니다. 그런데 부끄럽지만, 그렇게 만든 PNG들을 그동안 줄곧 손으로 올려 왔습니다. Finder에서 카드 이미지를 차례로 골라 인스타그램 앱에 끌어다 붙이고, 캡션을 복사해 붙여 넣는 일을 매번 반복했습니다. 인스타그램 API로 이걸 자동 발행할 수 있다는 걸, 그것도 본인 계정이면 무료라는 걸 한참 뒤에야 알았습니다.
자동화해야겠다는 생각은 늘 머릿속 한구석에 있었습니다. 다만 막연히 "인스타그램 API는 유료겠지, 심사도 깐깐하겠지"라고 짐작하고는 제대로 알아보지도 않은 채 미뤄두고 있었습니다. 그러다 어느 날 마음먹고 들여다보고서야, 제 짐작이 거의 다 틀렸다는 걸 알게 됐습니다. 본인 계정에 본인 글을 올리는 일은 무료였고, 그렇게까지 복잡하지도 않았습니다.
이 글은 그 뒤늦은 깨달음에 대한 회고입니다. 그리고 막상 만들면서 가장 의외였던 한 가지, 텍스트와 달리 이미지는 "외부 호스팅"이라는 한 겹이 더 필요했던 이야기를 함께 적어 두려 합니다.
무료인 줄 몰랐다는 게 가장 부끄러웠습니다
인스타그램에는 Content Publishing이라는 그래프 API가 있습니다. 이걸 쓰면 코드로 게시물을 발행할 수 있습니다. 제가 가장 먼저 확인한 건 비용과 심사였는데, 결론부터 말하면 본인이 소유하거나 관리하는 계정을 다루는 한 무료였습니다. App Review를 받을 필요도, 비즈니스 인증을 거칠 필요도 없었고, API 호출 자체에 드는 비용도 없었습니다.
핵심은 접근 권한의 구분에 있었습니다. 인스타그램 그래프 API는 권한을 두 단계로 나눕니다. 제가 막연히 두려워하던 "심사"는 그중 한쪽에만 해당하는 이야기였습니다.
| 구분 | Standard Access | Advanced Access |
|---|---|---|
| 대상 | 본인 / 내가 관리하는 계정 | 타인의 계정을 대신 운영 |
| App Review | 불필요 | 필요 (앱 심사) |
| 대표 용도 | 내 글을 내 계정에 발행 | 여러 고객사 계정을 다루는 서비스 |
저는 제 계정에 제 카드뉴스를 올리려는 것뿐이었으니, 처음부터 끝까지 Standard Access 범위 안에 있었습니다. 앱 심사는 남의 계정을 대신 운영하는, 이를테면 여러 브랜드의 계정을 관리하는 도구를 만들 때 비로소 필요한 절차였습니다. "내 글을 내 계정에"는 애초에 심사 대상이 아니었던 셈입니다.
돌이켜 생각해보면, 저를 가로막은 건 기술적 장벽이 아니라 제가 미리 그어 둔 선입견이었습니다. 알아보는 데 한나절이면 충분했을 일을, "어차피 유료겠지"라는 짐작 하나로 한참을 미뤄 둔 것입니다. 확인하지 않은 가정이 결국 가장 비싼 비용이었다는 생각이 들었습니다.
발급은 생각보다 평범했고, 함정은 엉뚱한 데 있었습니다
토큰을 발급받는 과정은 의외로 담백했습니다. 예전 자료들을 보면 인스타그램 API를 쓰려고 Facebook 페이지를 억지로 하나 만들어 연결하는 단계가 꼭 끼어 있었습니다. 그런데 2024년 7월 무렵부터 Instagram Login 경로가 생기면서, 이제는 페이지 없이 인스타그램 계정만으로 바로 연결할 수 있게 됐습니다.
큰 흐름은 이렇습니다. 인스타그램 계정을 프로페셔널 계정으로 전환하고, Meta 개발자 대시보드에서 앱을 하나 만든 다음, 대시보드에서 토큰을 생성합니다. 직접 OAuth 흐름을 코드로 짜지 않아도, 대시보드의 토큰 생성 버튼만으로 60일짜리 long-lived 토큰이 나옵니다. 발급받은 토큰은 코드에 박지 않고 .env에 두고 불러 쓰는, 늘 하던 방식 그대로 두었습니다.
"Insufficient developer role"이라는 엉뚱한 벽
정작 저를 한참 멈춰 세운 건 토큰 발급 자체가 아니라, 계정을 연결하려고 로그인하는 단계에서 튀어나온 Insufficient developer role 에러였습니다. 분명 제 인스타그램 계정인데 권한이 부족하다니, 처음엔 무슨 말인지 이해가 가지 않았습니다.
원인은 두 가지 '권한'이 서로 다른 것이었기 때문입니다. 인스타그램 계정의 '관리자'라는 사실과, Meta 개발자 대시보드에서 그 앱에 대해 갖는 '역할'은 별개였습니다. 제 계정이라고 해서 제가 만든 앱의 개발자 역할이 자동으로 따라붙지는 않았던 것입니다.
해결은 두 단계였습니다. 먼저 앱의 역할 설정에서 해당 계정을 Instagram 테스터로 등록합니다. 그리고 여기서 많이들 놓치는 단계가 하나 더 있는데, instagram.com/accounts/manage_access/에 들어가 그 초대를 직접 수락해야 합니다. 등록만 해 두고 수락을 빠뜨리면 같은 에러가 계속 납니다. 저도 이 수락 단계를 한참 뒤에야 발견하고서야 다음으로 넘어갈 수 있었습니다.
텍스트는 토큰이면 끝, 이미지는 image_url 한 겹이 더 있었습니다
여기서부터가 이 글에서 가장 적어 두고 싶었던 부분입니다. 캡션, 그러니까 텍스트는 토큰만 있으면 정말 그것으로 끝이었습니다. 그런데 이미지는 달랐습니다.
인스타그램의 Content Publishing API는 이미지를 받을 때 image_url, 즉 공개된 URL만 받습니다. 로컬에 있는 PNG 파일을 multipart로 직접 업로드하는 경로 자체가 없습니다. 처음엔 당연히 파일을 그냥 던져 넣으면 되는 줄 알았는데, API는 "파일을 받는" 게 아니라 "이미 인터넷 어딘가에 올라가 있는 이미지의 주소를 받는" 방식이었습니다.
그래서 흐름에 한 겹이 더 생깁니다. 텍스트는 손에 든 토큰으로 곧장 보내면 되지만, 이미지는 게시하기 직전에 어딘가 공개 URL로 먼저 올려 두어야 비로소 그 주소를 API에 건넬 수 있습니다. 로컬 도구를 만들면서 가장 예상 밖이었던 지점이 바로 이 한 겹이었습니다.
그래서 이미지를 어디에 올릴까
이미지를 잠깐이라도 공개 URL로 만들어 줄 곳이 필요했고, 몇 가지 선택지를 두고 저울질했습니다. 제 상황은 어디까지나 로컬에서 도는 작은 도구였다는 점을 염두에 두고 보면 좋겠습니다.
| 방법 | 장점 | 걸리는 점 |
|---|---|---|
| cloudflared 임시 터널 | 외부에 이미지가 남지 않음 | 게시할 때마다 터널을 띄워야 하고 URL이 매번 바뀜 |
| Cloudinary | 안정적인 URL과 CDN, REST로 직접 호출 가능 | 외부 저장소에 이미지가 남음 |
| Vercel Blob | 관리형 스토리지 | 배포까지 엮여야 해서 로컬 도구엔 과함 |
처음엔 외부에 아무것도 남기지 않는 cloudflared 임시 터널이 깔끔해 보였습니다. 다만 게시할 때마다 터널을 새로 띄워야 하고 그때마다 주소가 바뀌는 점이 도구로 쓰기엔 번거로웠습니다. Vercel Blob은 배포 환경까지 끌고 와야 해서, 노트북에서 도는 작은 도구에는 과한 무게였습니다.
결국 저는 이미 키를 가지고 있던 Cloudinary를 골랐습니다. 안정적인 URL과 CDN을 그대로 얻을 수 있었고, 무거운 SDK를 더하지 않아도 서명 업로드 방식의 REST 호출만으로 충분했습니다. 다만 외부 저장소에 이미지가 남는다는 점이 마음에 걸렸는데, 이건 게시가 성공한 직후 destroy로 그 이미지를 지워 잔존을 0으로 만드는 식으로 정리했습니다. 인스타그램에 게시되는 순간 그 임시 사본은 더 이상 필요 없으니, 역할이 끝나면 곧장 치우는 셈입니다.
카드뉴스는 캐러셀이라 한 박자 더
여기에 더해, 제 카드뉴스는 한 장이 아니라 여러 장을 넘겨 보는 형식, 즉 캐러셀(여러 장 묶음 게시물)이었습니다. 그래서 단일 이미지보다 단계가 한 박자 더 들어갑니다. 카드마다 이미지 컨테이너를 따로 만들어 두고, 그것들을 하나의 CAROUSEL 컨테이너로 묶은 뒤, 처리가 끝났는지 잠시 폴링으로 기다렸다가 발행하는 흐름입니다.
# 캐러셀(여러 장) 발행 흐름
1. 카드별 이미지를 공개 URL로 업로드
2. 카드마다 이미지 컨테이너 생성
3. 컨테이너들을 하나의 CAROUSEL 컨테이너로 묶기
4. 처리 완료까지 상태 폴링 (비동기 처리 대기)
5. media_publish 로 최종 발행
6. 게시 성공 후 임시 이미지 destroy
마지막 발행은 media_publish 한 번으로 끝나지만, 그 앞에 컨테이너를 만들고 묶고 기다리는 준비 과정이 있다는 점이 단일 이미지와 다른 부분이었습니다. 인스타그램 쪽에서 이미지를 받아 처리하는 데 약간의 시간이 걸려서, 완료 상태를 폴링으로 확인하고 넘어가도록 했습니다.
자동화하되, 마지막 한 칸은 일부러 남겨뒀습니다
이 자동 발행 파이프라인은 insta-cards라는, Next.js 16으로 만든 로컬 도구에 붙였습니다. 카드 이미지를 만들고 캡션을 다듬던 그 도구에서 곧장 발행까지 이어지도록 한 것입니다. 다만 한 가지, 저는 이걸 완전한 무인 자동 발행으로는 일부러 만들지 않았습니다.
실제 흐름은 이렇습니다. 캡션과 PNG를 준비하고, 먼저 dryRun으로 미리보기를 합니다. 이 단계에서는 인스타그램에 아무것도 올라가지 않고, 무엇이 어떻게 나갈지만 확인합니다. 그렇게 사람이 한 번 눈으로 확인한 뒤에야 confirm으로 실제 발행이 이루어집니다. dryRun을 기본값으로 두고, 발행에는 confirm이라는 명시적 게이트를 두었으며, 이 기능은 개발 환경에서만 동작하도록 묶어 두었습니다.
완전 무인으로 만들지 않은 이유는 단순합니다. 스크립트나 AI가 제 판단을 거치지 않고 계정에 무언가를 올리는 상황이, 편리함보다 훨씬 더 불안하게 느껴졌기 때문입니다. 게시물은 한번 올라가면 그 자체로 흔적이 남고, 캡션 한 줄의 실수가 그대로 공개됩니다. 그러니 캡션만큼은 사람이 마지막으로 한 번 검수하는 게 맞다고 생각했습니다.
손이 가던 일을 자동화하면서도 그 끝의 한 칸은 일부러 비워 두는 것. 처음엔 어딘가 미완성처럼 느껴지기도 했지만, 지나고 보니 이건 게으름이 아니라 의도된 절제였습니다. 결국 자동화의 목적은 사람을 완전히 지우는 게 아니라, 반복되는 수고를 덜고 정작 판단이 필요한 한 지점에 사람을 남겨 두는 데 있는 게 아닐까 하는 생각이 들었습니다. 무료인 줄도 모르고 손으로 올리던 일을, 가장 중요한 한 번의 확인만 남기고 기계에 넘긴 셈입니다.
관련 글
1인 개발자가 CODEF·쿠콘 마이데이터 API 를 알아본 회고
자영업자·프리랜서 자동화 도구를 만들려고 한국 마이데이터 중개 API 인 CODEF (운영사 쿠콘) 를 알아본 1인 개발자 사전 조사 회고입니다. 마이데이터의 두 의미, CODEF 가 다루는 영역, 가격 비공개, 부가세·무통장 입금 시나리오와 트레이드오프를 정리했습니다.
PWA로 admin 페이지에 모바일 푸시 알림 붙이기
알림 하나 받자고 네이티브 앱을 만드는 건 오버킬이었습니다. 그래서 이미 쓰던 admin 웹앱에 PWA를 최소한으로 얹어 모바일 푸시 알림만 받기로 했습니다. iOS 설치형 제약과 푸시 전용 service worker, Slack fan-out까지 직접 부딪쳐 얻은 교훈을 담았습니다.
claude -p 를 LaunchAgent 에 붙일 때 만난 7가지 함정
대화 세션에서 잘 돌아가던 Claude Code 가 LaunchAgent 의 자식 프로세스로 들어가면 전혀 다른 얼굴이 됩니다. 인증·도구 억제·타임아웃 등 실측 기반 7가지 함정을 정리했습니다.