데이터셋 기반 DB 구축 & DB 활용 실습 — 리액트 연동 완성
코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.
구문
1. 인트로
이번 차시는 커리큘럼의 최종 완성 단계이다. 1차시부터 학습한 모든 내용을 통합하여 실제 리액트 앱과 파이썬 API 서버를 연동한다.
최종 결과물: 리액트 영화 추천 앱
[리액트 앱] ←→ [파이썬 FastAPI 서버] ←→ [SQLite DB]
2. 인트로 — 전체 구조 이해
2.1. 최종 프로젝트 구조
project/
├── backend/
│ ├── main.py ← FastAPI(패스트에이피아이) 서버
│ ├── db.py ← DB 연결 및 초기화
│ └── movies.db ← SQLite DB 파일
└── frontend/
├── src/
│ ├── App.jsx
│ ├── MovieList.jsx
│ └── MovieDetail.jsx
└── package.json
2.2. 데이터 흐름
리액트 앱 → fetch('/api/movies') → FastAPI → SELECT * FROM movies → JSON 반환 → 리액트 화면
3. 데이터셋 기반 DB 구축 실습
3.1. DB 설계 및 생성
import sqlite3
import pandas as pd
conn = sqlite3.connect("movies.db")
cur = conn.cursor()
# 영화 테이블
cur.execute("""
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
director TEXT,
genre TEXT,
year INTEGER,
rating REAL,
country TEXT
)
""")
# 사용자 테이블
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)
""")
# 찜 목록 테이블
cur.execute("""
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
movie_id INTEGER NOT NULL,
added_dt TEXT DEFAULT (datetime('now','localtime')),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (movie_id) REFERENCES movies(id),
UNIQUE(user_id, movie_id)
)
""")
conn.commit()
print("DB 스키마(Schema(스키마)) 생성 완료")
코드 설명:
- 8~19행: movies 테이블. 영화 기본 정보를 저장한다.
- 22~28행: users 테이블. 회원 정보를 저장한다.
- 31~41행: favorites 테이블. 사용자와 영화를 연결하는 찜 목록이다.
- 41행:
UNIQUE(user_id, movie_id)— 같은 영화를 두 번 찜할 수 없다.
3.2. 데이터셋 기반 DB 구축 실습
# 영화 데이터 삽입
movies_data = [
("기생충", "봉준호", "드라마", 2019, 9.5, "한국"),
("범죄도시4", "허명행", "액션", 2024, 8.2, "한국"),
("인터스텔라", "크리스토퍼 놀런", "SF", 2014, 8.8, "미국"),
("어벤져스: 엔드게임","루소 형제", "액션", 2019, 8.4, "미국"),
("너의 이름은", "신카이 마코토", "애니메이션", 2016, 8.7, "일본"),
("오펜하이머", "크리스토퍼 놀런", "드라마", 2023, 8.9, "미국"),
("올드보이", "박찬욱", "스릴러", 2003, 9.2, "한국"),
("센과 치히로", "미야자키 하야오", "애니메이션", 2001, 9.3, "일본"),
("매트릭스", "워쇼스키 자매", "SF", 1999, 8.7, "미국"),
("범죄도시", "강윤성", "액션", 2017, 8.5, "한국"),
]
cur.executemany(
"INSERT INTO movies (title,director,genre,year,rating,country) VALUES (?,?,?,?,?,?)",
movies_data
)
# 사용자 데이터 삽입
import hashlib
def hash_pw(pw):
return hashlib.sha256(pw.encode()).hexdigest()
users_data = [
("김철수", "kim@test.com", hash_pw("pass1234")),
("이영희", "lee@test.com", hash_pw("pass1234")),
("박지민", "park@test.com", hash_pw("pass1234")),
]
cur.executemany("INSERT INTO users (name,email,password) VALUES (?,?,?)", users_data)
# 찜 목록 데이터
fav_data = [
(1, 1), (1, 3), (1, 6), (1, 9),
(2, 1), (2, 7), (2, 8),
(3, 2), (3, 4), (3, 5),
]
cur.executemany("INSERT INTO favorites (user_id,movie_id) VALUES (?,?)", fav_data)
conn.commit()
print(f"영화 {len(movies_data)}편, 사용자 {len(users_data)}명 삽입 완료")
코드 설명:
- 19~21행:
hashlib.sha256()— 비밀번호를 해시(Hash(해시))로 변환하여 저장한다. 평문 저장은 보안에 취약하다. - 31~34행: 각 사용자의 찜 목록 데이터를 삽입한다.
- 41행:
UNIQUE(user_id, movie_id)제약으로 중복 찜이 방지된다.
4. 데이터셋 기반 DB 활용 실습
4.1. 다양한 조회 실습
# 장르별 평균 평점
df1 = pd.read_sql("""
SELECT genre AS 장르, COUNT(*) AS 편수, ROUND(AVG(rating),2) AS 평균평점
FROM movies
GROUP BY genre
ORDER BY 평균평점 DESC
""", conn)
print("=== 장르별 통계 ===")
print(df1)
# 사용자별 찜한 영화 목록
df2 = pd.read_sql("""
SELECT u.name AS 사용자, m.title AS 영화, m.genre AS 장르
FROM favorites AS f
INNER JOIN users AS u ON f.user_id = u.id
INNER JOIN movies AS m ON f.movie_id = m.id
ORDER BY u.name, m.title
""", conn)
print("\n=== 찜 목록 ===")
print(df2)
5. 파이썬 FastAPI(패스트에이피아이) 서버 구축
5.1. FastAPI 설치
pip install fastapi uvicorn
- fastapi(패스트에이피아이): 파이썬 API 서버 프레임워크이다.
- uvicorn(유비콘): FastAPI 서버를 실행하는 ASGI(에이에스지아이) 서버이다.
5.2. main.py 작성
# backend/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import sqlite3
app = FastAPI()
# CORS(코스) 설정 — 리액트(localhost:5173)에서 접근 허용
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
def get_conn():
conn = sqlite3.connect("movies.db")
conn.row_factory = sqlite3.Row # 딕셔너리 형태로 결과 반환
return conn
# 전체 영화 목록
@app.get("/api/movies")
def get_movies(genre: str = None, order: str = "rating"):
conn = get_conn()
cur = conn.cursor()
sql = "SELECT * FROM movies"
params = []
if genre:
sql += " WHERE genre = ?"
params.append(genre)
sql += f" ORDER BY {order} DESC"
cur.execute(sql, params)
rows = cur.fetchall()
conn.close()
return [dict(r) for r in rows]
# 영화 상세 정보
@app.get("/api/movies/{movie_id}")
def get_movie(movie_id: int):
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT * FROM movies WHERE id = ?", (movie_id,))
row = cur.fetchone()
conn.close()
return dict(row) if row else {"error": "없는 영화입니다."}
# 사용자의 찜 목록
@app.get("/api/users/{user_id}/favorites")
def get_favorites(user_id: int):
conn = get_conn()
cur = conn.cursor()
cur.execute("""
SELECT m.id, m.title, m.genre, m.rating
FROM favorites AS f
INNER JOIN movies AS m ON f.movie_id = m.id
WHERE f.user_id = ?
""", (user_id,))
rows = cur.fetchall()
conn.close()
return [dict(r) for r in rows]
# 찜 추가
@app.post("/api/favorites")
def add_favorite(data: dict):
conn = get_conn()
cur = conn.cursor()
try:
cur.execute(
"INSERT INTO favorites (user_id, movie_id) VALUES (?, ?)",
(data["user_id"], data["movie_id"])
)
conn.commit()
return {"success": True, "message": "찜 추가 완료"}
except:
return {"success": False, "message": "이미 찜한 영화입니다."}
finally:
conn.close()
코드 설명:
- 2~3행: FastAPI와 CORS(코스) 미들웨어(Middleware(미들웨어))를 불러온다.
- 8~13행: CORS 설정. 리액트 개발 서버(localhost:5173)에서의 API 요청을 허용한다.
- 17행:
row_factory = sqlite3.Row— 쿼리 결과를 딕셔너리처럼 접근할 수 있게 한다. - 22행:
@app.get("/api/movies")— GET 요청이 오면 이 함수를 실행한다. - 22행:
genre: str = None— URL 쿼리 파라미터./api/movies?genre=액션형태로 전달한다. - 37행:
{movie_id}는 URL 경로 파라미터이다./api/movies/1이면movie_id=1이다.
5.3. 서버 실행
cd backend
uvicorn main:app --reload
main:app—main.py파일의app객체를 실행한다.--reload— 코드 수정 시 서버를 자동으로 재시작한다.
브라우저에서 http://localhost:8000/docs를 열면 API 문서(Swagger(스웨거))를 확인할 수 있다.
6. 리액트와 API 연동
6.1. 리액트 프로젝트 생성
cd frontend
npm create vite@latest . -- --template react
npm install
npm run dev
6.2. MovieList.jsx — 영화 목록 컴포넌트
// frontend/src/MovieList.jsx
import { useEffect, useState } from "react";
export default function MovieList() {
const [movies, setMovies] = useState([]);
const [genre, setGenre ] = useState("");
useEffect(() => {
const url = genre
? `http://localhost:8000/api/movies?genre=${genre}`
: "http://localhost:8000/api/movies";
fetch(url)
.then(res => res.json())
.then(data => setMovies(data));
}, [genre]);
return (
<div>
<h1>영화 목록</h1>
<select onChange={e => setGenre(e.target.value)}>
<option value="">전체</option>
<option value="액션">액션</option>
<option value="드라마">드라마</option>
<option value="SF">SF</option>
</select>
{movies.map(m => (
<div key={m.id}>
<h3>{m.title} ({m.year})</h3>
<p>장르: {m.genre} | 평점: ⭐ {m.rating}</p>
</div>
))}
</div>
);
}
코드 설명:
- 5행:
movies상태(State(스테이트))에 API에서 받은 영화 목록을 저장한다. - 8~15행:
useEffect— genre가 변경될 때마다 API를 다시 호출한다. - 9~11행: genre가 있으면 쿼리 파라미터를 추가하여 필터링 요청을 보낸다.
- 13~14행:
fetch()로 API 호출 →.json()으로 파싱 →setMovies()로 상태 업데이트 - 27~32행:
movies.map()으로 각 영화를 화면에 렌더링한다.
6.3. App.jsx 최종 구성
// frontend/src/App.jsx
import MovieList from "./MovieList";
export default function App() {
return (
<div>
<MovieList />
</div>
);
}
7. 활용 Plus — 찜하기 기능 연동
// 찜하기 버튼 기능
async function addFavorite(userId, movieId) {
const res = await fetch("http://localhost:8000/api/favorites", {
method : "POST",
headers: { "Content-Type": "application/json" },
body : JSON.stringify({ user_id: userId, movie_id: movieId }),
});
const data = await res.json();
alert(data.message);
}
코드 설명:
- 3행:
fetch()에 두 번째 인자로 옵션 객체를 전달한다. - 4행:
method: "POST"— 데이터를 생성하는 요청이므로 POST를 사용한다. - 5행:
"Content-Type": "application/json"— 요청 본문이 JSON 형식임을 서버에 알린다. - 6행:
JSON.stringify()— 자바스크립트 객체를 JSON 문자열로 변환한다.
8. 문제풀기
Q1. CORS(코스)를 설정하는 이유는?
브라우저 보안 정책상 서로 다른 출처(Origin(오리진)) 간의 HTTP 요청은 기본적으로 차단된다. 리액트(localhost:5173)에서 FastAPI(localhost:8000)로 요청할 때 CORS 설정이 없으면 오류가 발생한다.
Q2. row_factory = sqlite3.Row를 설정하는 이유는?
기본적으로 쿼리 결과는 튜플로 반환된다. sqlite3.Row를 설정하면 결과를 딕셔너리처럼 컬럼명으로 접근할 수 있어 JSON 변환이 편리해진다.
Q3. 리액트에서 API를 호출하는 기본 흐름을 설명하시오.
① useState로 데이터를 저장할 상태를 선언한다. ② useEffect 안에서 fetch()로 API를 호출한다. ③ .then(res => res.json())으로 응답을 JSON으로 파싱한다. ④ setState()로 상태를 업데이트한다. ⑤ 컴포넌트가 리렌더링되며 화면에 데이터가 표시된다.
Q4. UNIQUE(user_id, movie_id) 제약이 찜 목록 테이블에서 하는 역할은?
같은 사용자가 같은 영화를 두 번 찜하는 중복 데이터를 DB 수준에서 방지한다. INSERT 시 중복이 발생하면 오류를 발생시켜 데이터 무결성을 보장한다.