FastAPI를 이용한 웹 서비스 구현 연습_5

Frye 'de Bacon·2023년 10월 26일

본 시리즈는 '박응용' 님의 '점프 투 FastAPI'를 바탕으로 학습 및 실습한 내용을 정리한 것입니다.


구현 및 파인튜닝한 모델을 사용한 웹 서비스 구현을 위해 FastAPI의 학습 필요성을 느껴 학습 과정을 정리합니다. 내용의 정확성이나 이론적인 부분은 당연히 원본 페이지를 참조하시는 게 좋고, 본 시리즈에서는 구현 도중 발생하는 문제 등을 해결하는 과정을 함께 기록하여 '처음부터 끝까지 따라 할 수 있는' 시리즈를 만드는 것을 목표로 합니다(물론 제1목표는 학습 내용 기록입니다).


이번 포스트에서는 질문 목록 화면을 만들면서 다음과 같은 기능을 학습한다.

  • 질문 목록 화면 구현
  • 라우터 컴포넌트
  • API 호출 라이브러리

1. 질문 목록 화면 구현하기

이제부터는 스벨트를 이용해 프론트엔드를 구현한다. 스벨트의 경우 리액트나 뷰 등에 비해 코드가 간결한 것이 특징이지만, 제대로 배우려면 별도로 공부할 필요가 있을 듯하다. 일단 지금은 예제에서 주어지는 코드를 입력하면서 스벨트 코드 자체에 익숙해지는 것을 목표로 하는 것이 좋겠다.

frontend 디렉토리의 App.svelte 파일을 열어 다음과 같이 수정한다.

<script>
  let question_list = []

  function get_question_list() {
    fetch("http://127.0.0.1:8000/api/question/list").then((response) => {
      response.json().then((json) => {
        question_list = json
      })
    })
  }

  get_question_list()
</script>

<ul>
  {#each question_list as question}
    <li>{question.subject}</li>
  {/each}
</ul>

이 함수는 question_list라는 변수를 먼저 생성하고, get_question_list라는 함수(앞서 작성했던 질문 목록 API를 호출하는 함수이다)를 선언한 뒤 get_question_list 함수로 얻은 결괏값을 question_list 변수에 대입한다. 그리고 이 question_list는 스벨트의 each 블록을 통해 각 요소를 순회하며 제목만을 표시하도록 했다.

Svelte의 필수 문법

※ 스벨트의 블록은 여는 태그({#~})와 닫는 태그({/~})가 필수적이다.

  1. 분기 블록
    분기 블록은 파이썬의 if, elif, else와 유사하게 동작한다.
{# if 조건문1}
  <p>조건문1에 해당하면 실행</p>
{:else if 조건문2}
  <p>조건문2에 해당하면 실행</p>
{:else}
  <p>조건문1과 2에 모두 해당되지 않으면 실행</p>
{/if}
  1. 반복 블록
    이름에서 유추할 수 있듯 파이썬의 for 반복문과 유사하다.
{#each 리스트 as 아이템, index}
  <p>순서: {index}</p>
  <p>{아이템}</p>
{/each}
  1. 객체 표시
    객체의 값은 파이썬의 f포맷팅과 같이 중괄호({})를 이용해 표시할 수 있으며, 객체의 속성은 파이썬과 유사하게 점(.)을 이용해 출력할 수 있다.
{객체}
{객체.속성}

질문 목록 확인하기

이제 (프론트엔드 서버가 닫혀 있다면 npm run dev 명령어로 실행한 뒤) 브라우저를 열고 스벨트 페이지에 접속하면 다음과 같은 화면을 볼 수 있다.


2. 스벨트 라우터

본 프로젝트는 질문 답변 사이트의 구축이며, 따라서 다음과 같은 화면이 필요하다.

  • 질문 목록 : 질문 목록을 표시하는 화면
  • 질문 상세 : 질문의 상세 내용을 표시하고 답변을 작성하는 화면
  • 질문 작성 : 질문을 작성하는 화면
  • 질문 수정 : 질문을 수정하는 화면
  • 답변 수정 : 답변을 수정하는 화면
  • 회원 가입 : 회원 가입을 위한 화면
  • 로그인 : 로그인을 위한 화면

그러나 스벨트는 SPA(Single Page Application)으로서 웹 사이트 전체 페이지를 하나의 페이지에 담아 동적으로 화면을 바꾸며 표시하는 방식이기 때문에 하나의 페이지에서 내용을 달리하여 표시해야 한다. 그런데 이 경우 코드가 복잡해진다는 문제가 있다.
이를 해결하기 위해 사용하는 것이 스벨트의 svelte-spa-router이다.

svelte-spa-router 설치

우선 동작 중인 스벨트 서버를 중지하고, 터미널에서 다음 명령어를 입력해 svelte-spa-router를 설치한다.

(practice_1) C:\workspace\fastapi_practice\practice_1\frontend> npm install svelte-spa-router

설치 후에는 서버를 다시 시작하자.

라우터 적용하기

라우터를 적용하기 위해서는 앞서 구상했던 화면들 각각에 대하여 url 주소를 설정해야 한다. 다음과 같이 정리해볼 수 있겠다.

URL파일명화면명
/Home.svelte질문 목록(메인 화면)
/detail/:question_idDetail.svelte질문 상세 화면
question-createQuestionCreate.svelte질문 작성 화면
question-modify/:question_idQuestionModify.svelte질문 수정 화면
user-loginUserLogin.svelte로그인 화면
user-createUserCreate.svelte회원 가입 화면
answer-modifyAnswerModify.svelte답변 수정 화면

여기서 'detail/:question_id'는 가변 변수로서 '/detail/2'처럼 호출되었을 때 question_id 변수에 2라는 값을 대입한다는 의미이다.

이제 각 URL을 호출하면 그에 매핑되는 svelte 화면이 화면을 렌더링하도록 설계할 것이다. 예컨대 '/'에 해당하는 주소가 요청되면 Home.svelte 파일이 동작하여 화면을 생성하는 것이다. 현재는 질문 목록 화면만 작성되어 있으므로 질문 목록만 app에 등록하여 화면을 구현해보자. 당연히 URL 규칙은 App.svelte 파일에 작성하여 등록한다.

Routes 디렉토리 생성하기

우선은 App.svelte 파일의 내용을 Home.svelte 파일로 복사하자. App.svelte 파일에 이미 질문 목록 화면이 구현되어 있기 때문이다. 그리고 Hone.svelte와 같은 URL 주소와 매핑되는 파일들은 'src' 디렉토리 아래에 'routes'라는 디렉토리를 만들어 모아 두고 별도로 관리한다.
파일을 만들었다면 App.svelte의 내용을 그대로 복사해 붙여넣자.

Home.svelte를 잘 만들었다면, 이제 App.svelte 파일을 다음과 같이 수정한다.

<script>
  import Router from 'svelte-spa-router'
  import Home from "./routes/Home.svelte"
  
  const routes = {
    '/': Home,
  }
</script>

<Router {routes}/>

스크립트에서 Router를 임포트하고, '/' 주소에 매핑되는 컴포넌트로서 Home을 등록했다. 여기서 Home은 Home.svelte 파일의 내용들을 의미한다. 이후 추가되는 URL 규칙들도 routes 변수에 추가하면 된다.
이렇게 수정한 뒤 http://localhost:5173/ 사이트에 접속하면 이전과 동일한 질문 목록이 나타날 것이다. 즉 '/' 주소에 해당하는 Home.svelte가 호출되어 화면에 표시된 것이다.


3. API 호출 라이브러리

질문 목록과 같이 데이터를 조회하기 위해서는 백엔드 서버에 요청하여 데이터를 가져와야 한다. Home.svelte 파일을 예로 들면 다음 부분에서 데이터를 조회하게 된다.

fetch("http://127.0.0.1:8000/api/question/list").then((response) => {
        response.json().then((json) => {
          question_list = json
        })
      })

앞으로 만들 대부분의 기능들도 데이터 처리를 위해 fetch 함수를 사용해야 한다. 그런데 fetch 함수에는 URL 주소의 호스트명처럼 공통적으로 사용할 수 있는 부분이 많다. 따라서 이를 공통 라이브러리로 만들어 사용하면 편리할 것이다.

FastAPI 라이브러리 만들기

데이터 송수신을 위한 FastAPI 함수를 작성해 보자. src/lib 디렉토리에 api.js 파일을 만든다.

const fastapi = (operation, url, params, success_callback, failure_callback) => {
    let method = operation
    let content_type = 'application/json'
    let body = JSON.stringify(params)

    let url = 'http://127.0.0.1:8000'+url
    if(method === 'get') {
        _url += "?" + new URLSearchParams(params)
    }

    let options = {
        method: method,
        headers: {
            "Content-Type": content_type
        }
    }

    if(method !== "get") {
        options['body'] = body
    }

    fetch(_url, options).then(response => {
        response.json().them(json => {
            if(response.status >= 200 && response.status < 300) {
                if(success_callback) {
                    success_callback(json)
                }
            }
            else {
                if(failure_callback) {
                    failure_callback(json)
                }
                else{
                    alert(JSON.stringify(json))
                }
            }
        })
        .catch(error => {
            alert(JSON.stringify(error))
        })
    })
}

export default fastapi

위와 같이 api.js 파일 안에 fastapi라는 함수를 선언하였다. 그러면 fastapi 함수에 대해 자세히 알아보자.
우선 매개변수는 다음과 같다.

매개변수설명
operation데이터를 처리하는 방법으로, 소문자만 사용get, post, put, delete
url요청 url, 단 백엔드 서버의 호스트명 이후의 url만 전달/api/question/list
params요청 데이터{page: 1, keyword: "마크다운"}
success_callbackAPI 호출 성공 시 수행할 함수로서, 전달된 함수에는 API 호출 시 리턴되는 json이 입력으로 주어짐-
failure_callbackAPI 호출 실패 시 수행할 함수로서, 전달된 함수에는 오류값이 입력으로 주어짐-

그러면 fastapi 함수의 내용을 천천히 살펴보자.
if문에서 operation이 'get'일 경우에는 파라미터를 get 방식에 맞게끔 URLSearchParams를 사용하여 파라미터를 조립하도록 했고, 'get'이 아닌 경우에만 options['body'] 항목에 전달받은 파라미터 값을 설정하도록 했다. 그리고 body 항목에 값을 설정할 때는 JSON.stringify(params)처럼 params를 JSON 문자열로 변경하도록 한다.
그리고 API의 호출 주소는 호스트명(http://127.0.0.1:8000)에 전달받은 url 값을 더하여 만들도록 했다. 즉, fastapi 함수를 사용할 때는 호스트명을 생략하고 그 뒷부분만 전달한다.
이렇게 API를 호출하면 성공할 수도, 실패할 수도 있다. 성공은 HTTP 프로토콜의 응답 코드가 200~299까지인 경우이므로 이를 체크하고, 만약 성공이라면 매개변수로 전달받은 success_callback을 실행하도록 했다. 이때 success_callback 함수에는 호출한 API의 리턴값을 입력으로 전달하여 호출한다. 실패할 경우 마찬가지로 failure_callback 함수를 호출한다.
※ JavaScript이므로 현재 100% 이해되지는 않는다. 이 부분은 추후 따로 공부할 필요가 있을 듯

호스트명 환경 파일에서 불러오기

fastapi 함수에는 현재 호스트명이 http://127.0.0.1:8000 과 같이 하드코딩되어 있는데, 호스트명은 개발, 운영 등 상황에 따라 변할 수 있으므로 이처럼 하드코딩된 상태로 사용하는 것은 좋지 않다. 따라서 호스트명을 환경 파일에 저장하고 함수에서는 그 값을 불러와 사용하는 것으로 수정한다.
frontend 디렉토리에 .env라는 파일을 생성하고 다음과 같이 입력한다.

VITE_SERVER_URL=http://127.0.0.1:8000

.env 파일에 호스트명에 해당하는 VITESERVERURL 환경변수를 추가한 것이다. 스벨트 파일에서 .env 파일의 항목을 읽을 수 있도록 하려면 반드시 'VITE'로 시작하도록 환경변수명을 설정해야 한다.

그리고 등록한 환경변수를 사용하도록 api.js 파일을 수정한다.

...
    let url = import.meta.env.VITE_SERVER_URL+url
    if(method === 'get') {
        _url += "?" + new URLSearchParams(params)
    }

※ 운영 환경에서는 .env 대신 .env.production 파일을 작성하여 사용한다.

fastapi 함수 사용하기

이제 Home.svelte 파일에서 fastapi 함수를 사용하도록 변경한다.

<script>
    import fastapi from "../lib/fastapi"
    let question_list = []
  
    function get_question_list() {
      fastapi('get', '/api/question/list', {}, (json) => {
        question_list = json
      })
    }
  
    get_question_list()
  </script>

우선 fastapi 함수를 사용할 수 있도록 api 파일을 import하였다. 질문 목록 api는 get 방식이므로 operation 인자로 'get'을 넣었고, 전달할 파라미터 값은 따로 없는 상태이므로 params 항목은 빈 값인 {}을 전달한다. 그리고 success_callback 함수는 화살표 함수로 작성하여 전달했다.

(json) => {
    question_list = json
}

화살표 함수의 내용은 응답으로 받은 json 데이터를 question_list에 대입하라는 내용이다.
※ 화살표 함수에 대해서는 JavaScript 문서의 화살표 함수 항목에서 추가로 확인해 보자.
fastapi 함수는 오류 발생 시 오류의 내용을 alert으로 표시하게 되어 있으므로 failure_callback 함수는 따로 전달하지 않았다.

이처럼 Home.svelte 파일을 수정하고 다시 질문 목록 화면을 조회하면 여전히 동일한 결과가 나타나는 것을 확인할 수 있다.

profile
AI, NLP, Data analysis로 나아가고자 하는 개발자 지망생

0개의 댓글