도서관 검색 AI를
Solar로 처음부터 만들기
서울 100여 개 도서관을 한 번에 검색하는 AI 에이전트를, 빈 폴더에서 한 걸음씩 다시 만듭니다.
이번 버전의 핵심 변화: OpenAI GPT-4o-mini 대신 Upstage Solar를 두뇌로 씁니다. 아래로 죽 내리면 전 과정이 한 페이지에 있습니다.
이게 뭔가요?
BookToss는 2025 IA×AI 해커톤에서 만든 프로젝트입니다. 서울의 각 구청 도서관 사이트는 제각각이고 API도 없어서, 책 한 권을 찾으려면 사이트마다 들어가 팝업을 닫고 검색을 반복해야 합니다. BookToss는 이 과정을 AI 에이전트가 브라우저를 직접 조작해 한 번에 처리합니다.
이 페이지는 그 프로젝트를 완전히 처음부터 다시 만드는 과정을 누구나 따라 할 수 있도록 단계별로 기록한 튜토리얼입니다. 각 단계마다 무엇을 했는지뿐 아니라 왜 그렇게 했는지를 함께 적었습니다.
따라 하는 개발자 — 명령어를 그대로 복사해 같은 결과를 재현할 수 있습니다.
저장소를 구경하는 사람 — 프로젝트가 어떻게 자랐는지 스크롤 한 번으로 봅니다.
Claude (AI 페어) — 다음 단계를 이어갈 때 이 문서를 맥락으로 참고합니다.
왜 Solar로 바꾸나요?
원본은 OpenAI GPT-4o-mini로 브라우저를 조작했습니다. Upstage의 Solar는 OpenAI 호환 엔드포인트를 제공하는 한국형 LLM이라, 인터페이스를 거의 바꾸지 않고 두뇌만 교체할 수 있습니다. 이 교체가 얼마나 쉬운지/어디서 막히는지를 직접 겪어보는 것이 이 재구축의 학습 목표 중 하나입니다.
Solar는 https://api.upstage.ai/v1에서 OpenAI 호환 Chat Completions를
제공합니다. 모델은 solar-pro2(31B sLLM, 빠른 chat 모드 / 추론 모드 선택 가능).
browser-use의 ChatOpenAI 래퍼에 base_url과 api_key만
Solar로 바꿔주면 됩니다 — 별도 어댑터가 필요 없습니다.
전체 그림 (목표 구조)
| 레이어 | 기술 | 역할 |
|---|---|---|
| LLM | Upstage Solar (solar-pro2) | 브라우저를 어떻게 조작할지 판단 |
| 에이전트 | browser-use | 각 도서관 사이트 자동 검색 |
| 파이프라인 | LangGraph | resolve_catalog → search_book → parse_html |
| 앱 | Streamlit | 웹 UI · 지도 |
아래 로드맵을 차례로 따라가면 이 구조가 한 조각씩 완성됩니다.
로드맵 — v0.0.1 → v0.1.0
"한 번에 다 만들기"는 막히면 어디서 틀어졌는지 알 수 없습니다. 그래서
각 버전이 끝나면 눈으로 확인할 수 있는 결과물이 남도록 10개의 작은
단계로 쪼갰습니다. v0.1.0의 목표는 거창하지 않습니다 —
도서관 딱 한 곳에서 책 한 권을 검색해 결과를 화면에 띄우는 것.
그 한 줄기 흐름(end-to-end)이 통하면 나머지는 도서관을 늘리는 일입니다.
| 버전 | 이 버전이 끝나면… | 핵심 기술 | 상태 |
|---|---|---|---|
| v0.0.1 | Streamlit "Hello, BookToss" 화면이 브라우저에 뜬다 | Streamlit | ✅ 완료 |
| v0.0.2 | 터미널에서 Solar API에 첫 호출이 성공한다 | Upstage Solar · .env | ⏳ 다음 |
| v0.0.3 | Streamlit 입력창에 쓰면 Solar의 답이 화면에 나온다 | Streamlit + Solar | 예정 |
| v0.0.4 | 코드를 src/ 구조로 정리한다 (llm·nodes·graph 분리) | 패키지 레이아웃 | 예정 |
| v0.0.5 | Playwright로 브라우저를 열고 어떤 페이지의 제목을 읽어온다 | Playwright | 예정 |
| v0.0.6 | 설정에서 도서관 한 곳의 검색 URL을 찾아낸다 (resolve_catalog) | YAML · LangGraph 노드 | 예정 |
| v0.0.7 | 그 도서관 페이지에서 검색창을 찾아 책 제목을 입력·제출한다 | Playwright | 예정 |
| v0.0.8 | Solar가 페이지를 보고 "어떻게 검색할지"를 판단한다 | Solar + 브라우저 제어 | 예정 |
| v0.0.9 | 검색 결과 HTML에서 책 정보(제목·저자·대출가능)를 뽑아낸다 | BeautifulSoup (parse_html) | 예정 |
| v0.1.0 | 🎉 도서관 1곳 end-to-end: 검색어 입력 → 결과를 화면에 표시 | 전체 통합 | 🎯 목표 |
위로 갈수록 "기반 다지기"(UI·API·구조), 아래로 갈수록 "실제 도서관 다루기"입니다. 각 줄은 독립적으로 동작하는 작은 마일스톤이라, 하나씩 PR로 머지하며 진행합니다. 진행하면서 이 표의 상태가 ✅로 바뀌고, 해당 버전의 상세 섹션이 이 아래에 채워집니다.
깨끗한 출발점 만들기
"처음부터 다시"라면, 정말로 빈 폴더에서 시작해야 합니다. 단, 옛 코드는 버리지 않고 참고용으로 보존합니다.
목표
- 기존 프로젝트 위에 덧칠하지 않고 빈 루트에서 새로 시작한다.
- 그러면서도
main의 옛 코드는 안전하게 남겨둔다. - 다시 쓸 가치가 있는 자산(도서관 URL 맵, 파싱 규칙)은 손 닿는 곳에 둔다.
왜 새 브랜치인가, 왜 orphan인가
같은 저장소 안에서 새 시도를 하려면 새 브랜치를 파는 게 자연스럽습니다. 그래야 안정된 상태를 건드리지 않고 자유롭게 실험할 수 있으니까요.
일반 git checkout -b는 기존 커밋 이력을 그대로 물려받습니다.
--orphan은 이력이 0인 브랜치를 만듭니다. 우리가 만들 첫 커밋이
이 브랜치의 진짜 첫 줄이 되죠. "from scratch"라는 의도와 가장 잘 맞습니다.
1. orphan 브랜치 생성
git checkout --orphan v2-solar
git reset # 옛 파일들을 스테이지에서 내려놓음 (작업트리는 그대로)
2. 옛 코드를 docs/v1/ 로 이동
mkdir -p docs/v1
git mv 00_src README.md app.py requirements.txt docs/v1/
mv "BookToss_시연영상2.mp4" *.hwp *.pdf docs/v1/ # 추적 안 되던 자료들도 함께
재구축의 두 핵심 자산 — 서울 도서관 100여 곳의 포털 URL을 담은
catalog_index.yaml, 그리고 구청 사이트별 HTML 파싱 규칙 — 은 처음부터
다시 만들 필요가 없습니다. 바퀴를 재발명하지 않고 "참고해서 골라 옮기는" 것이
현명한 재시작입니다.
3. 큰 바이너리는 git에서 제외
# .gitignore
.env
__pycache__/
docs/v1/*.mp4
docs/v1/*.hwp
docs/v1/*.pdf
docs/v1/00_src/data/raw/
4. 출발점 커밋
git add -A
git commit -m "chore: start v2-solar from scratch; archive v1 under docs/v1"
결과
repo 루트
├── README.md ← v2 의도 + 진행표
├── .gitignore ← .env, 캐시, 큰 미디어 제외
└── docs/
├── index.html ← 지금 보고 있는 이 페이지
└── v1/ ← 기존 해커톤 프로젝트 통째로 보존
└── 00_src/configs/catalog_index.yaml ← 나중에 재활용
같은 저장소 안에 두 개의 독립된 역사가 공존하다가, 재구축 결과가 무르익으면
main으로 합쳐집니다. 옛 코드는 docs/v1/에 박제되어 사라지지 않습니다.
Streamlit "Hello, BookToss"
첫 결과물은 거창하지 않습니다 — 브라우저에 화면 하나를 띄우는 것. 이후 모든 단계의 결과를 볼 자리를 먼저 만듭니다.
왜 UI를 가장 먼저?
Solar 응답도, 도서관 검색 결과도, 결국 어딘가에 보여줘야 합니다. 화면을 먼저
세워두면 다음 단계부터는 print 대신 실제 화면으로 결과를 확인할 수 있습니다.
Streamlit은 파이썬 스크립트 하나로 웹 UI를 그려주는
도구라, 가장 빠르게 "보이는 것"을 만들 수 있습니다.
1. Streamlit 설치
# requirements.txt
streamlit
# 설치
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
2. app.py — 화면의 뼈대
페이지 설정과 제목까지는 정해진 틀입니다. 그 아래 첫 화면 본문은 직접 채워봅니다 (아래 "직접 해보기").
# app.py
import streamlit as st
st.set_page_config(page_title="BookToss", page_icon="📚", layout="centered")
st.title("📚 BookToss")
st.caption("서울 도서관을 한 번에 검색하는 AI 에이전트 · v0.0.1")
# 👉 여기에 첫 화면 본문을 직접 채웁니다 (TODO(human))
3. 실행
streamlit run app.py
# 브라우저가 자동으로 http://localhost:8501 을 엽니다
streamlit run은 스크립트를 위→아래로 통째로 다시 실행하며
화면을 그립니다. 위젯과 상호작용할 때마다 전체가 재실행돼요. 그래서 "상태"는 보통
st.session_state로 따로 관리합니다 — 지금은 몰라도 되고, v0.0.3에서 만납니다.
첫 화면에 뭘 보여줄까 — 이렇게 채웠습니다
"무엇이 보일지"를 정하는 자리입니다. 이번엔 한 줄 소개 + 책 제목 입력칸 + 검색 버튼을
뒀습니다. 버튼 로직은 아직 진짜 검색이 아니라 "다음 버전에서 연결된다"는 안내만 합니다 —
이 입력칸과 버튼이 v0.1.0의 실제 검색창으로 자랍니다.
st.write("서울 곳곳의 도서관을 **한 번에** 검색하는 AI 에이전트입니다. 찾는 책을 입력해 보세요.")
book = st.text_input("찾는 책 제목", placeholder="예: 코스모스")
if st.button("검색", type="primary"):
if book:
st.info(f"'{book}' 검색은 다음 버전에서 연결됩니다. (지금은 화면만!)")
else:
st.warning("책 제목을 입력해 주세요.")
없었습니다 — streamlit run app.py 한 번에 화면이 떴습니다. Streamlit이
"파이썬만으로 가장 빠르게 보이는 것"을 만들어주는 이유죠. (앞으로 막히는 단계가 나오면
이 칸에 솔직히 기록합니다.)
✅ v0.0.1 완료 — 화면이 뜬다. 다음은
v0.0.2 · Solar API 연결.
Solar API 연결
두뇌를 붙입니다. 터미널에서 Solar에 한 마디 건네고 답이 오는지 확인 — 이게 되면 이후 모든 LLM 단계의 토대가 섭니다.
핵심: Solar는 OpenAI 호환
Solar 전용 SDK는 필요 없습니다. 표준 openai 파이썬 SDK에서
base_url과 api_key만 Solar로 바꾸면 됩니다.
덕분에 LangChain·browser-use 같은 OpenAI 생태계 도구를 그대로 재사용할 수 있어요.
| 항목 | 값 |
|---|---|
| base_url | https://api.upstage.ai/v1 |
| 모델 | solar-pro2 |
| API 키 (env) | UPSTAGE_API_KEY — console.upstage.ai에서 발급 |
1. 의존성 + 키 보관
# requirements.txt 에 추가
openai
python-dotenv
pip install -r requirements.txt
키는 코드에 박지 않고 .env에 둡니다. .env는 git에 올리지 않고
(.gitignore에 포함), 대신 빈 틀 .env.example을 커밋해 둡니다.
# .env (git에 올리지 않음)
UPSTAGE_API_KEY=up_여기에_본인_키
2. solar.py — 클라이언트와 호출
load_dotenv()가 .env의 값을 환경변수로 불러오고, 그 키로
OpenAI 클라이언트를 Solar에 연결합니다. 실제 호출 한 줄은 직접 채웁니다.
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=KEY, base_url="https://api.upstage.ai/v1")
# 👉 chat 호출은 직접 (TODO(human))
resp = client.chat.completions.create(model="solar-pro2", messages=[...])
3. 실행
python solar.py
# Solar: 안녕하세요, 저는 ... ← 이런 응답이 뜨면 성공
(구현 후 채워집니다 — 키 오타, base_url 누락, 모델명 등 자주 막히는 자리.)
⏳ 진행 중. 응답이 오면 v0.0.2 완료. 다음은
v0.0.3 · Streamlit 입력 → Solar 응답 표시.