인스타그램 릴스를 API로 올릴 때 공개 URL을 없앤 방법
쇼츠를 만든 뒤 유튜브와 인스타그램, 스레드에 각각 올리는 흐름을 자동화하려 했습니다. 처음에는 인스타그램도 영상 파일을 공개 저장소에 올리고 URL을 넘겨야 하는 줄 알았습니다. 먼저 눈에 들어온 쪽은 video_url 방식. 구현도 그쪽에서 출발했습니다.
그러다 저장소 공개 범위와 파일 정리 방식을 고민하던 중 문서를 더 읽었는데, 인스타그램 릴스에는 파일 바이트를 직접 보내는 resumable 업로드가 따로 있었습니다. 공개 URL은 필요 없었습니다. 다만 컨테이너 생성, 업로드, 처리 확인, 발행이 이어지고 중간 오류도 이유 없이 돌아와서 한 번에 끝나는 흐름은 아닙니다.
먼저 결론부터
- 릴스 발행은 컨테이너 생성, 영상 업로드, 처리 상태 확인, 발행 순서로 진행됩니다.
video_url은 공개 URL을 메타가 내려받는 방식이고, resumable 업로드는 컨테이너가 돌려준uri로 파일을 직접 보냅니다.- resumable 방식으로 바꾸면서 공개 저장소에 MP4를 올리려던 작업을 없앴습니다.
POST /{ig-user-id}/media
GET /{container-id}?fields=status_code
POST /{ig-user-id}/media_publish
- 업로드 주소는 직접 조립하지 않고 컨테이너 응답의
uri를 그대로 사용해야 합니다. - 파일 바이트를 보내더라도
Content-Type은application/x-www-form-urlencoded여야 했습니다. 400 ProcessingFailedError는 응답에서 재시도 불가라고 해도 다시 보내면 통과한 경우가 있었습니다.- 릴스는 세로 9:16 H.264 MP4, 5~90초, 300MB 이하 조건을 맞춰야 하며, 어긋나면 오류 코드 24만 받았습니다.
컨테이너를 먼저 만들고 업로드 주소를 받습니다
처음 요청은 영상을 보내는 요청이 아닙니다. 인스타그램 사용자 ID로 미디어 컨테이너를 만든 뒤, 응답에 포함된 업로드용 uri를 받습니다. 이후 그 주소에 파일을 올리고, 컨테이너의 status_code가 처리 완료 상태가 될 때까지 조회한 다음 발행 요청을 보냅니다.
처음 구현에서는 업로드 주소가 정해진 규칙일 것이라고 생각해, 아래처럼 rupload.facebook.com 주소를 직접 만들었습니다.
rupload.facebook.com/ig-api-upload/v21.0/{id}
그런데 컨테이너 응답으로 받은 주소에는 v26.0이 들어 있었습니다. 제가 조립한 주소로 요청하면 400이 돌아왔습니다. 버전 문제로 보였습니다. 응답에 이미 정확한 uri가 있는데 굳이 추측할 이유는 없었습니다.
그 뒤로는 컨테이너 응답에서 받은 값을 저장해 그대로 씁니다. 업로드 주소는 API 호출자가 만드는 값이 아니라, 해당 컨테이너를 만든 쪽이 알려 주는 값입니다.
공개 URL 대신 파일 바이트를 보냅니다
video_url 방식은 공개된 영상 주소를 전달하는 구조입니다. 메타가 그 주소에서 파일을 가져갑니다. 이 방식만 알고 있을 때는 완성된 MP4를 외부에서 접근 가능한 저장소에 올릴 계획이었고, 릴스 게시가 끝나도 파일을 남길지 게시가 실패했을 때 언제 지울지까지 추가로 정해야 했습니다.
resumable 방식은 컨테이너 생성 때 업로드 방식을 지정하고, 응답의 uri로 파일 바이트를 전송합니다. 아래가 요청 형태입니다.
POST {컨테이너 응답의 uri}
Authorization: OAuth {토큰}
offset: 0
file_size: {바이트 수}
Content-Type: application/x-www-form-urlencoded
본문: 파일 바이트 그대로
여기서 가장 오래 걸린 부분이 Content-Type. 바이너리를 보내니 application/octet-stream이 자연스럽다고 생각했고, 헤더를 빼도 서버가 알아서 처리할 것이라고 본 적도 있습니다. 둘 다 400 ProcessingFailedError.
curl에서는 --data-binary를 썼을 때 통과해서 파일이나 토큰 문제로 오해했습니다. 직접 HTTP 요청을 만드는 코드에서만 실패해서 비교해 보니, curl 요청에는 application/x-www-form-urlencoded가 들어가 있었습니다. 그 값을 맞추니 같은 파일이 올라갔습니다. 본문은 파일 바이트, 헤더는 폼 인코딩이라는 조합.
처리 상태와 오류는 따로 다뤘습니다
파일 전송이 끝났다고 바로 발행할 수 있는 것은 아닙니다. 컨테이너 상태를 조회해 처리가 끝난 것을 확인한 뒤에만 media_publish를 호출합니다. 업로드 성공 응답을 받고 곧바로 발행을 붙였을 때는 영상 처리 상태와 발행 요청을 구분해서 볼 수 없었습니다.
영상 규격도 답답했습니다. 릴스는 세로 9:16, H.264 MP4, 5~90초, 300MB 이하 조건이 있는데, 서버는 오류 코드 24만 돌려줬습니다. 어떤 조건이 틀렸는지는 응답에 없었습니다. 결국 발행 코드보다 앞에서 영상 규격을 확인하는 흐름을 따로 둡니다.
400 ProcessingFailedError도 단순한 입력 오류가 아닙니다. 같은 파일과 같은 캡션으로 요청했는데 한 번은 실패하고 다시 보내자 성공했으며, 응답에는 "retriable": false가 있었지만 실제 동작은 달랐습니다. 처음에는 캡션과 해시태그 조합이 문제라고 보고 본문을 나눠 가며 확인했습니다. 원인은 거기에 없었습니다.
지금은 이 오류가 나오면 컨테이너 생성부터 다시 시도합니다. 재시도 불가라는 응답 문구를 그대로 믿고 실패 처리했다면 간헐적으로 성공하는 업로드까지 놓칠 뻔했습니다.
스레드와 유튜브는 같은 흐름으로 묶이지 않았습니다
스레드도 메타 API 계열이어서 릴스와 같은 resumable 업로드를 기대했지만, 실제로 파일 바이트 업로드를 시도하자 스레드는 video_url 매개변수가 필요하다는 500 오류를 반환했습니다. 스레드 영상에는 공개 URL이 필요합니다.
인스타그램에서 공개 URL을 사용하지 않는 방식을 스레드에 그대로 적용할 수 없습니다. 같은 자동 발행 작업 안에서도 인스타그램은 파일 직접 업로드, 스레드는 공개 URL 전달로 분기해야 합니다.
유튜브는 제약이 달랐습니다. 2020년 7월 28일 이후 만든 API 프로젝트는 감사를 통과하지 않으면 videos.insert로 올린 영상이 비공개로 잠깁니다. 요청에 privacyStatus: public을 넣어도 바뀌지 않습니다. 서비스 계정으로는 업로드할 수 없습니다. 그래서 사용자 OAuth가 필요합니다.
그래서 현재 흐름은 플랫폼마다 업로드 준비를 다르게 둡니다. 인스타그램 릴스는 컨테이너 응답의 uri로 파일을 보내고, 스레드는 공개 URL을 준비하며, 유튜브는 API 프로젝트 상태와 사용자 OAuth를 별도로 확인합니다. 처음에는 모두 영상 업로드 API라고 묶어 생각했지만, 실제로 막힌 지점은 파일 전달 방식과 게시 권한에서 서로 달랐습니다.