o2o-castad-backend/poc/instagram2-simple/manual.md

23 KiB

InstagramClient 사용 매뉴얼

Instagram Graph API를 사용한 콘텐츠 게시 및 조회를 위한 비동기 클라이언트입니다.


목차

  1. 개요
  2. 클래스 구조
  3. 초기화 및 설정
  4. 메서드 상세
  5. 예외 처리
  6. 데이터 모델
  7. 사용 예제
  8. 내부 동작 원리

개요

주요 특징

  • 비동기 지원: asyncio 기반의 비동기 HTTP 클라이언트
  • 멀티테넌트: 각 사용자가 자신의 access_token으로 독립적인 인스턴스 생성
  • 자동 재시도: Rate Limit 및 서버 에러 시 지수 백오프 재시도
  • 컨텍스트 매니저: async with 패턴으로 리소스 자동 관리
  • 타입 힌트: 완전한 타입 힌트 지원

지원 기능

기능 메서드 설명
미디어 목록 조회 get_media_list() 계정의 게시물 목록 조회
미디어 상세 조회 get_media() 특정 게시물 상세 정보
이미지 게시 publish_image() 단일 이미지 게시
비디오/릴스 게시 publish_video() 비디오 또는 릴스 게시
캐러셀 게시 publish_carousel() 2-10개 이미지 게시

클래스 구조

파일 구조

poc/instagram/
├── __init__.py      # 패키지 초기화 및 export
├── client.py        # InstagramClient 클래스
├── exceptions.py    # 커스텀 예외 클래스
├── models.py        # Pydantic 데이터 모델
├── main.py          # 테스트 실행 파일
└── manual.md        # 본 문서

클래스 다이어그램

InstagramClient
├── __init__(access_token, ...)     # 초기화
├── __aenter__()                    # 컨텍스트 진입
├── __aexit__()                     # 컨텍스트 종료
│
├── get_media_list()                # 미디어 목록 조회
├── get_media()                     # 미디어 상세 조회
├── publish_image()                 # 이미지 게시
├── publish_video()                 # 비디오 게시
├── publish_carousel()              # 캐러셀 게시
│
├── _request()                      # (내부) HTTP 요청 처리
├── _wait_for_container()           # (내부) 컨테이너 대기
├── _get_account_id()               # (내부) 계정 ID 조회
├── _get_client()                   # (내부) HTTP 클라이언트 반환
└── _build_url()                    # (내부) URL 생성

초기화 및 설정

생성자 파라미터

InstagramClient(
    access_token: str,              # (필수) Instagram 액세스 토큰
    *,
    base_url: str = None,           # API 기본 URL (기본값: https://graph.instagram.com/v21.0)
    timeout: float = 30.0,          # HTTP 요청 타임아웃 (초)
    max_retries: int = 3,           # 최대 재시도 횟수
    container_timeout: float = 300.0,       # 컨테이너 처리 대기 타임아웃 (초)
    container_poll_interval: float = 5.0,   # 컨테이너 상태 확인 간격 (초)
)

파라미터 상세 설명

파라미터 타입 기본값 설명
access_token str (필수) Instagram Graph API 액세스 토큰
base_url str https://graph.instagram.com/v21.0 API 엔드포인트 기본 URL
timeout float 30.0 개별 HTTP 요청 타임아웃 (초)
max_retries int 3 Rate Limit/서버 에러 시 재시도 횟수
container_timeout float 300.0 미디어 컨테이너 처리 대기 최대 시간 (초)
container_poll_interval float 5.0 컨테이너 상태 확인 폴링 간격 (초)

기본 사용법

from poc.instagram import InstagramClient

async with InstagramClient(access_token="YOUR_TOKEN") as client:
    # API 호출
    media_list = await client.get_media_list()

커스텀 설정 사용

async with InstagramClient(
    access_token="YOUR_TOKEN",
    timeout=60.0,               # 타임아웃 60초
    max_retries=5,              # 최대 5회 재시도
    container_timeout=600.0,    # 컨테이너 대기 10분
) as client:
    # 대용량 비디오 업로드 등에 적합
    await client.publish_video(video_url="...", caption="...")

메서드 상세

get_media_list()

계정의 미디어 목록을 조회합니다.

async def get_media_list(
    self,
    limit: int = 25,            # 조회할 미디어 수 (최대 100)
    after: Optional[str] = None # 페이지네이션 커서
) -> MediaList

파라미터:

파라미터 타입 기본값 설명
limit int 25 조회할 미디어 수 (최대 100)
after str None 다음 페이지 커서 (페이지네이션)

반환값: MediaList - 미디어 목록

예외:

  • InstagramAPIError - API 에러 발생 시
  • AuthenticationError - 인증 실패 시
  • RateLimitError - Rate Limit 초과 시

사용 예제:

# 기본 조회
media_list = await client.get_media_list()

# 10개만 조회
media_list = await client.get_media_list(limit=10)

# 페이지네이션
media_list = await client.get_media_list(limit=25)
if media_list.next_cursor:
    next_page = await client.get_media_list(limit=25, after=media_list.next_cursor)

get_media()

특정 미디어의 상세 정보를 조회합니다.

async def get_media(
    self,
    media_id: str              # 미디어 ID
) -> Media

파라미터:

파라미터 타입 설명
media_id str 조회할 미디어 ID

반환값: Media - 미디어 상세 정보

조회되는 필드:

  • id, media_type, media_url, thumbnail_url
  • caption, timestamp, permalink
  • like_count, comments_count
  • children (캐러셀인 경우 하위 미디어)

사용 예제:

media = await client.get_media("17895695668004550")
print(f"타입: {media.media_type}")
print(f"좋아요: {media.like_count}")
print(f"링크: {media.permalink}")

publish_image()

단일 이미지를 게시합니다.

async def publish_image(
    self,
    image_url: str,                  # 이미지 URL (공개 접근 가능)
    caption: Optional[str] = None    # 게시물 캡션
) -> Media

파라미터:

파라미터 타입 설명
image_url str 공개 접근 가능한 이미지 URL (JPEG 권장)
caption str 게시물 캡션 (해시태그, 멘션 포함 가능)

반환값: Media - 게시된 미디어 정보

이미지 요구사항:

  • 형식: JPEG 권장
  • 최소 크기: 320x320 픽셀
  • 비율: 4:5 ~ 1.91:1
  • URL: 공개 접근 가능 (인증 없이)

사용 예제:

media = await client.publish_image(
    image_url="https://cdn.example.com/photo.jpg",
    caption="오늘의 사진 #photography #daily"
)
print(f"게시 완료: {media.permalink}")

publish_video()

비디오 또는 릴스를 게시합니다.

async def publish_video(
    self,
    video_url: str,                  # 비디오 URL (공개 접근 가능)
    caption: Optional[str] = None,   # 게시물 캡션
    share_to_feed: bool = True       # 피드 공유 여부
) -> Media

파라미터:

파라미터 타입 기본값 설명
video_url str (필수) 공개 접근 가능한 비디오 URL (MP4 권장)
caption str None 게시물 캡션
share_to_feed bool True 피드에 공유 여부

반환값: Media - 게시된 미디어 정보

비디오 요구사항:

  • 형식: MP4 (H.264 코덱)
  • 길이: 3초 ~ 60분 (릴스)
  • 해상도: 최소 720p
  • 비율: 9:16 (세로), 16:9 (가로), 1:1 (정사각형)

참고:

  • 비디오 처리 시간이 이미지보다 오래 걸립니다
  • 내부적으로 container_timeout * 2 시간까지 대기합니다

사용 예제:

media = await client.publish_video(
    video_url="https://cdn.example.com/video.mp4",
    caption="새로운 릴스! #reels #trending",
    share_to_feed=True
)
print(f"게시 완료: {media.permalink}")

캐러셀(멀티 이미지)을 게시합니다.

async def publish_carousel(
    self,
    media_urls: list[str],           # 이미지 URL 목록 (2-10개)
    caption: Optional[str] = None    # 게시물 캡션
) -> Media

파라미터:

파라미터 타입 설명
media_urls list[str] 이미지 URL 목록 (2-10개 필수)
caption str 게시물 캡션

반환값: Media - 게시된 미디어 정보

예외:

  • ValueError - 이미지 수가 2-10개가 아닌 경우

특징:

  • 각 이미지의 컨테이너가 병렬로 생성됩니다 (성능 최적화)
  • 모든 이미지가 동일한 요구사항을 충족해야 합니다

사용 예제:

media = await client.publish_carousel(
    media_urls=[
        "https://cdn.example.com/img1.jpg",
        "https://cdn.example.com/img2.jpg",
        "https://cdn.example.com/img3.jpg",
    ],
    caption="여행 사진 모음 #travel #photos"
)
print(f"게시 완료: {media.permalink}")

예외 처리

예외 계층 구조

Exception
└── InstagramAPIError           # 기본 예외
    ├── AuthenticationError     # 인증 오류 (code=190)
    ├── RateLimitError          # Rate Limit (code=4, 17, 341)
    ├── ContainerStatusError    # 컨테이너 ERROR 상태
    └── ContainerTimeoutError   # 컨테이너 타임아웃

예외 클래스 상세

InstagramAPIError

모든 Instagram API 예외의 기본 클래스입니다.

class InstagramAPIError(Exception):
    message: str              # 에러 메시지
    code: Optional[int]       # API 에러 코드
    subcode: Optional[int]    # API 서브코드
    fbtrace_id: Optional[str] # Facebook 트레이스 ID (디버깅용)

AuthenticationError

인증 관련 에러입니다.

  • 토큰 만료
  • 유효하지 않은 토큰
  • 앱 권한 부족
try:
    await client.get_media_list()
except AuthenticationError as e:
    print(f"인증 실패: {e.message}")
    print(f"에러 코드: {e.code}")  # 보통 190

RateLimitError

API 호출 제한 초과 에러입니다.

class RateLimitError(InstagramAPIError):
    retry_after: Optional[int]  # 재시도까지 대기 시간 (초)
try:
    await client.get_media_list()
except RateLimitError as e:
    print(f"Rate Limit 초과: {e.message}")
    if e.retry_after:
        print(f"{e.retry_after}초 후 재시도")
        await asyncio.sleep(e.retry_after)

ContainerStatusError

미디어 컨테이너가 ERROR 상태가 된 경우 발생합니다.

  • 잘못된 미디어 형식
  • 지원하지 않는 코덱
  • 미디어 URL 접근 불가

ContainerTimeoutError

컨테이너가 지정된 시간 내에 처리되지 않은 경우 발생합니다.

try:
    await client.publish_video(video_url="...", caption="...")
except ContainerTimeoutError as e:
    print(f"타임아웃: {e}")

에러 코드 매핑

에러 코드 예외 클래스 설명
4 RateLimitError API 호출 제한
17 RateLimitError 사용자별 호출 제한
190 AuthenticationError 인증 실패
341 RateLimitError 앱 호출 제한

종합 예외 처리 예제

from poc.instagram import (
    InstagramClient,
    AuthenticationError,
    RateLimitError,
    ContainerStatusError,
    ContainerTimeoutError,
    InstagramAPIError,
)

async with InstagramClient(access_token="YOUR_TOKEN") as client:
    try:
        media = await client.publish_image(
            image_url="https://example.com/image.jpg",
            caption="테스트"
        )
        print(f"성공: {media.permalink}")

    except AuthenticationError as e:
        print(f"인증 오류: {e}")
        # 토큰 갱신 로직 실행

    except RateLimitError as e:
        print(f"Rate Limit: {e}")
        if e.retry_after:
            await asyncio.sleep(e.retry_after)
            # 재시도

    except ContainerStatusError as e:
        print(f"미디어 처리 실패: {e}")
        # 미디어 형식 확인

    except ContainerTimeoutError as e:
        print(f"처리 시간 초과: {e}")
        # 더 긴 타임아웃으로 재시도

    except InstagramAPIError as e:
        print(f"API 에러: {e}")
        print(f"코드: {e.code}, 서브코드: {e.subcode}")

    except Exception as e:
        print(f"예상치 못한 에러: {e}")

데이터 모델

Media

미디어 정보를 담는 Pydantic 모델입니다.

class Media(BaseModel):
    id: str                              # 미디어 ID
    media_type: Optional[str]            # IMAGE, VIDEO, CAROUSEL_ALBUM
    media_url: Optional[str]             # 미디어 URL
    thumbnail_url: Optional[str]         # 썸네일 URL (비디오)
    caption: Optional[str]               # 캡션
    timestamp: Optional[datetime]        # 게시 시간
    permalink: Optional[str]             # 퍼머링크
    like_count: int = 0                  # 좋아요 수
    comments_count: int = 0              # 댓글 수
    children: Optional[list[Media]]      # 캐러셀 하위 미디어

MediaList

미디어 목록 응답 모델입니다.

class MediaList(BaseModel):
    data: list[Media]                    # 미디어 목록
    paging: Optional[dict[str, Any]]     # 페이지네이션 정보

    @property
    def next_cursor(self) -> Optional[str]:
        """다음 페이지 커서"""

MediaContainer

미디어 컨테이너 상태 모델입니다.

class MediaContainer(BaseModel):
    id: str                              # 컨테이너 ID
    status_code: Optional[str]           # IN_PROGRESS, FINISHED, ERROR
    status: Optional[str]                # 상태 메시지

    @property
    def is_finished(self) -> bool: ...

    @property
    def is_error(self) -> bool: ...

    @property
    def is_in_progress(self) -> bool: ...

사용 예제

미디어 목록 조회 및 출력

import asyncio
from poc.instagram import InstagramClient

async def main():
    async with InstagramClient(access_token="YOUR_TOKEN") as client:
        media_list = await client.get_media_list(limit=10)

        for media in media_list.data:
            print(f"[{media.media_type}] {media.caption[:30] if media.caption else '(캡션 없음)'}")
            print(f"  좋아요: {media.like_count:,} | 댓글: {media.comments_count:,}")
            print(f"  링크: {media.permalink}")
            print()

asyncio.run(main())

이미지 게시

async def post_image():
    async with InstagramClient(access_token="YOUR_TOKEN") as client:
        media = await client.publish_image(
            image_url="https://cdn.example.com/photo.jpg",
            caption="오늘의 사진 #photography"
        )
        return media.permalink

permalink = asyncio.run(post_image())
print(f"게시됨: {permalink}")

멀티테넌트 병렬 게시

여러 사용자가 동시에 게시물을 올리는 예제입니다.

import asyncio
from poc.instagram import InstagramClient

async def post_for_user(user_id: str, token: str, image_url: str, caption: str):
    """특정 사용자의 계정에 게시"""
    async with InstagramClient(access_token=token) as client:
        media = await client.publish_image(image_url=image_url, caption=caption)
        return {"user_id": user_id, "permalink": media.permalink}

async def main():
    users = [
        {"user_id": "user1", "token": "TOKEN1", "image": "https://...", "caption": "User1 post"},
        {"user_id": "user2", "token": "TOKEN2", "image": "https://...", "caption": "User2 post"},
        {"user_id": "user3", "token": "TOKEN3", "image": "https://...", "caption": "User3 post"},
    ]

    # 병렬 실행
    tasks = [
        post_for_user(u["user_id"], u["token"], u["image"], u["caption"])
        for u in users
    ]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    for result in results:
        if isinstance(result, Exception):
            print(f"실패: {result}")
        else:
            print(f"성공: {result['user_id']} -> {result['permalink']}")

asyncio.run(main())

페이지네이션으로 전체 미디어 조회

async def get_all_media(client: InstagramClient, max_items: int = 100):
    """전체 미디어 조회 (페이지네이션)"""
    all_media = []
    cursor = None

    while len(all_media) < max_items:
        media_list = await client.get_media_list(limit=25, after=cursor)
        all_media.extend(media_list.data)

        if not media_list.next_cursor:
            break
        cursor = media_list.next_cursor

    return all_media[:max_items]

내부 동작 원리

HTTP 클라이언트 생명주기

async with InstagramClient(...) as client:
    │
    ├── __aenter__()
    │   └── httpx.AsyncClient 생성
    │
    ├── API 호출들...
    │   └── 동일한 HTTP 클라이언트 재사용 (연결 풀링)
    │
    └── __aexit__()
        └── httpx.AsyncClient.aclose()

미디어 게시 프로세스

Instagram API의 미디어 게시는 3단계로 진행됩니다:

┌─────────────────────────────────────────────────────────┐
│                   미디어 게시 프로세스                    │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Step 1: Container 생성                                 │
│  POST /{account_id}/media                               │
│  ├── image_url / video_url 전달                         │
│  └── Container ID 반환                                  │
│                                                         │
│  Step 2: Container 상태 대기 (폴링)                      │
│  GET /{container_id}?fields=status_code                 │
│  ├── IN_PROGRESS: 계속 대기                             │
│  ├── FINISHED: 다음 단계로                              │
│  └── ERROR: ContainerStatusError 발생                   │
│                                                         │
│  Step 3: 게시                                           │
│  POST /{account_id}/media_publish                       │
│  └── Media ID 반환                                      │
│                                                         │
└─────────────────────────────────────────────────────────┘

캐러셀 게시 프로세스

┌─────────────────────────────────────────────────────────┐
│                 캐러셀 게시 프로세스                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Step 1: 각 이미지 Container 병렬 생성                   │
│  ├── asyncio.gather()로 동시 실행                       │
│  └── children_ids = [id1, id2, id3, ...]               │
│                                                         │
│  Step 2: 캐러셀 Container 생성                          │
│  POST /{account_id}/media                               │
│  ├── media_type: "CAROUSEL"                             │
│  └── children: "id1,id2,id3"                            │
│                                                         │
│  Step 3: Container 상태 대기                            │
│                                                         │
│  Step 4: 게시                                           │
│                                                         │
└─────────────────────────────────────────────────────────┘

자동 재시도 로직

retry_base_delay = 1.0

for attempt in range(max_retries + 1):
    try:
        response = await client.request(...)

        if response.status_code == 429:  # Rate Limit
            wait_time = max(retry_base_delay * (2 ** attempt), retry_after)
            await asyncio.sleep(wait_time)
            continue

        if response.status_code >= 500:  # 서버 에러
            wait_time = retry_base_delay * (2 ** attempt)
            await asyncio.sleep(wait_time)
            continue

        return response.json()

    except httpx.HTTPError:
        wait_time = retry_base_delay * (2 ** attempt)
        await asyncio.sleep(wait_time)
        continue

계정 ID 캐싱

계정 ID는 첫 조회 후 캐시됩니다:

async def _get_account_id(self) -> str:
    if self._account_id:
        return self._account_id  # 캐시 반환

    async with self._account_id_lock:  # 동시성 안전
        if self._account_id:
            return self._account_id

        response = await self._request("GET", "me", {"fields": "id"})
        self._account_id = response["id"]
        return self._account_id

API 제한사항

Rate Limits

제한 설명
시간당 요청 200회 사용자 토큰당
일일 게시 25개 계정당 (공식 문서 확인 필요)

미디어 요구사항

이미지:

  • 형식: JPEG 권장
  • 최소 크기: 320x320 픽셀
  • 비율: 4:5 ~ 1.91:1

비디오:

  • 형식: MP4 (H.264)
  • 길이: 3초 ~ 60분 (릴스)
  • 해상도: 최소 720p
  • 비율: 9:16 (세로), 16:9 (가로), 1:1 (정사각형)

캐러셀:

  • 이미지 수: 2-10개
  • 각 이미지는 위 요구사항 충족 필요

URL 요구사항

게시할 미디어 URL은:

  • HTTPS 프로토콜 권장
  • 공개적으로 접근 가능 (인증 없이)
  • CDN 또는 S3 등의 공개 URL 사용

참고 문서