LangChain 챗봇7장 · 웹 UI 연동과 배포20 / 22← 이전목차다음 →MetaContents

7장. 웹 UI 연동과 배포

프로젝트 1 — 배포 점검

구름다리여행 약관 챗봇을 Streamlit Community Cloud 에 올리기 전에, 배포할 폴더가 준비되었는지 자동으로 점검하는 스크립트를 만들려고 합니다. 실습 6의 out_ch07_deploy/ 와, 일부러 실수를 넣은 out_ch07_deploy_bad/ 를 함께 점검해 결과를 out_ch07_deploy_check.csv 로 남기세요. 점검은 파일만 읽고 AppTest 를 연습 모드로 돌리므로 API 키가 없어도 됩니다. 실습 6을 먼저 해서 out_ch07_deploy/ 가 있어야 합니다. 약관은 연습용으로 지어낸 가상 문서입니다.

항목내용
점검할 폴더out_ch07_deploy/(실습 6), out_ch07_deploy_bad/(1단계에서 만든다)
일부러 넣은 실수앱 파일에 키 꼴의 글을 적은 줄, requirements.txt 에서 빠진 pypdf·faiss-cpu, 없는 .gitignore, 견본 대신 진짜 이름의 secrets.toml
점검 항목꼭 있어야 할 파일, 패키지 목록, 코드에 적힌 키, .gitignore, 비밀 값 견본, 첫 화면, 질문 하나
결과 파일out_ch07_deploy_check.csv(폴더, 점검, 결과, 까닭)

요구사항은 다섯 가지입니다.

  1. out_ch07_deploy/ 를 복사해 실수 넷을 넣은 out_ch07_deploy_bad/ 를 만듭니다.
  2. 파일만 읽는 점검 다섯 가지를 함수로 만듭니다. 꼭 있어야 할 파일(app.py, requirements.txt, travel_terms.pdf, .gitignore), 앱이 쓰는 패키지가 requirements.txt 에 모두 있는지, 코드와 설정 파일에 키 꼴의 글(sk- 로 시작하는 긴 글)이 없는지, .gitignore 가 .env 와 .streamlit/secrets.toml 을 막는지, 비밀 값은 견본(secrets.toml.example)만 있는지입니다.
  3. AppTest 로 돌려 보는 점검 두 가지를 만듭니다. 첫 화면이 오류 없이 뜨는지, 질문 하나에 답과 근거 조항이 나오는지입니다. 진짜 모델을 부르지 않도록 연습 모드로 돌립니다.
  4. 두 폴더를 점검해 한 표로 모으고 out_ch07_deploy_check.csv 로 저장합니다.
  5. 표를 보고 두 폴더가 배포할 수 있는 상태인지, 고칠 것은 무엇인지 두세 문장으로 적습니다.

단계별 힌트입니다.

  1. 폴더 복사는 shutil.copytree(원래, 새것, dirs_exist_ok=True) 입니다. 실수를 넣기 전에 shutil.rmtree 로 지난번 것을 지우면 몇 번을 돌려도 같은 결과가 납니다.
  2. 앱이 쓰는 패키지는 ast.parse 로 앱 코드를 읽어 import 줄의 맨 앞 이름(langchain_community 등)을 모으고, 패키지 이름(langchain-community)으로 바꿉니다. PyPDFLoader 가 쓰는 pypdf, FAISS 가 쓰는 faiss-cpu 는 import 에 안 보이므로 따로 더합니다.
  3. 키 꼴의 글은 re.search(r"sk-[A-Za-z0-9_-]{20,}", 글) 로 찾습니다. {20,} 은 "20글자 이상"입니다.
  4. AppTest 를 열 때 at.secrets["OPENAI_API_KEY"] = "발급받은_인증키" 를 넣으면 앱이 연습 모드가 됩니다. 시험 전에 st.cache_resource.clear() 로 캐시를 비웁니다.
  5. 점검마다 {"폴더": …, "점검": …, "결과": "통과" 또는 "고칠 것", "까닭": …} 사전을 만들어 목록에 모으면 pd.DataFrame 하나로 표가 됩니다.

확인할 것: 다 풀었으면 항목마다 스스로 확인해 체크해 보세요.

코드의 흐름

예시 풀이의 코드가 어떤 차례로 흘러가는지 한 장으로 그렸습니다. 풀기 전에 큰 줄기를 먼저 보고, 풀다가 막히면 지금 어느 상자에 있는지 확인해 보세요.

실수 넣은 폴더 만들기, 파일 점검 다섯 가지, AppTest 점검 두 가지, 표로 모아 저장하기의 네 단계를 이은 코드 흐름도

상자 하나가 예시 풀이의 단계 하나이고, 회청 화살표를 따라가면 실행 차례입니다. 상자 아래의 글은 그 단계에서 쓰는 핵심 코드입니다. 2단계는 파일을 읽기만 하는 점검이라 빠르고, 3단계는 앱을 실제로 돌려 보는 점검이라 몇 초 걸립니다. 4단계에서 두 폴더의 결과를 한 표에 모읍니다.

실제로 배포하려면

점검을 모두 통과한 out_ch07_deploy/ 를 실제로 올리는 차례입니다. 이 교안을 만들 때는 GitHub 와 Streamlit Community Cloud 에 올리지 않았고, 아래 화면 이름은 Streamlit 공식 문서(docs.streamlit.io)의 배포 안내에서 읽은 것이라 실제 화면과 대조하지 못했습니다. 화면의 이름이 다르면 공식 문서의 최신 안내를 따르세요. 무료로 쓸 수 있는 조건과 한도도 공식 문서에서 확인합니다.

출력
(실행하지 않은 안내. 화면에서 하는 일)
1. GitHub 에 빈 저장소를 만들고, 본문 "배포 과정" 절의 git 명령으로 out_ch07_deploy 폴더를 올린다
   - 올린 뒤 GitHub 저장소 화면에서 .env 와 .streamlit/secrets.toml 이 없는지 눈으로 확인한다
2. share.streamlit.io 에 GitHub 계정으로 로그인한다
3. 작업 공간 오른쪽 위의 "Create app" 을 누르고, 이미 만든 앱이 있다고 고른다
4. Repository(저장소), Branch(가지: main), Main file path(앱 파일: app.py) 를 고른다
   - App URL 칸에서 주소의 앞부분을 정할 수 있다(예: gureum-docbot)
5. "Advanced settings" 를 열어
   - Python version 에서 내 PC 와 같거나 가까운 판을 고른다
   - Secrets 칸에 secrets.toml 의 내용을 붙여 넣는다
       OPENAI_API_KEY = "발급받은_인증키"   ← 여기에 진짜 키
       OPENAI_MODEL = "gpt-4.1-mini"
   - 저장한다
6. 배포 단추를 누르고, 설치 로그가 끝날 때까지 기다린다
7. https://gureum-docbot.streamlit.app 꼴의 주소로 들어가 본문 "최종 동작 테스트" 절의 점검표를 하나씩 확인한다
8. 앱을 고치면 같은 저장소에 git push 한다. Community Cloud 가 새 코드를 받아 다시 띄운다

수업용 키로 앱을 계속 공개해 두면 주소를 아는 누구의 질문에도 요금이 붙습니다. 확인이 끝나면 앱 설정에서 앱을 지우거나 Secrets 의 키를 빼 두세요.

결과 보기 (예시 풀이)

1단계. 실수를 넣은 폴더 만들기

Python
import shutil
from pathlib import Path

GOOD = Path("out_ch07_deploy")
BAD = Path("out_ch07_deploy_bad")

# 지난번에 만든 것을 지우고 실습 6의 폴더를 그대로 복사한다
shutil.rmtree(BAD, ignore_errors=True)
shutil.copytree(GOOD, BAD)

# 실수 1: 앱 파일 맨 위에 키 꼴의 글을 적은 줄(0 으로 채운 가짜 값)
app = (BAD / "app.py").read_text(encoding="utf-8")
(BAD / "app.py").write_text('OPENAI_API_KEY = "sk-proj-' + "0" * 24 + '"   # 지우는 것을 잊은 줄\n' + app, encoding="utf-8")
# 실수 2: requirements.txt 에서 pypdf 와 faiss-cpu 를 뺀다(import 에 안 보여 잊기 쉽다)
req = (BAD / "requirements.txt").read_text(encoding="utf-8").splitlines()
(BAD / "requirements.txt").write_text("\n".join(l for l in req if not l.startswith(("pypdf", "faiss-cpu"))) + "\n", encoding="utf-8")
# 실수 3: .gitignore 를 지운다
(BAD / ".gitignore").unlink()
# 실수 4: 견본 대신 진짜 이름의 secrets.toml 을 둔다(값은 자리표시)
(BAD / ".streamlit" / "secrets.toml.example").rename(BAD / ".streamlit" / "secrets.toml")

for folder in [GOOD, BAD]:
    print(folder, "→", sorted(p.relative_to(folder).as_posix() for p in folder.rglob("*") if p.is_file()))
출력
out_ch07_deploy → ['.gitignore', '.streamlit/secrets.toml.example', 'app.py', 'requirements.txt', 'travel_terms.pdf']
out_ch07_deploy_bad → ['.streamlit/secrets.toml', 'app.py', 'requirements.txt', 'travel_terms.pdf']

코드가 의미하는 것 shutil.rmtree 로 지난번 폴더를 지우고 shutil.copytree 로 좋은 폴더를 그대로 복사한 뒤, 배포에서 흔한 실수 넷을 일부러 넣었습니다. 키 꼴의 글은 0 으로 채운 가짜 값이라 실제 키가 아닙니다. 점검 스크립트가 실수를 잡는지 보려면 "잡혀야 하는 폴더"가 있어야 하므로 이 단계를 먼저 둡니다.

2단계. 파일 점검 다섯 가지

Python
import ast
import re

REQUIRED_FILES = ["app.py", "requirements.txt", "travel_terms.pdf", ".gitignore"]
# import 이름 → 패키지 이름. import 에 안 보이는 것은 HIDDEN 에 따로 적는다
IMPORT_TO_PACKAGE = {
    "streamlit": "streamlit", "dotenv": "python-dotenv",
    "langchain_core": "langchain-core", "langchain_community": "langchain-community",
    "langchain_classic": "langchain-classic", "langchain_openai": "langchain-openai",
    "langchain_text_splitters": "langchain-text-splitters",
}
HIDDEN = {"PyPDFLoader": "pypdf", "FAISS": "faiss-cpu"}
KEY_PATTERN = re.compile(r"sk-[A-Za-z0-9_-]{20,}")


def row(folder, name, ok, why):
    return {"폴더": folder.name, "점검": name, "결과": "통과" if ok else "고칠 것", "까닭": why}


def check_files(folder):
    missing = [f for f in REQUIRED_FILES if not (folder / f).exists()]
    return row(folder, "꼭 있어야 할 파일", not missing, "빠진 파일: " + ", ".join(missing) if missing else "네 개 모두 있음")


def check_requirements(folder):
    code = (folder / "app.py").read_text(encoding="utf-8")
    # ast.parse: 코드를 실행하지 않고 문법 나무로 읽는다. ast = abstract syntax tree(추상 구문 나무)
    tree = ast.parse(code)
    mods = {n.module.split(".")[0] for n in ast.walk(tree) if isinstance(n, ast.ImportFrom) and n.module}
    mods |= {a.name.split(".")[0] for n in ast.walk(tree) if isinstance(n, ast.Import) for a in n.names}
    need = {IMPORT_TO_PACKAGE[m] for m in mods if m in IMPORT_TO_PACKAGE}
    need |= {pkg for word, pkg in HIDDEN.items() if word in code}
    req = (folder / "requirements.txt").read_text(encoding="utf-8")
    have = {line.split("==")[0].strip().lower() for line in req.splitlines() if line.strip()}
    missing = sorted(need - have)
    return row(folder, "패키지 목록", not missing, "빠진 패키지: " + ", ".join(missing) if missing else f"필요한 {len(need)}개 모두 있음")


def check_keys(folder):
    found = []
    for f in folder.rglob("*"):
        if f.is_file() and f.suffix in {".py", ".toml", ".txt", ".example", ""}:
            if KEY_PATTERN.search(f.read_text(encoding="utf-8", errors="ignore")):
                found.append(f.relative_to(folder).as_posix())   # 키는 찍지 않고 파일 이름만 모은다
    return row(folder, "코드에 적힌 키", not found, "키 꼴의 글이 든 파일: " + ", ".join(found) if found else "키 꼴의 글 없음")


def check_gitignore(folder):
    path = folder / ".gitignore"
    lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
    missing = [x for x in [".env", ".streamlit/secrets.toml"] if x not in lines]
    return row(folder, ".gitignore", not missing, "막지 않은 것: " + ", ".join(missing) if missing else ".env 와 secrets.toml 을 막음")


def check_secrets(folder):
    real = (folder / ".streamlit" / "secrets.toml").exists()
    example = (folder / ".streamlit" / "secrets.toml.example").exists()
    ok = example and not real
    why = "견본만 있음" if ok else ("진짜 이름의 secrets.toml 이 있음" if real else "견본이 없음")
    return row(folder, "비밀 값 견본", ok, why)


FILE_CHECKS = [check_files, check_requirements, check_keys, check_gitignore, check_secrets]
for check in FILE_CHECKS:
    r = check(GOOD)
    print(f"{r['점검']:10} {r['결과']:5} {r['까닭']}")
출력
꼭 있어야 할 파일 통과    네 개 모두 있음
패키지 목록     통과    필요한 9개 모두 있음
코드에 적힌 키   통과    키 꼴의 글 없음
.gitignore 통과    .env 와 secrets.toml 을 막음
비밀 값 견본    통과    견본만 있음

코드가 의미하는 것 다섯 함수는 모두 파일을 읽기만 하고 앱을 실행하지 않습니다. check_requirements 는 ast.parse 로 앱 코드의 import 를 모으고 HIDDEN 으로 import 에 안 보이는 패키지를 더해, 클라우드가 설치할 목록과 견줍니다. check_keys 는 키 꼴의 글이 든 파일 이름만 모아 키 자체는 찍지 않고, row() 가 모든 점검을 같은 꼴의 사전으로 돌려줘 나중에 한 표로 모을 수 있게 합니다.

3단계. AppTest 점검 두 가지

Python
import streamlit as st
from streamlit.testing.v1 import AppTest


def check_app(folder):
    """첫 화면과 질문 하나를 연습 모드로 시험한다"""
    st.cache_resource.clear()
    at = AppTest.from_file(str(folder / "app.py"), default_timeout=30)
    # 자리표시를 Secrets 에 넣어 연습 모드로 돌린다(진짜 모델을 부르지 않는다)
    at.secrets["OPENAI_API_KEY"] = "발급받은_인증키"
    at.run()
    first_ok = len(at.exception) == 0 and len(at.title) == 1
    rows = [row(folder, "첫 화면", first_ok, f"제목 {len(at.title)}개, 오류 {len(at.exception)}개")]

    at.chat_input[0].set_value("여권은 기간이 얼마나 남아야 해요?").run()
    bot = at.chat_message[-1]
    sources = [c.value for c in bot.expander[0].caption] if bot.expander else []
    answer_ok = len(at.exception) == 0 and bot.name == "assistant" and any("조" in s for s in sources)
    rows.append(row(folder, "질문 하나", answer_ok, "근거: " + " / ".join(sources)))
    return rows


for r in check_app(GOOD):
    print(f"{r['점검']:6} {r['결과']:5} {r['까닭']}")
출력
첫 화면   통과    제목 1개, 오류 0개
질문 하나  통과    근거: 4쪽 · 제13조, 제14조 / 1쪽 · 제1조, 제2조

코드가 의미하는 것 check_app 은 캐시를 비우고 Secrets 에 자리표시를 넣어 연습 모드로 앱을 돌립니다. 첫 화면은 오류 수와 제목 수로, 질문 하나는 답 말풍선의 역할과 접힌 칸의 근거에 "조"가 들었는지로 가립니다. 진짜 모델을 부르지 않으므로 키 없이, 몇 초 만에 같은 결과를 되풀이해 얻습니다.

4단계. 두 폴더를 점검해 표로 모으기

Python
import pandas as pd

rows = []
for folder in [GOOD, BAD]:
    rows += [check(folder) for check in FILE_CHECKS]
    rows += check_app(folder)

table = pd.DataFrame(rows)
print(table.pivot(index="점검", columns="폴더", values="결과").loc[[r["점검"] for r in rows[:7]]].to_string())
print()
for _, r in table[table["결과"] == "고칠 것"].iterrows():
    print(f"[{r['폴더']}] {r['점검']}: {r['까닭']}")

table.to_csv("out_ch07_deploy_check.csv", index=False, encoding="utf-8-sig")
print("저장: out_ch07_deploy_check.csv,", len(table), "행")
출력
폴더         out_ch07_deploy out_ch07_deploy_bad
점검
꼭 있어야 할 파일              통과                고칠 것
패키지 목록                  통과                고칠 것
코드에 적힌 키                통과                고칠 것
.gitignore              통과                고칠 것
비밀 값 견본                 통과                고칠 것
첫 화면                    통과                  통과
질문 하나                   통과                  통과

[out_ch07_deploy_bad] 꼭 있어야 할 파일: 빠진 파일: .gitignore
[out_ch07_deploy_bad] 패키지 목록: 빠진 패키지: faiss-cpu, pypdf
[out_ch07_deploy_bad] 코드에 적힌 키: 키 꼴의 글이 든 파일: app.py
[out_ch07_deploy_bad] .gitignore: 막지 않은 것: .env, .streamlit/secrets.toml
[out_ch07_deploy_bad] 비밀 값 견본: 진짜 이름의 secrets.toml 이 있음
저장: out_ch07_deploy_check.csv, 14 행

실습 6의 out_ch07_deploy 는 일곱 점검을 모두 통과했습니다. 패키지 점검은 앱이 import 하는 일곱 개에 pypdf 와 faiss-cpu 를 더해 필요한 패키지 9개가 모두 있다고 확인했고, 질문 하나 점검에서는 "여권은 기간이 얼마나 남아야 해요?"의 첫 근거로 제14조가 든 4쪽 조각이 나왔습니다. 실수를 넣은 out_ch07_deploy_bad 는 파일 점검 다섯 가지에서 모두 "고칠 것"이 나왔습니다. .gitignore 하나를 지운 실수가 "꼭 있어야 할 파일"과 ".gitignore" 두 점검에 함께 걸렸고, 빠진 패키지 둘(faiss-cpu, pypdf), 키 꼴의 글이 든 app.py, 진짜 이름의 secrets.toml 도 모두 잡혔습니다. 그런데 이 폴더도 AppTest 의 첫 화면과 질문 하나는 통과했습니다. 내 PC 에는 빠진 두 패키지가 설치되어 있어 앱이 돌았기 때문입니다.

그래서 배포할 수 있는 것은 out_ch07_deploy 하나입니다. out_ch07_deploy_bad 를 그대로 올리면 클라우드에서는 pypdf 와 faiss-cpu 가 없어 앱이 뜨지 않고, .gitignore 가 없어 secrets.toml 이 저장소에 올라갑니다. 키를 적은 줄을 지우고, 두 패키지를 requirements.txt 에 더하고, .gitignore 를 다시 만들고, secrets.toml 은 견본으로 바꿔야 합니다. 화면 시험만으로는 이 네 가지를 잡지 못하므로 파일 점검을 함께 돌립니다.

코드가 의미하는 것 두 폴더에 일곱 점검을 모두 돌려 pd.DataFrame 한 표로 모으고, pivot 으로 점검을 줄에, 폴더를 칸에 놓아 견주기 쉽게 했습니다. "고칠 것"인 줄만 골라 까닭을 찍고, 표 전체는 out_ch07_deploy_check.csv 로 남깁니다. git push 하기 전마다 이 스크립트를 돌리면 화면 시험만으로는 못 잡는 파일 문제를 같은 기준으로 확인합니다.

이 프로젝트가 뜻하는 것

이 프로젝트는 가게로 보낼 상자를 싣기 전에 점검표를 들고 상자를 열어 보는 일과 같습니다. 일부러 빠뜨린 상자를 하나 만들고(1단계), 상자 속 물건을 목록과 대조하고(2단계), 상자를 풀어 한 번 차려 보고(3단계), 두 상자의 점검표를 한 장에 모읍니다(4단계). 여기서 점검표가 out_ch07_deploy_check.csv, 상자 속 물건 대조가 파일 점검, 차려 보기가 AppTest 점검입니다. 비유와 다른 점은, 사람의 눈은 하다 보면 빠뜨리지만 이 스크립트는 코드를 고칠 때마다 같은 일곱 가지를 똑같이 본다는 것입니다.

단계출력뜻
1. 실수 넣기좋은 폴더 파일 5개, 나쁜 폴더 4개.gitignore 가 빠지고 견본이 진짜 이름으로 바뀌었습니다
2. 파일 점검좋은 폴더 다섯 모두 통과필요한 패키지 9개가 모두 있고, 키 꼴의 글이 없습니다
3. AppTest 점검제목 1개, 오류 0개, 근거 4쪽 · 제13조, 제14조연습 모드로 첫 화면과 질문 하나가 오류 없이 돌았습니다
4. 표14행, 나쁜 폴더 "고칠 것" 5개나쁜 폴더는 파일 점검만 걸리고 화면 점검은 통과했습니다

한 줄로 줄이면, 배포 준비는 "화면이 돈다"와 "올릴 파일이 갖춰졌다"를 따로 확인해야 끝난다는 것을 표로 본 프로젝트입니다. 나쁜 폴더는 내 PC 에서 AppTest 를 두 개 다 통과했지만, 클라우드에서는 빠진 패키지 때문에 뜨지 않고 비밀 값이 새어 나갈 상태였습니다. 점검 스크립트를 배포 폴더 옆에 두고 git push 하기 전마다 돌리면, 사람이 빠뜨리기 쉬운 이 네 가지를 매번 같은 기준으로 확인할 수 있습니다.

← 이전 절7장 · 실습 6 — 배포할 폴더 만들기다음 절 →7장 · 정리