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

18일차 회고|이미지 업로드·비동기 처리·상품 조회|AI 오케스트레이션 개발자 국비지원

by 랩보다 AI 더 잘해지기 2026. 8. 4.
728x90

이미지 파일과 상품 정보를 함께 다뤄본 하루

오늘은 FastAPI와 Streamlit을 이용해 상품 이미지를 업로드하고, 이미지 정보를 상품 데이터와 함께 Supabase에 저장하는 과정을 실습했다.

지금까지는 상품명, 가격, 설명처럼 문자나 숫자로 이루어진 데이터가 중심이었다. 오늘은 여기에 이미지 파일이 추가되면서 처리 과정이 조금 더 복잡해졌다.

이미지는 데이터베이스에 그대로 넣는 것이 아니라 파일 시스템에 먼저 저장하고, 데이터베이스에는 이미지에 접근하기 위한 URL과 파일명을 기록해야 했다. 이를 통해 하나의 상품을 등록할 때 파일 저장과 데이터베이스 저장이 어떻게 연결되는지 이해할 수 있었다.

주요 학습 내용

FastAPI에서 이미지 파일 받기

상품 등록 API에서는 상품명, 가격, 설명과 함께 이미지 파일도 전달받았다.

일반적인 JSON 데이터와 달리 파일을 함께 전송하려면 Form, File, UploadFile을 사용해야 한다.

  • Form: 상품명, 가격, 설명과 같은 입력값을 받는다.
  • File: 해당 값이 파일이라는 것을 FastAPI에 알려준다.
  • UploadFile: 업로드된 파일의 이름과 내용 등을 다룰 수 있게 해준다.

이미지는 선택적으로 등록할 수 있도록 구성했다. 이미지가 전달되지 않으면 이미지 URL과 파일명에 None을 사용해 이미지가 없는 상품도 등록할 수 있게 했다.

이미지 파일과 DB 정보 분리하기

오늘 실습에서 중요했던 부분은 이미지 파일과 상품 데이터를 서로 다른 위치에 저장한다는 점이었다.

사용자가 상품 정보와 이미지 입력
→ FastAPI 라우터에서 요청 수신
→ 이미지 파일을 uploads 폴더에 저장
→ 이미지 URL과 파일명 생성
→ 상품 정보에 이미지 정보 추가
→ Supabase items 테이블에 저장
→ 등록 결과 반환

실제 이미지 파일은 uploads 폴더에 저장하고, Supabase에는 다음과 같은 상품 정보가 들어가도록 구성했다.

  • 상품명
  • 가격
  • 설명
  • 이미지 URL
  • 이미지 파일명
  • 등록 날짜

파일 자체를 데이터베이스에 넣는 것이 아니라 파일의 위치를 찾을 수 있는 정보를 저장한다는 점을 새롭게 이해했다.

ItemCreate 모델에 이미지 정보 담기

라우터에서는 입력받은 상품 정보와 이미지 저장 결과를 이용해 ItemCreate 객체를 새로 만들었다.

이미지가 정상적으로 저장되면 반환받은 image_url과 image_filename을 상품명, 가격, 설명과 함께 모델에 담았다. 이후 완성된 상품 데이터를 서비스 계층으로 전달해 Supabase에 저장했다.

이 과정에서 팀 프로젝트의 공통 모델을 임의로 변경하면 다른 코드와 설계에도 영향을 줄 수 있다는 점을 배웠다. 공통 스키마를 수정해야 한다면 개인이 바로 바꾸기보다 팀원들과 먼저 논의해야 한다.

async와 await를 사용하는 이유

이미지를 저장하는 함수에서는 async와 await가 사용됐다.

async def create_item(...):
    image_url, image_filename = await save_image(image)

async는 기다리는 작업이 포함된 비동기 함수라는 뜻이고, await는 해당 작업이 끝날 때까지 기다리면서 실행 순서를 다른 작업에 넘기는 역할을 한다.

이미지 저장처럼 시간이 걸릴 수 있는 작업을 일반적인 동기 방식으로 처리하면, 해당 작업이 끝날 때까지 서버가 다른 요청을 처리하기 어려울 수 있다. 비동기 처리를 적용하면 이미지가 저장되는 동안에도 서버가 다른 사용자의 요청을 받을 수 있다.

식당 직원이 음식 하나가 완성될 때까지 가만히 기다리는 것이 아니라, 기다리는 동안 다른 주문을 처리하는 것과 비슷하다.

서비스 계층에서 Supabase에 저장하기

이미지 정보까지 담긴 ItemCreate 객체는 서비스 계층으로 전달됐다. 서비스에서는 상품 데이터를 Supabase의 items 테이블에 저장하고, 저장 결과를 ItemPublic 모델로 검증해 반환했다.

또한 입력 모델에서는 설명 필드가 desc, 데이터베이스에서는 description으로 사용되고 있어 서비스 계층에서 서로 연결해주었다.

응답은 ApiResponse를 사용해 다음과 같이 일정한 구조를 유지했다.

  • 요청 성공 여부
  • 사용자에게 보여줄 메시지
  • 실제 상품 데이터

HTTP 오류와 입력값 검증 오류도 공통 핸들러를 이용해 비슷한 형태로 반환하도록 구성했다.

실행 오류와 해결 과정

메인 애플리케이션에 라우터 등록하기

상품 등록 API를 작성했더라도 main.py에 라우터를 등록하지 않으면 Swagger에서 해당 API가 나타나지 않는다.

따라서 item_router를 불러온 뒤 다음과 같은 방식으로 애플리케이션에 연결해야 했다.

app.include_router(item_router)

라우터, 서비스, 스키마 파일을 만드는 것만으로 API가 자동 등록되는 것이 아니라, 최종적으로 메인 애플리케이션과 연결해야 한다는 점을 다시 확인했다.

복사한 코드에 남아 있던 이름 수정하기

기존 Product 코드를 참고해 Item 기능을 만들면서 이전 코드의 이름이 일부 남아 있어 import 오류가 발생했다.

예를 들어 존재하지 않는 이미지 관련 함수나 이전 product 모듈의 이름이 남아 있으면 Uvicorn이 정상적으로 실행되지 않았다. 오류 메시지를 확인하면서 잘못된 import를 삭제하고, 관련 이름을 item 구조에 맞게 수정했다.

코드를 복사해 사용하는 것은 빠르게 기능을 만들 수 있다는 장점이 있지만, 이전 모듈명과 함수명이 남아 있지 않은지 반드시 확인해야 한다.

VS Code 단축키 충돌 해결하기

실습 중 VS Code의 한 줄 삭제 단축키인 Ctrl + Shift + K가 작동하지 않고 Notion이 실행되는 문제도 있었다.

VS Code의 키보드 로그를 확인한 결과 K 키 자체는 정상적으로 인식됐지만, 조합 단축키는 Notion Desktop이 먼저 가져가고 있었다. Notion의 명령어 검색 단축키가 같은 키로 설정된 것이 원인이었다.

Notion에서 명령어 검색 기능을 끄거나 다른 단축키로 변경해 프로그램 간 단축키 충돌을 해결할 수 있었다.

상품 조회 화면 구현

상품 등록 기능에 이어 Supabase에 저장된 상품을 조회하는 화면도 구현하기 시작했다.

조회 화면에는 다음 정보가 나타나도록 계획했다.

  • 상품명
  • 상품 가격
  • 상품 설명
  • 상품 이미지
  • 상품 등록 날짜

이번에는 수정과 삭제 기능까지 확장하지 않고 조회 기능에 집중했다. Supabase에서 상품 정보를 가져오고, 저장된 이미지 정보를 이용해 Streamlit 화면에 이미지를 출력하는 흐름을 연습했다.

Streamlit에서 이미지를 보여주는 기능은 이전에 학습했지만, 이번에는 데이터베이스의 상품 정보와 파일 시스템의 이미지를 연결해야 한다는 차이가 있었다.

다만 오늘 확인된 범위에서는 상품 조회 페이지 구현을 시작한 단계이므로, 완성했다고 단정하기보다 이후 실제 코드와 실행 결과를 다시 점검할 필요가 있다.

추가로 복습할 부분

이미지 설정 파일에는 최대 이미지 크기와 허용할 이미지 형식에 관한 값도 준비돼 있었다.

  • 최대 이미지 크기: 5MB
  • 허용 이미지 형식
  • 이미지 업로드 경로

다만 현재 확인한 코드에서는 이 설정을 이용한 실제 검증 로직까지 완성되지는 않았다. 이후에는 다음 기능을 추가해볼 수 있다.

  • 허용하지 않는 이미지 형식 차단
  • 최대 용량을 초과한 파일 차단
  • 같은 이름의 파일이 올라왔을 때 파일명 중복 방지
  • 잘못된 파일명이나 경로 처리
  • 상품 삭제 시 연결된 이미지 파일도 함께 삭제

설정값을 만드는 것과 그 설정을 실제 검증 과정에서 사용하는 것은 별개의 작업이라는 점도 기억해야겠다.

오늘의 회고

오늘은 단순한 CRUD에서 한 단계 더 나아가 파일 업로드가 포함된 상품 등록 과정을 경험했다.

상품 이미지 하나를 추가했을 뿐인데도 라우터에서 파일을 받고, 이미지 저장 함수를 기다리고, 반환된 정보를 모델에 담고, 서비스에서 데이터베이스에 저장하는 여러 과정이 필요했다.

처음에는 파일 저장과 데이터베이스 저장이 한 번에 이루어지는 것처럼 느껴졌지만, 실제로는 각각 다른 작업이며 코드가 두 작업을 순서대로 연결하고 있다는 것을 이해할 수 있었다.

또한 async와 await도 단순히 FastAPI 함수 앞에 붙이는 문법이 아니라, 파일 저장처럼 기다림이 발생하는 동안 서버가 다른 요청을 처리할 수 있게 도와주는 기능이라는 점이 조금 더 명확해졌다.

다음에는 상품 조회 화면을 완성하고, 이미지 형식과 용량을 실제로 검증하는 과정까지 이어서 복습해봐야겠다.

댓글