23 KiB
23 KiB
InstagramClient 사용 매뉴얼
Instagram Graph API를 사용한 콘텐츠 게시 및 조회를 위한 비동기 클라이언트입니다.
목차
개요
주요 특징
- 비동기 지원:
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_urlcaption,timestamp,permalinklike_count,comments_countchildren(캐러셀인 경우 하위 미디어)
사용 예제:
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}")
publish_carousel()
캐러셀(멀티 이미지)을 게시합니다.
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 사용