인스타 릴스 발행이 막혔다 — 캐러셀과 캡션이 같으면 중복으로 보는 이유 (실측)

·

같은 duckpipe 글을 인스타 캐러셀과 릴스로 각각 발행하려는데, 캐러셀은 문제없이 올라갔고 릴스만 발행 직전에 에러로 막혔다. 원인은 버그가 아니라 4일 전 다른 사고를 막으려고 넣어둔 안전장치였다.

무슨 일이 있었나

같은 블로그 글을 카드뉴스 캐러셀로 먼저 올린 뒤, 같은 캡션 텍스트로 릴스를 이어서 발행하려고 했다. 릴스 스크립트를 실행하니 이런 에러가 났다.

같은 캡션의 게시물이 이미 있다 — 중복 발행을 막았다.
     기존: 18090605828408075 (FEED, 2026-08-08T05:23:32+0000)
     Instagram은 삭제 API가 없어서 중복이 올라가면 앱에서 손으로 지워야 한다.
     의도한 재발행이면 --force 를 붙여라.

방금 전에 올린 캐러셀(FEED)이 “이미 게시된 것”으로 잡혀서, 릴스 발행이 중복으로 판단돼 막힌 것이다.

원인 — 4일 전 사고를 막으려고 넣어둔 가드

에러 메시지만으로는 왜 캐러셀 발행 기록이 릴스 발행을 막는지 알 수 없어서, 발행 스크립트 코드를 직접 확인했다. 이런 함수가 있었다.

// `state.json`은 릴스를 추적하지 않는다(published에 threads/instagram 키만 있다). 게다가
// 2026-08-04에 인스타 캐러셀이 "실패"로 표시됐는데 실제로는 게시된 false negative가
// 두 번 연속 발생했다 — 그때 재시도했으면 중복이었다. 릴스도 같은 위험이 있는데
// Instagram은 삭제 API가 없어서(code 100/subcode 33) 중복이 나면 앱에서 손으로 지워야 한다.
// 그래서 발행 직전에 계정의 최근 게시물을 읽어 같은 캡션이 이미 있는지 본다(읽기 전용).
async function assertNotAlreadyPosted(text) {
  const { data } = await apiCall("GET", `${USER_ID}/media`, {
    fields: "id,caption,timestamp,media_product_type",
    limit: "25",
  });
  const norm = (s) => (s ?? "").replace(/\s+/g, " ").trim();
  const dup = data.find((m) => norm(m.caption) === norm(text));
  if (dup) {
    throw new Error(`같은 캡션의 게시물이 이미 있다 — 중복 발행을 막았다.\n...`);
  }
}

이 가드는 2026-08-04에 겪은 실제 사고에서 나왔다. 인스타 API가 “실패”라고 응답했는데 실제로는 이미 게시돼 있던 경우가 있었고, 그 응답을 그대로 믿고 재시도했다면 똑같은 글이 두 번 올라갔을 것이다. 삭제 API가 없는 플랫폼이라 한 번 중복이 올라가면 앱에서 손으로 지워야 하므로, 발행 직전에 최근 게시물과 캡션을 대조해 미리 막는 방어 코드를 넣어둔 것이다.

이 가드가 조회하는 필드에는 media_product_type(피드인지 릴스인지)도 포함돼 있지만, 실제 비교는 caption만 본다. 그래서 “같은 텍스트를 다른 화면(피드/릴스)에 의도적으로 올리는” 이번 상황도 “같은 텍스트를 같은 화면에 실수로 두 번 올리는” 진짜 사고와 똑같이 취급됐다.

이 채널의 구조상 캐러셀과 릴스는 “정상적으로” 캡션이 겹친다

duckpipe는 하나의 블로그 글을 캐러셀(피드)과 릴스(숏폼) 두 화면에 각각 올린다. 두 화면의 알고리즘과 도달 경로가 다르기 때문이다. 이 발행 흐름 자체가 “같은 콘텐츠를 다른 포맷으로 두 번 올리는” 구조이므로, 캡션을 그대로 재사용하면 캡션 기준 중복 검사에 반드시 걸리게 돼 있었다. 사고를 막으려던 가드가, 이 채널의 정상 발행 패턴과 충돌한 것이다.

선택 — —force 대신 캡션을 다시 썼다

옵션은 두 가지였다.

  • --force로 중복 검사를 건너뛴다 — 당장은 되지만, 앞으로 있을 수 있는 진짜 중복(예: 스크립트를 실수로 두 번 실행)까지 같이 눈감아주게 된다.
  • 릴스용 캡션을 캐러셀과 다르게 쓴다 — 검사 로직은 그대로 두고, 애초에 캡션이 겹치지 않게 한다.

두 번째를 골랐다. 캐러셀은 카드뉴스를 스와이프로 넘기는 포맷이라 본문 요약형 캡션이 맞고, 릴스는 짧은 영상이라 더 짧고 훅 위주인 캡션이 맞다. 포맷에 맞게 캡션을 다시 쓰는 게 품질 면에서도, 안전장치를 그대로 유지하는 면에서도 나은 선택이었다.

정리

중복 발행 가드는 원래 목적(API 응답을 못 믿을 때의 실수 방지)대로 정확히 동작했다. 문제는 이 채널이 그 가드를 만들 때는 없었던 새로운 패턴 — 같은 콘텐츠를 여러 화면에 의도적으로 재사용하는 흐름 — 을 나중에 갖게 됐다는 점이다. 캡션 텍스트 하나만 보는 검사는 “이게 실수인지 의도인지”를 구분하지 못한다. 안전장치를 없애는 대신 운영 방식(캡션을 화면마다 다르게 쓰기)을 바꿔서 맞춘 게, 검사 로직을 더 복잡하게 만들지 않고 문제를 없애는 방법이었다.

자주 묻는 질문

이 중복 발행 가드는 왜 있는 건가요?

2026-08-04에 인스타 캐러셀 발행이 API 에러로 '실패'라고 표시됐는데 실제로는 이미 게시돼 있던 사고가 있었습니다. 그 상태에서 재시도하면 같은 글이 두 번 올라갑니다. Instagram Graph API에는 게시물 삭제 기능이 없어서, 중복이 올라가면 앱에서 손으로 지워야 합니다. 그래서 발행 직전에 최근 게시물 25개를 조회해 같은 캡션이 있으면 미리 막는 코드를 넣어뒀습니다.

캐러셀과 릴스 캡션을 다르게 쓰면 SNS 운영에 문제가 없나요?

오히려 낫습니다. 캐러셀은 스와이프로 여러 장을 넘기는 포맷이라 본문 요약형 캡션이 맞고, 릴스는 짧은 영상이라 더 짧고 훅 위주인 캡션이 맞습니다. 같은 텍스트를 그대로 재사용하는 것보다 포맷에 맞게 다시 쓰는 게 각 화면에서 더 자연스럽습니다.

--force 옵션은 언제 써야 하나요?

의도적으로 같은 캡션을 다시 올려야 하는 경우(예: 첫 시도가 진짜로 실패해 재발행이 필요한 경우)에만 씁니다. 이번처럼 캐러셀과 릴스를 같은 캡션으로 올리려던 상황에서 --force를 쓰면, 앞으로 있을 수 있는 진짜 실수(같은 화면에 같은 글을 두 번 올리는 것)까지 눈감아주는 셈이라 원인 해결이 아니라 우회입니다.

가드가 media_product_type을 같이 비교하도록 고치는 게 더 낫지 않나요?

그 방법도 가능하지만, 이번엔 캡션을 다르게 쓰는 쪽을 택했습니다. 캐러셀과 릴스는 어차피 포맷이 달라 캡션도 다르게 쓰는 게 콘텐츠 품질 면에서 낫고, 가드 로직을 더 복잡하게 만들지 않고도 문제가 해결됩니다. 코드를 고치는 것보다 운영 방식을 바꾸는 게 더 간단할 때는 그쪽을 먼저 본다는 원칙을 적용했습니다.

#Instagram#API#릴스#중복방지#실측