REST API 연동
JSON 데이터를 받아 화면에 출력하는 전체 흐름을 배웁니다.
지금까지 만든 Vue 예제에서는 사원 데이터를 Vue 파일 안에 직접 작성했습니다.
const employees = ref([
{
id: 1,
name: '홍길동',
department: '개발팀'
},
{
id: 2,
name: '김철수',
department: '영업팀'
}
])
연습 단계에서는 편리하지만 실제 프로그램에서는 일반적으로 사원정보를 Vue 파일에 직접 저장하지 않습니다. 실제 데이터는 데이터베이스에서 관리하는 경우가 많습니다.
이번 교재에서는 다음과 같은 구조를 생각해 볼 수 있습니다.
Vue는 화면을 담당하고, FastAPI는 서버 프로그램을 담당합니다. 데이터는 필요에 따라 SQLite, MySQL, PostgreSQL 같은 데이터베이스에 저장할 수 있습니다.
| 구분 | 역할 |
|---|---|
| Vue | 화면 출력, 사용자 입력 |
| FastAPI | 데이터 처리 |
| Database | 데이터 저장 |
| REST API | Vue와 FastAPI 사이의 데이터 전달 |
사용자가 사원목록 화면을 열었을 때의 흐름을 단순하게 표현하면 다음과 같습니다.
→ FastAPI → JSON 전달 → Vue → 화면 출력
Vue와 서버 사이에서는 주로 JSON 형식으로 데이터를 주고받습니다.
{
"id": 1,
"name": "홍길동",
"department": "개발팀"
}
사원이 여러 명이면 배열 안에 여러 객체가 들어가는 형태가 됩니다.
[
{
"id": 1,
"name": "홍길동",
"department": "개발팀"
},
{
"id": 2,
"name": "김철수",
"department": "영업팀"
},
{
"id": 3,
"name": "이영희",
"department": "관리팀"
}
]
서버에서 배열 형태의 JSON 데이터를 받으면 Vue에서는 이를 변수에 저장하고 v-for로 반복 출력할 수 있습니다.
서버는 특정 URL을 통해 데이터를 제공합니다. 예를 들어 FastAPI가 다음 주소에서 실행된다고 가정하겠습니다.
http://localhost:8000
| 방식 | 주소 | 기능 |
|---|---|---|
| GET | /employees | 전체 사원 조회 |
| GET | /employees/1 | 1번 사원 조회 |
| POST | /employees | 사원 등록 |
| PUT | /employees/1 | 1번 사원 수정 |
| DELETE | /employees/1 | 1번 사원 삭제 |
이번 장에서는 먼저 GET 요청을 이용하여 서버 데이터를 가져오는 방법을 중심으로 학습합니다.
Vue와 연결하기 전에 간단한 FastAPI 서버를 만들어 봅니다.
pip install fastapi uvicorn
from fastapi import FastAPI
app = FastAPI()
employees = [
{
"id": 1,
"name": "홍길동",
"department": "개발팀"
},
{
"id": 2,
"name": "김철수",
"department": "영업팀"
},
{
"id": 3,
"name": "이영희",
"department": "관리팀"
}
]
@app.get("/employees")
def get_employees():
return employees
서버를 실행합니다.
uvicorn main:app --reload
브라우저에서 다음 주소에 접속합니다.
http://localhost:8000/employees
이제 FastAPI가 Vue에게 데이터를 제공할 준비가 되었습니다.
Vue에서 서버로 HTTP 요청을 보내는 방법에는 fetch()나 Axios 등이 있습니다.
Axios는 JavaScript에서 HTTP 요청을 편리하게 처리할 수 있도록 도와주는 라이브러리입니다. 이번 교재에서는 Axios를 사용합니다.
Vue 프로젝트 폴더의 터미널에서 설치합니다.
npm install axios
먼저 Axios를 불러옵니다.
import axios from 'axios'
GET 요청의 기본 형태는 다음과 같습니다.
axios.get( 'http://localhost:8000/employees' )
서버에서 응답한 결과를 변수에 저장할 수도 있습니다.
const response = await axios.get( 'http://localhost:8000/employees' ) console.log(response.data)
Axios로 받은 응답에서 서버가 전달한 실제 데이터는 response.data에 들어 있습니다.
서버에 요청을 보낸다고 해서 결과가 즉시 들어오는 것은 아닙니다.
따라서 응답이 올 때까지 기다리는 과정이 필요합니다. 이때 async와 await를 사용할 수 있습니다.
async function loadEmployees() {
const response = await axios.get(
'http://localhost:8000/employees'
)
}
await는 여기서는 쉽게 "서버의 응답이 올 때까지 기다린다"라고 이해하면 됩니다.
await를 사용하는 함수 앞에는 async를 붙입니다.
이제 FastAPI의 사원 데이터를 Vue에서 직접 가져와 보겠습니다.
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const employees = ref([])
async function loadEmployees() {
const response = await axios.get(
'http://localhost:8000/employees'
)
employees.value = response.data
}
</script>
<template>
<h1>사원목록</h1>
<button @click="loadEmployees">
사원 불러오기
</button>
<ul>
<li
v-for="emp in employees"
:key="emp.id"
>
{{ emp.id }}
{{ emp.name }}
{{ emp.department }}
</li>
</ul>
</template>
[사원 불러오기]
2 김철수 영업팀
3 이영희 관리팀
처음에는 빈 배열을 준비합니다.
const employees = ref([])
Axios로 서버에 데이터를 요청합니다.
const response = await axios.get( 'http://localhost:8000/employees' )
받은 데이터를 Vue의 반응형 데이터에 저장합니다.
employees.value = response.data
→ employees.value → v-for → 화면 출력
employees가 변경되면 Vue가 변경을 감지하고 화면을 자동으로 다시 출력합니다.
일반 JavaScript에서는 서버 데이터를 받아 HTML 문자열을 직접 만들고 DOM을 수정하는 방식을 사용할 수 있습니다.
fetch('/employees')
.then(response => response.json())
.then(data => {
let html = ''
data.forEach(emp => {
html += `
<li>
${emp.name}
</li>
`
})
document.getElementById(
'employeeList'
).innerHTML = html
})
Vue에서는 서버 데이터를 변수에 넣는 방식으로 처리할 수 있습니다.
employees.value = response.data
<li
v-for="emp in employees"
:key="emp.id"
>
{{ emp.name }}
</li>
| 방식 | 흐름 |
|---|---|
| 일반 DOM | 서버 데이터 → HTML 생성 → DOM 찾기 → innerHTML 변경 |
| Vue | 서버 데이터 → employees 변경 → 화면 자동 갱신 |
실제 프로그램에서는 버튼을 누르지 않아도 화면이 열리는 순간 데이터를 자동으로 가져오는 경우가 많습니다.
import {
ref,
onMounted
} from 'vue'
onMounted(() => {
loadEmployees()
})
<script setup>
import {
ref,
onMounted
} from 'vue'
import axios from 'axios'
const employees = ref([])
async function loadEmployees() {
const response = await axios.get(
'http://localhost:8000/employees'
)
employees.value = response.data
}
onMounted(() => {
loadEmployees()
})
</script>
<template>
<h1>사원목록</h1>
<ul>
<li
v-for="emp in employees"
:key="emp.id"
>
{{ emp.name }}
</li>
</ul>
</template>
→ loadEmployees() → FastAPI 요청
→ 사원 데이터 → 화면 출력
onMounted()는 컴포넌트가 화면에 만들어진 후 실행되는 함수입니다.
화면을 열자마자 서버 데이터를 불러오는 작업에 사용할 수 있습니다.
- 게시판 화면 열기 → 게시글 조회
- 상품목록 화면 열기 → 상품 조회
- 사원목록 화면 열기 → 사원 조회
<script setup>
import {
ref,
onMounted
} from 'vue'
import axios from 'axios'
const employees = ref([])
async function loadEmployees() {
const response = await axios.get(
'http://localhost:8000/employees'
)
employees.value = response.data
}
onMounted(() => {
loadEmployees()
})
</script>
<template>
<h1>사원목록</h1>
<table border="1">
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>부서</th>
</tr>
</thead>
<tbody>
<tr
v-for="emp in employees"
:key="emp.id"
>
<td>{{ emp.id }}</td>
<td>{{ emp.name }}</td>
<td>{{ emp.department }}</td>
</tr>
</tbody>
</table>
</template>
이제 사원 데이터는 Vue 파일 안에 직접 작성되어 있는 것이 아니라 FastAPI 서버에서 전달받습니다.
서버가 실행되지 않았거나 네트워크 문제가 발생하면 요청에 실패할 수 있습니다. 따라서 서버 통신에는 오류 처리를 추가하는 것이 좋습니다.
async function loadEmployees() {
try {
const response = await axios.get(
'http://localhost:8000/employees'
)
employees.value = response.data
} catch (error) {
console.log(error)
alert(
'사원정보를 불러오지 못했습니다.'
)
}
}
├─ 성공 → response.data
└─ 실패 → catch → 오류 처리
서버 응답을 기다리는 동안 사용자에게 데이터를 불러오는 중이라는 상태를 보여줄 수도 있습니다.
const loading = ref(false)
요청을 시작할 때 true로 변경합니다.
loading.value = true
요청이 끝나면 false로 변경합니다.
loading.value = false
<script setup>
import {
ref,
onMounted
} from 'vue'
import axios from 'axios'
const employees = ref([])
const loading = ref(false)
async function loadEmployees() {
loading.value = true
try {
const response = await axios.get(
'http://localhost:8000/employees'
)
employees.value = response.data
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
onMounted(() => {
loadEmployees()
})
</script>
<template>
<h1>사원목록</h1>
<p v-if="loading">
데이터를 불러오는 중입니다...
</p>
<table
v-else
border="1"
>
<tr>
<th>번호</th>
<th>이름</th>
<th>부서</th>
</tr>
<tr
v-for="emp in employees"
:key="emp.id"
>
<td>{{ emp.id }}</td>
<td>{{ emp.name }}</td>
<td>{{ emp.department }}</td>
</tr>
</table>
</template>
데이터를 불러오는 중입니다...
try ~ catch와 함께 finally를 사용할 수 있습니다.
try {
// 서버 요청
} catch (error) {
// 오류 처리
} finally {
// 성공/실패와 관계없이 실행
}
loading.value = true
try {
const response = await axios.get(
'http://localhost:8000/employees'
)
employees.value = response.data
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
서버 요청이 성공하든 실패하든 마지막에는 로딩 상태를 종료해야 하므로 loading.value = false를 finally에서 처리할 수 있습니다.
전체 사원 목록뿐 아니라 특정 번호의 사원 한 명만 조회할 수도 있습니다.
@app.get("/employees/{employee_id}")
def get_employee(employee_id: int):
for employee in employees:
if employee["id"] == employee_id:
return employee
return None
http://localhost:8000/employees/1
7장에서 배운 URL 파라미터를 API 요청과 연결할 수 있습니다.
/employees/1
Vue Router에서 사원번호를 읽고, 그 번호를 이용해 FastAPI에 요청합니다.
<script setup>
import {
ref,
onMounted
} from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
const route = useRoute()
const employee = ref(null)
async function loadEmployee() {
const id = route.params.id
const response = await axios.get(
'http://localhost:8000/employees/' + id
)
employee.value = response.data
}
onMounted(() => {
loadEmployee()
})
</script>
<template>
<h1>사원 상세정보</h1>
<div v-if="employee">
<p>
번호 : {{ employee.id }}
</p>
<p>
이름 : {{ employee.name }}
</p>
<p>
부서 : {{ employee.department }}
</p>
</div>
</template>
→ 2 → Axios → GET /employees/2
→ FastAPI → JSON → employee.value → 상세화면 출력
Vue와 FastAPI를 각각 실행하면 사용하는 주소가 서로 다릅니다.
| 프로그램 | 예시 주소 |
|---|---|
| Vue | http://localhost:5173 |
| FastAPI | http://localhost:8000 |
브라우저에서 서로 다른 주소의 서버로 요청할 때 CORS 문제가 발생할 수 있습니다.
from fastapi import FastAPI
from fastapi.middleware.cors import (
CORSMiddleware
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
개발 연습 단계에서 모든 출처를 허용하는 설정을 사용하는 경우도 있지만, 실제 운영 환경에서는 허용할 주소를 명확하게 지정하는 것이 좋습니다.
개발 중에는 Vue와 FastAPI를 각각 실행하는 경우가 많습니다.
uvicorn main:app --reload
npm run dev
↓
Vue : localhost:5173
↓ Axios
FastAPI : localhost:8000
↓
Database
같은 API 기본 주소를 여러 파일에서 반복해서 작성하면 나중에 주소가 변경될 때 수정하기 번거로울 수 있습니다.
const API_URL = 'http://localhost:8000'
const response = await axios.get( API_URL + '/employees' )
프로그램이 커지면 Axios의 기본 설정을 별도 파일로 분리하면 편리합니다.
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8000'
})
export default api
다른 Vue 파일에서는 다음처럼 불러옵니다.
import api from '../api/axios'
API 요청은 짧게 작성할 수 있습니다.
const response = await api.get( '/employees' )
매번 http://localhost:8000 전체 주소를 반복해서 작성할 필요가 없습니다.
사원과 관련된 API 요청 자체도 별도의 파일로 분리할 수 있습니다.
import api from './axios'
export function getEmployees() {
return api.get('/employees')
}
export function getEmployee(id) {
return api.get(
'/employees/' + id
)
}
화면에서는 필요한 함수만 불러옵니다.
import {
getEmployees
} from '../api/employeeApi'
const response = await getEmployees() employees.value = response.data
이렇게 구성하면 화면 코드와 서버 통신 코드를 분리해서 관리할 수 있습니다.
<script setup>
import {
ref,
onMounted
} from 'vue'
import api from '../api/axios'
const employees = ref([])
const loading = ref(false)
const errorMessage = ref('')
async function loadEmployees() {
loading.value = true
errorMessage.value = ''
try {
const response = await api.get(
'/employees'
)
employees.value = response.data
} catch (error) {
console.log(error)
errorMessage.value =
'사원정보를 불러오지 못했습니다.'
} finally {
loading.value = false
}
}
onMounted(() => {
loadEmployees()
})
</script>
<template>
<div>
<h1>사원목록</h1>
<button @click="loadEmployees">
새로고침
</button>
<p v-if="loading">
데이터를 불러오는 중입니다...
</p>
<p v-if="errorMessage">
{{ errorMessage }}
</p>
<table
v-if="!loading"
border="1"
>
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>부서</th>
</tr>
</thead>
<tbody>
<tr
v-for="emp in employees"
:key="emp.id"
>
<td>
{{ emp.id }}
</td>
<td>
<router-link
:to="{
name: 'employeeDetail',
params: {
id: emp.id
}
}"
>
{{ emp.name }}
</router-link>
</td>
<td>
{{ emp.department }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
→ API 요청 → loading = true
→ 성공 : employees 저장
→ 실패 : errorMessage 저장
→ finally : loading = false
→ 화면 자동 갱신
const employees = ref([
{ id: 1, name: '홍길동' }
])
연습할 때는 편리하지만 실제 서버나 데이터베이스의 데이터는 아닙니다.
const employees = ref([])
const response =
await api.get('/employees')
employees.value =
response.data
| 구분 | 데이터 구조 |
|---|---|
| 초기 연습 | Vue → 내부 데이터 |
| 서버 연동 | Vue → REST API → FastAPI → Database |
기존 Python 데스크톱 프로그램에서는 Python 코드가 데이터베이스에 직접 접근하는 구조를 사용할 수 있습니다.
cursor.execute(
"SELECT * FROM employee"
)
하지만 이 교재에서 다루는 웹 프로그램 구조에서는 브라우저에서 실행되는 Vue가 데이터베이스에 직접 접근하지 않습니다.
Vue는 FastAPI에 데이터를 요청합니다.
await api.get('/employees')
FastAPI가 데이터베이스를 조회합니다.
SELECT * FROM employee
FastAPI가 조회 결과를 JSON으로 만들어 Vue에 전달하고, Vue는 받은 데이터를 화면에 출력합니다.
기존 Python 프로그램의 구조와 가장 크게 달라지는 부분 중 하나는 프론트엔드와 데이터베이스 사이에 서버 API가 존재한다는 점입니다.
- 실제 웹 프로그램에서는 데이터를 Vue 파일에 직접 저장하기보다 서버와 데이터베이스에서 관리할 수 있습니다.
- 이번 장의 기본 구조는 Vue → REST API → FastAPI → Database입니다.
- Vue와 FastAPI 사이에서는 주로 JSON 형식으로 데이터를 주고받습니다.
- REST API에서는 GET, POST, PUT, DELETE 같은 HTTP 방식을 사용할 수 있으며, 이번 장에서는 주로 GET 조회를 학습했습니다.
- Vue에서 HTTP 요청을 편리하게 처리하기 위해 Axios를 사용할 수 있습니다.
- Axios 응답에서 서버가 전달한 실제 데이터는 response.data에 있습니다.
- 서버 응답을 기다리는 비동기 처리에는 async와 await를 사용할 수 있습니다.
- 서버에서 받은 데이터를 employees.value 등에 저장하면 Vue가 화면을 자동으로 갱신합니다.
- onMounted()를 이용하면 화면이 만들어진 후 서버 데이터를 자동으로 불러올 수 있습니다.
- 서버 통신에는 try ~ catch를 이용한 오류 처리를 적용할 수 있습니다.
- 요청 중인 상태를 표시하려면 loading과 v-if를 사용할 수 있습니다.
- finally는 요청 성공과 실패에 관계없이 마지막에 실행할 코드를 작성할 때 사용할 수 있습니다.
- Vue Router의 URL 파라미터와 Axios를 연결하면 특정 사원의 상세정보를 서버에서 조회할 수 있습니다.
- Vue와 FastAPI의 출처가 다르면 CORS 설정이 필요할 수 있습니다.
- Axios의 baseURL을 별도 파일로 관리하면 API 주소를 반복해서 작성하지 않아도 됩니다.
- API 요청 함수까지 별도 파일로 나누면 화면 코드와 서버 통신 코드를 분리해 관리할 수 있습니다.
- 가장 중요한 전체 흐름은 Vue 요청 → Axios → FastAPI → JSON 응답 → Vue 데이터 변경 → 화면 자동 갱신입니다.