본문 바로가기
AI 오케스트레이션 캠프/일차별 회고

14일차 회고|Streamlit·FastAPI CRUD 연동|AI 오케스트레이션 개발자 국비지원

by 랩보다 AI 더 잘해지기 2026. 7. 29.
728x90

 

오늘은 Streamlit으로 여러 페이지를 가진 프론트엔드 화면을 구성하고, FastAPI 백엔드 및 외부 API와 연결하는 실습을 진행했다.

로그인 상태에 따라 사용할 수 있는 페이지를 구분하고, 날씨 데이터를 표와 차트로 표현해 보았다. 이후에는 Product 입력·조회·수정·삭제 기능을 직접 구현하며 프론트엔드와 백엔드가 데이터를 주고받는 전체 흐름을 익혔다.

특히 Streamlit은 사용자가 버튼을 누르거나 입력값을 바꿀 때마다 코드를 다시 실행한다는 특징이 있다. 이 과정에서 조회 결과가 사라지는 문제를 st.session_state로 해결하며 상태 관리의 필요성을 이해할 수 있었다.

Streamlit 멀티페이지 앱 구성

먼저 st.Page()와 st.navigation()을 이용해 여러 화면으로 구성된 Streamlit 앱을 만들었다.

앱에는 다음과 같은 페이지를 연결했다.

  • 로그인
  • 회원가입
  • 날씨 조회
  • FastAPI 서버 상태 확인
  • Product 입력
  • Product 조회

로그인 여부에 따라 보여주는 페이지도 다르게 구성했다. 로그아웃 상태에서는 로그인·회원가입·서버 확인 페이지를 표시하고, 로그인 상태에서는 날씨와 Product 관련 페이지를 사용할 수 있도록 했다.

if st.session_state.loginout == "login":
    pages = [
        home_page,
        weather_page,
        product_create_page,
        product_select_page,
    ]
else:
    pages = [
        home_page,
        login_page,
        signup_page,
        health_page,
    ]

position="hidden"으로 Streamlit의 기본 메뉴를 숨긴 뒤 st.page_link()를 사용해 사이드바 메뉴를 직접 만들었다.

이를 통해 하나의 app.py에서 전체 페이지 이동을 관리하고, 각 기능은 별도의 파일로 나누는 멀티페이지 구조를 익혔다.

로그인 상태와 인증 기능 분리

로그인 관련 기능은 core/auth.py에 따로 작성했다.

  • init_state() : 로그인 상태의 초기값 설정
  • login() : 입력한 아이디와 비밀번호 확인
  • logout() : 로그인 정보 초기화
  • is_logged_in() : 현재 로그인 상태 확인

화면을 만드는 코드와 로그인 상태를 처리하는 코드를 분리하니 각 파일의 역할이 더 명확해졌다.

또한 st.session_state와 브라우저 저장 공간인 SessionStorage를 함께 사용해 화면이 다시 실행되거나 새로고침되더라도 로그인 상태를 유지하는 구조를 살펴보았다.

현재 로그인 정보는 실습용으로 코드 안에 고정돼 있지만, 실제 서비스에서는 데이터베이스 조회와 비밀번호 암호화가 필요하다는 점도 기억해야 한다.

홈·로그인·회원가입 화면 만들기

홈 화면에서는 st.columns()로 화면을 세 부분으로 나누고 st.metric()을 이용해 접속자, 로그 수, 완료율과 같은 예시 데이터를 표시했다.

로그인과 회원가입 화면은 st.form()으로 구성했다. 폼을 사용하면 여러 입력값을 작성한 뒤 제출 버튼을 눌렀을 때 한 번에 처리할 수 있다.

회원가입 화면에서는 아이디, 비밀번호, 이름 중 하나라도 입력되지 않으면 안내 메시지를 표시하도록 검사했다.

if not signup_id or not signup_pwd or not signup_name:
    st.warning("ID, PWD, 이름을 모두 입력해 주세요.")

사용자가 잘못된 값을 입력했을 때 바로 요청을 보내지 않고 먼저 검사하는 과정도 중요한 기능이라는 것을 알게 됐다.

외부 날씨 API와 Pandas 활용

Open-Meteo API를 이용해 서울, 부산, 대전, 제주의 시간별 날씨를 조회했다.

도시별 위도와 경도를 미리 딕셔너리에 저장하고, 사용자가 선택한 도시와 조회 기간을 API 요청의 매개변수로 전달했다.

response = httpx.get(
    WEATHER_URL,
    params={
        "latitude": location["latitude"],
        "longitude": location["longitude"],
        "hourly": (
            "temperature_2m,"
            "relative_humidity_2m,"
            "precipitation_probability"
        ),
        "forecast_days": forecast_days,
        "timezone": "Asia/Seoul",
    },
    timeout=10.0,
)

API가 반환한 JSON 데이터 중 hourly 부분을 Pandas의 DataFrame으로 변환하고, 영문 컬럼명을 시간·기온·습도·강수확률로 변경했다.

이후 st.dataframe()으로 표를 표시하고 st.line_chart()로 시간별 기온과 습도를 시각화했다.

이번 실습을 통해 API 응답은 화면에 바로 출력하는 것뿐 아니라, Pandas를 이용해 분석하기 좋은 표 구조로 바꿀 수 있다는 것을 배웠다.

Streamlit에서 FastAPI 상태 확인하기

Streamlit 프론트엔드에서 로컬 FastAPI 서버의 /health 주소로 GET 요청을 보내 백엔드가 정상적으로 실행되고 있는지 확인했다.

응답이 도착하면 상태 코드와 JSON 데이터를 출력하고, 서버가 실행되지 않았거나 응답 시간이 초과됐을 때는 오류 메시지를 보여주도록 했다.

response = httpx.get(
    f"{API_BASE_URL}/health",
    timeout=5.0,
)

이 실습을 통해 Streamlit은 사용자에게 화면을 보여주는 프론트엔드 역할을 하고, FastAPI는 요청을 받아 데이터를 처리하는 백엔드 역할을 한다는 점을 다시 정리할 수 있었다.

그 사이에서 httpx는 요청과 응답을 전달하는 역할을 한다.

Product CRUD 기능 구현

오늘의 핵심 실습은 Product 데이터를 입력하고 조회·수정·삭제하는 CRUD 기능이었다.

기능HTTP 메서드역할

Create POST 새로운 제품 등록
Read GET 제품 목록 조회
Update PUT 기존 제품 수정
Delete DELETE 제품 삭제

Product 입력

Product 입력 화면에서는 상품명과 가격을 입력받아 백엔드 등록 API로 전송했다.

입력값은 다음과 같은 딕셔너리로 만든 뒤 JSON 형식으로 전달할 수 있다.

payload = {
    "name": product_name.strip(),
    "price": int(product_price),
}

상품명이 비어 있거나 공백만 입력된 경우에는 백엔드 요청을 보내지 않고 안내 메시지를 표시하도록 했다.

Product 전체 조회

Product 조회 화면에서는 다음 주소로 GET 요청을 보내 전체 제품 목록을 불러왔다.

f"{API_BASE_URL}/product/getall"

처음에는 서버에서 받은 데이터를 st.write()로 출력해 응답 구조를 확인했다. 이후 id, name, price가 포함된 목록을 st.dataframe()으로 변경해 보기 좋은 표로 표시했다.

제품이 하나도 없을 때는 빈 화면만 보여주는 대신 등록된 제품이 없습니다.라는 안내 문구가 나타나도록 처리했다.

session_state로 조회 결과 유지하기

Streamlit은 버튼을 누르거나 선택값을 변경할 때 전체 코드를 위에서부터 다시 실행한다.

따라서 조회한 제품을 일반 변수에만 저장하면 다른 입력값을 조작했을 때 표가 사라질 수 있다. 이 문제를 해결하기 위해 제품 목록을 st.session_state.products에 저장했다.

if "products" not in st.session_state:
    st.session_state.products = []

st.session_state.products = response.json()

제품 표를 조회 버튼 조건문 밖에서 표시하도록 옮기니 화면이 다시 실행돼도 조회 결과가 유지됐다.

이번 실습을 통해 일반 변수와 session_state의 차이를 직접 확인할 수 있었다.

  • 일반 변수는 코드가 다시 실행되면 초기화될 수 있다.
  • session_state는 같은 사용자 세션 안에서 값을 임시로 기억한다.

수정할 제품 선택하기

조회된 제품 중 수정하거나 삭제할 제품은 st.selectbox()로 선택했다.

실제 선택값은 제품 ID로 저장하면서 화면에는 100 - 바지처럼 ID와 상품명이 함께 나타나도록 format_func를 사용했다.

선택한 ID와 같은 제품은 next()를 이용해 전체 목록에서 찾았다.

selected_product = next(
    product
    for product in st.session_state.products
    if product["id"] == selected_product_id
)

이를 통해 선택된 제품의 기존 상품명과 가격을 수정 폼의 기본값으로 넣을 수 있었다.

Product 수정 기능

수정 화면에서는 선택한 제품의 기존 정보가 자동으로 입력되도록 구성했다.

update_name = st.text_input(
    "수정할 상품명",
    value=selected_product["name"],
)

update_price = st.number_input(
    "수정할 가격",
    value=int(selected_product["price"]),
)

수정 요청을 구현하는 과정에서 API 주소와 요청 데이터 형식을 다시 확인했다.

처음에는 제품 ID를 JSON 데이터 안에 넣고 /product/update로 요청하는 방식을 생각했지만, 실제 백엔드 구조는 제품 ID를 URL에 넣는 방식이었다.

response = httpx.put(
    f"{API_BASE_URL}/product/update/{selected_product_id}",
    json={
        "name": update_name.strip(),
        "price": int(update_price),
    },
    timeout=15.0,
)

이 과정을 통해 프론트엔드 코드를 작성하기 전에 백엔드의 API 주소, HTTP 메서드, 요청 데이터 구조를 정확히 확인해야 한다는 점을 배웠다.

Product 삭제 기능

제품 삭제는 다음 주소로 DELETE 요청을 보내도록 구현했다.

response = httpx.delete(
    f"{API_BASE_URL}/product/delete/{selected_product_id}",
    timeout=15.0,
)

삭제는 데이터를 되돌리기 어려운 기능이므로 확인 체크박스를 선택해야 삭제 버튼이 활성화되도록 구성했다.

수정과 삭제 기능을 별도의 탭으로 나눠 사용자가 실수로 삭제 버튼을 누를 가능성도 줄였다.

수정·삭제 후 목록 자동 갱신

제품을 수정하거나 삭제한 뒤 기존 제품 목록을 그대로 표시하면 화면에는 이전 정보가 남아 있을 수 있다.

이를 해결하기 위해 요청이 성공하면 전체 제품 목록을 다시 조회하고, 새로운 데이터를 session_state에 저장한 뒤 st.rerun()으로 화면을 다시 실행했다.

refresh_products()
st.session_state.success_message = (
    "제품 수정이 완료되었습니다."
)
st.rerun()

st.rerun()을 실행하면 성공 메시지도 사라질 수 있기 때문에 메시지 내용을 session_state에 저장했다. 페이지가 다시 실행된 뒤 메시지를 표시하고 값을 비우는 방식으로 처리했다.

조회, 상태 저장, 새로고침이 서로 연결되면서 단순히 API 요청을 보내는 것을 넘어 실제로 사용할 수 있는 화면에 가까워졌다.

API 요청 예외 처리

API는 서버 상태나 인터넷 연결에 따라 언제든지 실패할 수 있다. 따라서 정상 응답만 처리하는 것이 아니라 여러 오류 상황을 구분했다.

  • httpx.TimeoutException : 정해진 시간 안에 응답이 오지 않은 경우
  • httpx.RequestError : 서버에 연결할 수 없거나 요청 자체에 실패한 경우
  • httpx.HTTPStatusError : 서버가 400번대 또는 500번대 상태 코드를 반환한 경우
  • ValueError : 서버 응답을 올바른 JSON 데이터로 변환하지 못한 경우

response.raise_for_status()를 사용하면 실패 상태 코드를 HTTPStatusError로 처리할 수 있다.

Render 무료 서버가 잠들어 있으면 첫 요청이 오래 걸릴 수 있으므로, 단순히 앱이 고장 났다고 판단하기 전에 로딩 시간과 시간 초과 설정도 함께 확인해야 한다.

Product 입력·조회 기능 확인 문서 작성

마지막에는 Product 입력과 조회가 실제로 연결되는지 확인하기 위한 Markdown 테스트 문서를 작성했다.

테스트 데이터는 다음과 같이 정했다.

항목입력값

상품명 기능확인용 노트
가격 12000

확인 흐름은 다음과 같다.

  1. Product 입력 화면에서 테스트 상품을 등록한다.
  2. 등록 성공 메시지를 확인한다.
  3. Product 조회 화면으로 이동한다.
  4. 제품 목록을 다시 불러온다.
  5. 입력한 상품명과 가격이 동일하게 표시되는지 확인한다.
  6. 빈 상품명, 서버 연결 실패, 시간 초과 등의 상황도 확인한다.

기능을 만들고 끝내는 것이 아니라 정상 작동 기준과 테스트 결과를 기록하는 과정까지 포함해야 문제를 더 쉽게 찾을 수 있다는 점을 배웠다.

Git 작업 중 확인한 점

프로젝트를 별도 폴더에서 구성하는 과정에서는 상위 Git 저장소가 하위 폴더의 변경 사항까지 추적하는 모습도 확인했다.

VS Code의 Git Graph와 git status를 통해 로컬 main과 원격 origin/main의 위치가 다를 수 있다는 점을 살펴봤다. 프로젝트 폴더 안에 또 다른 Git 저장소를 만들면 어느 저장소가 어떤 파일을 관리하는지 헷갈릴 수 있으므로 저장소의 경계를 명확히 정하는 것이 중요하다.

앞으로 Git 명령을 실행하기 전에는 현재 터미널 위치와 활성화된 저장소를 먼저 확인해야겠다.

오늘의 회고

오늘은 단순한 Streamlit 화면 제작에서 한 단계 더 나아가 로그인 상태, 멀티페이지 구성, 외부 API, FastAPI 백엔드, Product CRUD 기능을 하나의 앱 안에서 연결했다.

특히 조회 결과가 사라지는 문제를 통해 Streamlit의 재실행 구조와 session_state의 필요성을 이해한 것이 가장 큰 수확이었다. 수정과 삭제 후 최신 목록을 다시 불러오고 st.rerun()으로 화면까지 갱신하면서 데이터와 화면 상태가 함께 움직여야 한다는 점도 알게 됐다.

API를 연결할 때는 주소만 맞추는 것이 아니라 HTTP 메서드, 경로 매개변수, JSON 구조, 상태 코드까지 백엔드와 정확히 일치해야 한다. 수정 API 주소를 실제 구조에 맞게 바로잡은 과정이 이를 확실히 이해하는 데 도움이 됐다.

앞으로는 반복되는 API 요청과 예외 처리 코드를 함수로 더 깔끔하게 분리하고, 코드에 직접 작성한 서버 주소를 환경변수로 관리하는 방법을 복습하고 싶다. Product 입력부터 조회·수정·삭제까지 순서대로 다시 테스트하면서 CRUD 흐름도 확실하게 익혀야겠다.

댓글