
이번에는 iTunes Search API를 사용하여, AI 도서 추천 앱을 만들어봅시다.
iTunes Search API는 Apple이 제공하는 미디어(도서/음악/앱 등) 정보를 취득할 수 있는 인증 키 없이 무료로 사용 가능한 공개 API 입니다.
이 앱을 통해 "Streamlit에 의한 UI 구축"으로, 사용자로부터 입력을 받아 처리하는 인터랙티브 화면을 만드는 방법을 배워나갑시다.
완성 앱의 흐름:
사용자가 키워드 + 고민을 폼에 입력
↓ [AI에게 물어보기] 버튼 클릭
iTunes Search API에서 관련 도서 정보 취득
↓ Gemini에게 프롬프트 전달
AI가 도서를 분석하여 최적의 한 권 추천
↓
브라우저에 추천 도서·이유·표지 이미지 표시
지금까지 Streamlit은 st.title(), st.markdown() 등 "표시 전용"으로만 사용해왔습니다.
여기서부터는 "사용자의 입력을 받아서 처리하는" 인터랙티브한 화면을 만드는 방법을 배웁니다.
| 역할 | 함수 | 설명 |
|---|---|---|
| 표시 (지금까지) | st.title() st.markdown() st.subheader() | 텍스트·마크다운 출력 |
| 한 줄 입력 | st.text_input("라벨") | 키워드 등 짧은 입력 |
| 여러 줄 입력 | st.text_area("라벨") | 고민·설명 등 긴 입력 |
| 숫자 선택 | st.slider("라벨", 최솟값, 최댓값, 기본값) | 건수 등 범위 지정 |
| 폼 묶기 | st.form("ID") | 입력 부품을 하나로 묶기 |
| 전송 버튼 | st.form_submit_button("버튼명") | 버튼 클릭 시 처리 실행 |
| 로딩 표시 | st.spinner("메시지") | 처리 중 스피너 표시 |
st.form()이 필요한 이유Streamlit은 기본적으로 입력값이 바뀔 때마다 스크립트 전체를 재실행합니다.
키워드를 한 글자씩 입력할 때마다 API 호출이 발생하면 불필요한 통신과 비용이 생깁니다.
# ❌ form 없음: 슬라이더를 조금만 움직여도 즉시 API 호출 발생
item_count = st.slider("건수", 1, 20, 10)
titles = get_velog_titles(query, item_count) # 조작할 때마다 실행됨
# ✅ form 있음: [전송] 버튼을 눌렀을 때만 API 호출
with st.form("my_form"):
item_count = st.slider("건수", 1, 20, 10)
submitted = st.form_submit_button("검색")
if submitted:
titles = get_velog_titles(query, item_count) # 버튼 클릭 시에만 실행
st.form() 기본 구조with st.form("고유한 폼 ID"):
변수명1 = st.text_input("라벨명") # 한 줄 텍스트 입력
변수명2 = st.text_area("라벨명") # 여러 줄 텍스트 입력
변수명3 = st.slider("라벨명", 최솟값, 최댓값, 기본값) # 슬라이더
submitted = st.form_submit_button("버튼명") # 전송 버튼
if submitted:
# 전송 버튼이 눌렸을 때만 실행되는 처리
8장에서 만든 get_velog_titles() 함수는 item_count만 인수로 받았습니다.
여기서 query 인수를 추가하여, 지정한 태그(키워드)로 게시글을 필터링할 수 있도록 개량합니다.
변경 전 → 변경 후:
# 변경 전: 건수만 지정
def get_velog_titles(item_count): ...
# posts(limit: {item_count})
# 변경 후: 키워드(태그) + 건수 지정
def get_velog_titles(query, item_count): ...
# posts(limit: {item_count}, tag: "{query}")
GraphQL 쿼리에서 tag: "{query}" 부분이 핵심입니다. query 변수에 "python" 이 들어오면, posts(limit: 10, tag: "python") 이라는 쿼리가 완성됩니다. f-string의 { } 와 GraphQL 쿼리의 {{ }} 를 혼동하지 않도록 주의하세요. Python f-string 안에서 GraphQL의 중괄호를 표현하려면 {{ / }} 로 이스케이프해야 합니다.
# ⚠️ f-string과 GraphQL 중괄호 혼용 시 주의
graphql_query = {
"query": f"""
query {{ ← GraphQL의 {{ }} 는 f-string 이스케이프
posts(limit: {item_count}, tag: "{query}") {{ ← {item_count}, {query} 는 f-string 변수
title
}}
}}
"""
}
개조버전 get_velog_titles 함수 전체 코드:
def get_velog_titles(query, item_count):
url = "https://v2.velog.io/graphql"
graphql_query = {
"query": f"""
query {{
posts(limit: {item_count}, tag: "{query}") {{
id
title
short_description
tags
}}
}}
"""
}
try:
response = requests.post(
url,
json=graphql_query,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
posts = result.get("data", {}).get("posts", [])
titles = []
for post in posts:
title = post.get("title")
if title:
titles.append(title)
return titles
except Exception as e:
print(f"Velog에서 데이터 취득에 실패했습니다: {e}")
return []
Streamlit 폼과 조합한 전체 코드:
import requests
import streamlit as st
# 【여기서부터 함수】
def get_velog_titles(query, item_count):
"""Velog API에서 키워드(태그)로 필터링한 게시글 제목 목록을 취득하는 함수"""
url = "https://v2.velog.io/graphql"
graphql_query = {
"query": f"""
query {{
posts(limit: {item_count}, tag: "{query}") {{
id
title
short_description
tags
}}
}}
"""
}
try:
response = requests.post(
url,
json=graphql_query,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
posts = result.get("data", {}).get("posts", [])
titles = []
for post in posts:
title = post.get("title")
if title:
titles.append(title)
return titles
except Exception as e:
print(f"Velog에서 데이터 취득에 실패했습니다: {e}")
return []
# 【여기까지 함수】
st.title("최신 Velog 게시글 제목 취득 앱")
# with 블록으로 폼을 만든다 (with 블록 안에 입력란과 버튼을 배치)
with st.form("velog_form"):
query = st.text_input("검색 키워드(태그)를 입력해주세요 (예: python, react, ai)")
item_count = st.slider("취득할 게시글 수", 1, 20, 10)
submitted = st.form_submit_button("제목 목록을 취득")
# with 블록(폼)에서 전송 버튼이 눌렸을 때의 처리
if submitted:
if not query:
st.warning("키워드를 입력해주세요.")
else:
titles = get_velog_titles(query, item_count)
if titles:
st.markdown(f"### 「{query}」 관련 게시글 제목 ({len(titles)}건):")
for title in titles:
st.markdown(f"- {title}")
else:
st.warning("게시글을 찾을 수 없습니다. 다른 키워드를 시도해보세요.")
위의 폼 패턴을 응용하여, iTunes Search API + Gemini AI를 결합한 도서 추천 앱을 만들어봅시다.
아래 코드를 st_ebook.py라는 이름으로 저장하세요.
import os
from langchain_google_genai import GoogleGenerativeAI
from dotenv import load_dotenv
import requests
import streamlit as st
load_dotenv()
def search_itunes_books(keyword="Python", limit=10):
url = "https://itunes.apple.com/search"
params = {
"term": keyword,
"country": "kr", # 한국 앱스토어 기준
"media": "ebook",
"limit": limit
}
try:
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
return data.get("results", [])
except Exception as e:
print(f"통신 에러:{e}")
return []
def ask_gemini(prompt):
google_api_key = os.getenv("GOOGLE_API_KEY")
if not google_api_key:
st.error("GOOGLE_API_KEY가 설정되어 있지 않습니다. .env를 확인해주세요.")
st.stop() # Streamlit을 안전하게 정지시킨다
try:
gemini = GoogleGenerativeAI(
google_api_key=google_api_key,
model="gemini-2.5-flash",
max_retries=1
)
response = gemini.invoke(prompt)
return response
except Exception as e:
st.warning(f"Gemini 에러:{e}")
return None
st.title("AI 도서 추천 앱")
st.subheader("AI가 추천 도서를 알려줍니다.")
with st.form("book_info"):
keyword = st.text_input("관심 있는 토픽의 키워드를 입력해주세요(예:파이썬).")
issue = st.text_area("독서를 통해 해결하고 싶은 고민을 입력해주세요(예:파이썬 기초를 익히고 싶다).")
limit = st.slider("몇 권 정도의 책을 비교 검토하고 싶으신가요?", 2, 50, 10)
submitted = st.form_submit_button("AI에게 물어보기")
if submitted:
# 사용자에게 처리 중임을 알리는 스피너를 표시
with st.spinner("도서를 검색하고, AI가 분석 중입니다..."):
books = search_itunes_books(keyword, limit)
prompt = f"""
다음의 【도서 목록】과 【사용자의 과제】를 바탕으로 최적의 한 권을 추천하고, 그 이유와 URL을 알려주세요.\n
또한 "책 표지 이미지"를 Markdown으로 표시해주세요.\n
\n
【도서 목록】\n\n
{books}
\n
【사용자의 과제】\n\n
{issue}
"""
answer = ask_gemini(prompt)
if answer:
st.success("분석이 완료되었습니다!")
st.markdown(answer)
if st.button("다시 물어보기"):
st.rerun()
search_itunes_books() — iTunes Search API 호출params = {
"term": keyword, # 검색 키워드
"country": "kr", # 한국 앱스토어 기준
"media": "ebook", # 전자책만 검색
"limit": limit # 취득 건수
}
response = requests.get(url, params=params)
iTunes Search API의 country 파라미터는 검색 결과의 국가 스토어를 지정합니다. "kr"로 설정하면 한국 앱스토어 기반의 도서 정보가 반환됩니다. 반환되는 도서 정보 딕셔너리에는 제목(trackName), 저자(artistName), 표지 이미지 URL(artworkUrl100), 구매 링크(trackViewUrl) 등이 포함되어 있습니다.
media 파라미터로 다른 미디어도 검색 가능:
media 값 | 검색 대상 |
|---|---|
"ebook" | 전자책 |
"music" | 음악 |
"podcast" | 팟캐스트 |
"software" | 앱 |
"all" | 전체 |
response.raise_for_status() — HTTP 에러 자동 처리:
# 지금까지의 방식: 수동으로 상태 코드 확인
if response.status_code == 200:
...
else:
st.warning(f"에러: {response.status_code}")
# raise_for_status() 방식: 200이 아니면 자동으로 예외 발생 → try-except가 처리
response.raise_for_status()
ask_gemini() — st.stop() vs exit(1)if not google_api_key:
st.error("GOOGLE_API_KEY가 설정되어 있지 않습니다.")
st.stop() # ← Streamlit 앱에서는 exit(1) 대신 st.stop()
exit(1) | st.stop() | |
|---|---|---|
| 사용 장소 | 일반 Python 스크립트 | Streamlit 앱 |
| 동작 | 프로세스 강제 종료 | 이후 코드 실행을 안전하게 중단 |
| 화면 | 에러 화면이 될 수 있음 | 에러 메시지만 표시하고 멈춤 |
Streamlit 앱에서는 exit(1) 대신 st.stop()을 사용하는 것이 올바른 패턴입니다.
with st.spinner(...) — 처리 중 로딩 표시with st.spinner("도서를 검색하고, AI가 분석 중입니다..."):
books = search_itunes_books(keyword, limit) # ← 수초 소요
answer = ask_gemini(prompt) # ← 수초~수십 초 소요
with st.spinner(...) 블록 안의 처리가 실행되는 동안 화면에 회전하는 로딩 아이콘 이 표시됩니다. 처리가 완료되면 스피너는 사라지고 결과가 표시됩니다. 네트워크 통신이나 AI API 호출처럼 시간이 걸리는 처리에는 반드시 스피너를 사용하여 사용자가 "멈췄나?"라고 느끼지 않도록 합니다.
st.success() / st.button() / st.rerun()if answer:
st.success("분석이 완료되었습니다!") # 🟢 초록 박스
st.markdown(answer)
if st.button("다시 물어보기"):
st.rerun() # 스크립트 전체를 처음부터 재실행
st.form_submit_button() vs st.button() 용도 차이:
# st.form_submit_button: 반드시 with st.form() 블록 안에서 사용
with st.form("my_form"):
submitted = st.form_submit_button("검색") # 폼 전체를 전송
# st.button: 폼 밖에서 사용. 클릭 즉시 처리 실행
if st.button("다시 물어보기"):
st.rerun() # 앱 재시작
사용자가 폼에 keyword + issue + limit 입력
↓ [AI에게 물어보기] 버튼 클릭 → submitted = True
with st.spinner("분석 중...") 스피너 표시 시작
↓
search_itunes_books(keyword, limit)
→ requests.get(iTunes URL, params=딕셔너리)
→ books = [{"trackName": "책1", "artworkUrl100": "...", ...}, ...]
↓
f-string으로 books + issue → prompt 조립
↓
ask_gemini(prompt)
→ gemini.invoke(prompt)
→ answer = "추천 도서: ... 이유: ... "
↓
스피너 종료 → st.success() + st.markdown(answer)
브라우저에 AI 추천 결과 표시
media 파라미터를 바꾸면 도서 외에도 다양한 추천 앱을 만들 수 있습니다.
# 팟캐스트 추천 앱
def search_itunes_podcasts(keyword, limit=10):
params = {"term": keyword, "country": "kr", "media": "podcast", "limit": limit}
...
# 앱 추천 앱
def search_itunes_apps(keyword, limit=10):
params = {"term": keyword, "country": "kr", "media": "software", "limit": limit}
...
iTunes Search API는 인증 키가 필요 없으므로, .env 설정 없이 즉시 테스트할 수 있습니다. API 연동 연습 대상으로 최적인 API입니다.
| 개념 | 한 줄 요약 |
|---|---|
st.form("ID") | 입력 부품을 하나로 묶는 폼. 전송 버튼 클릭 시에만 처리 실행 |
st.text_input("라벨") | 한 줄 텍스트 입력란 |
st.text_area("라벨") | 여러 줄 텍스트 입력란 |
st.slider("라벨", 최솟값, 최댓값, 기본값) | 슬라이더로 숫자 선택 |
st.form_submit_button("버튼명") | 폼 전송 버튼. True / False 반환 |
st.button("버튼명") | 폼 밖에서 쓰는 일반 버튼 |
with st.spinner("메시지"): | 블록 처리 중 로딩 아이콘 표시 |
st.success() / st.warning() / st.error() | 색상별 알림 메시지 박스 |
st.rerun() | 앱 전체를 처음부터 재실행 |
st.stop() | Streamlit 앱을 안전하게 중단 (exit(1) 대신 사용) |
response.raise_for_status() | HTTP 에러 코드 자동 예외 처리 |
iTunes Search API country: "kr" | 한국 앱스토어 기반 검색. 인증 키 불필요 |
f-string + GraphQL {{ }} | GraphQL 쿼리 안에서 Python 변수를 삽입할 때 {{ / }} 로 이스케이프 |
©2024-2026 MDRULES.dev, Hand-crafted & made with Jaewoo Kim.
이메일문의: jaewoo@mdrules.dev
AI강의/개발/기술자문, AI 업무 자동화 컨설팅 문의: https://talk.naver.com/ct/w5umt5
AI 프롬프트 및 워크플로우 설계 대행: https://mdrules.dev
📌 Streamlit 폼 패턴 6단계
with st.form("form_id"): # ① 폼 시작 값1 = st.text_input("키워드") # ② 한 줄 입력 값2 = st.text_area("고민") # ③ 여러 줄 입력 값3 = st.slider("건수", 1, 50, 10) # ④ 슬라이더 submitted = st.form_submit_button("실행") # ⑤ 전송 버튼 if submitted: # ⑥ 버튼이 눌렸을 때만 처리 with st.spinner("처리 중..."): # ⑦ 무거운 처리는 스피너로 감싼다 result = api_call(값1, 값3) answer = ask_gemini(result) st.success("완료!") # ⑧ 성공 메시지 st.markdown(answer) # ⑨ 결과 표시