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