본 시리즈는 '박응용' 님의 '점프 투 FastAPI'를 바탕으로 학습 및 실습한 내용을 정리한 것입니다.
구현 및 파인튜닝한 모델을 사용한 웹 서비스 구현을 위해 FastAPI의 학습 필요성을 느껴 학습 과정을 정리합니다. 내용의 정확성이나 이론적인 부분은 당연히 원본 페이지를 참조하시는 게 좋고, 본 시리즈에서는 구현 도중 발생하는 문제 등을 해결하는 과정을 함께 기록하여 '처음부터 끝까지 따라 할 수 있는' 시리즈를 만드는 것을 목표로 합니다(물론 제1목표는 학습 내용 기록입니다).
이제 게시판 페이징 기능까지 구현하였다. 그런데 질문 글을 들어가다 보면 이상한 점이 있다. 만약 4페이지에 있는 어떤 게시물을 눌러 들어갔다가 다시 '뒤로 가기'를 눌러 나오면 4페이지가 아니라 1페이지로 돌아간다는 것이다. 이는 Home.svelte 파일이 다시 호출되면서 get_question_list(0)이 실행되기 때문이다.
이를 방지하려면 상세 페이지 호출 시 현재 질문 목록의 페이지 번호를 전달하고 다시 질문 목록으로 돌아올 때도 전달받은 페이지 번호를 다시 넘기는 식으로 개발해야 한다. 그러려면 코드가 매우 복잡해질 것이다.
Svelte는 스토어(Store)라는 기능으로 이를 매우 간단하게 해결해준다. 스토어를 통해 변수의 값을 전역적으로 저장함으로써 라우팅되는 페이지와 무관하게 스토어에 저장된 변수를 사용하는 것이다.
먼저 'frontend/scr/lib' 디렉토리에 store.js 파일을 생성하고 다음과 같이 입력한다.
import { writable } from 'svelte/store'
export const page = writable(0)
이렇게 쓰기 가능한 변수 page를 만들었다. writable(0)에서 (0)은 초깃값을 9으로 설정한다는 의미이다.
※ 스벨트 스토어에 대한 자세한 내용은 여기에서 확인하자.
이제 질문 목록 화면에서 스토어 변수 page를 사용할 수 있도록 Home.svelte 파일을 다음과 같이 수정한다.
<script>
import fastapi from "../lib/api"
import { link } from 'svelte-spa-router'
import { page } from '../lib/store'
let question_list = []
let size = 10
let page = 0 // 삭제
let total = 0
$: total_page = Math.ceil(total/size)
function get_question_list(_page) {
let params = {
page: _page,
size: size,
}
fastapi('get', '/api/question/list', params, (json) => {
question_list = json.question_list
$page = _page
total = json.total
})
}
get_question_list($page)
</script>
<div class="container my-3">
<table class="table">
<thead>
<tr class="table-dark">
<th>번호</th>
<th>제목</th>
<th>작성 일시</th>
</tr>
</thead>
<tbody>
{#each question_list as question, i}
<tr>
<td>{i+1}</td>
<td>
<a use:link href="/detail/{question.id}">{question.subject}</a>
</td>
<td>{question.create_date}</td>
</tr>
{/each}
</tbody>
</table>
<!-- 페이징 처리 시작 -->
<ul class="pagination justify-content-center">
<!-- 이전 페이지 -->
<li class="page-item {$page <= 0 && 'disabled'}">
<button class="page-link" on:click="{() => get_question_list($page-1)}">이전</button>
</li>
<!-- 페이지 번호 -->
{#each Array(total_page) as _, loop_page}
{#if loop_page >= $page-5 && loop_page <= $page+5}
<li class="page-item {loop_page === $page && 'active'}">
<button on:click="{() => get_question_list(loop_page)}" class="page-link">{loop_page+1}</button>
</li>
{/if}
{/each}
<!-- 다음 페이지 -->
<li class="page-item {$page >= total_page-1 && 'disabled'}">
<button class="page-link" on:click="{() => get_question_list($page+1)}">다음</button>
</li>
</ul>
<!-- 페이징 처리 끝 -->
<a use:link href="/question-create" class="btn btn-primary">질문 등록하기</a>
</div>
스토어 변수를 사용하기 위해 기존 page 변수는 삭제하고, 스토어 변수 page는 page와 같이 앞에 $을 붙여 참조한다. 그리고 페이지 최초 로딩 시 get_question_list(0)으로 요청하던 것을 get_question_list(page)로 변경하여 스토어에 저장된 페이지 값으로 요청하게 하였고, page 변수를 사용하던 부분을 모두 $page로 변경했다.
이제 다시 테스트를 해 보면 뒤로 가기를 해도 페이지가 유지되는 것을 확인할 수 있다.
아직 해결하지 못한 부분이 하나 더 있다. 질문 목록에서 4페이지를 선택하고 질문을 조회한 뒤에 '새로고침'을 하고 나서 뒤로 가기를 하면 다시 첫 번째 페이지로 이동하게 된다. 이는 브라우저에서 새로고침을 하는 순간 스토어 변수가 초기화되기 때문이다.
이는 새로고침 외에도 자바스크립트의 location.href 또는 a 태그를 통한 링크 호출 시에도 발생한다. 이를 해결하기 위해서는 스토어에 저장한 변수 값이 항상 유지되도록 지속성을 지닌 스토어가 필요하다.
store.js 파일을 수정하여 스토어 변수가 지속성을 가질 수 있게 바꾸어 보자.
import { writable } from 'svelte/store'
const persist_page = (key, initValue) => {
const storedValueStr = localStorage.getItem(key)
const store = writable(storedValueStr != null ? JSON.parse(storedValueStr) : initValue)
store.subscribe((val) => {
localStorage.setItem(key, JSON.stringify(val))
})
return store
}
export const page = persist_page("page", 0)
store.js 파일에 persist_page 함수를 신규로 선언하였다. 이 함수는 이름(key)과 초깃값(initValue)을 입력받아 writable 스토어를 생성하고 리턴하는 함수이다. 이때 localStorage를 사용하여 지속성을 갖도록 했다.
localStorage에 해당 이름의 값이 이미 존재하는 경우 초깃값 대신 기존의 값으로 스토어를 생성하여 리턴하며, 여기에 저장되는 값을 항상 문자열로 유지하기 위해 저장할 때는 JSON.stringify를, 읽을 때는 JSON.parse를 사용하도록 했다.
store의 subscribe 함수는 스토어에 저장된 값이 변경될 때 실행된느 콜백 함수이다. 따라서 스토어의 값이 변경되면 localSorage의 값도 변경된다.
이렇게 스토어를 지속성 스토어로 변경하면 브라우저를 새로고침해도 이전에 저장한 page의 값이 유지된다.
이렇게 되면 또 문제가, 로고를 눌렀을 때 스토어에 저장된 페이지 번호로 인해 첫 번째 페이지가 아닌 기존 페이지로 이동한다는 것이다. 따라서 로고를 눌렀을 때는 항상 첫 번째 페이지로 이동하도록 Navigation.svelte 파일을 수정해야 한다.
<script>
import { link } from 'svelte-spa-router'
import { page } from '../lib/store'
</script>
<!-- 내비게이션 바 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a use:link class="navbar-brand" href='/' on:click="{() => {$page = 0}}">Practice_1</a>
...
로고를 누르면 스토어 변수에 0을 할당함으로써 첫 번째 페이지로 이동하도록 했다. 이제 Home.svelte 파일을 수정한다.
...
$: get_question_list(page)
</script>
...
페이지 로드 시에 호출되는 get_question_list 함수 앞에 :)는 변수뿐만 아니라 함수나 구문에도 붙일 수 있는데, $: get_question_list(page)의 의미는 page 값이 변경될 경우 get_question_list 함수도 다시 호출하라는 의미이다.
※ 스벨트의 반응형 구문에 대한 내용은 여기에서 확인할 수 있다.
이제 로고를 누르면 첫 번째 페이지로 이동하는 것을 확인할 수 있다.
브라우저의 뒤로 가기 대신 상세 페이지에서 이전 질문 목록으로 돌아갈 수 있는 질문 목록 버튼을 추가해 보자. Detail.svelte 파일을 다음과 같이 수정한다.
<script>
import fastapi from "../lib/api"
import Error from "../components/Error.svelte"
import { push } from "svelte-spa-router"
...
</script>
<div class="container my-3">
<!-- 질문 -->
<h2 class="border-botom py-2">{question.subject}</h2>
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{question.content}</div>
<div class="d-flex justify-content-end">
<div class="badge bglight text-dark p-2">
{question.create_date}
</div>
</div>
</div>
</div>
<button class="btn btn-secondary" on:click="{() => {
push('/')}}">목록</button>
<!-- 답변 목록 -->
...
질문과 답변 목록 사이에 '목록' 버튼이 추가된 것을 확인할 수 있다. 이를 누르면 이전의 질문 목록으로 이동하게 된다.
