Type something to search...

16 데이터셋 기반 DB 구축 & DB 활용 실습 — 리액트 연동 완성

1. 인트로

이번 차시는 커리큘럼의 최종 완성 단계이다. 1차시부터 학습한 모든 내용을 통합하여 실제 리액트 앱과 파이썬 API 서버를 연동한다.

최종 결과물: 리액트 영화 추천 앱

1
[리액트 앱] ←→ [파이썬 FastAPI 서버] ←→ [SQLite DB]

2. 인트로 — 전체 구조 이해

2.1. 최종 프로젝트 구조

1
project/
2
├── backend/
3
│ ├── main.py ← FastAPI(패스트에이피아이) 서버
4
│ ├── db.py ← DB 연결 및 초기화
5
│ └── movies.db ← SQLite DB 파일
6
└── frontend/
7
├── src/
8
│ ├── App.jsx
9
│ ├── MovieList.jsx
10
│ └── MovieDetail.jsx
11
└── package.json

2.2. 데이터 흐름

1
리액트 앱 → fetch('/api/movies') → FastAPI → SELECT * FROM movies → JSON 반환 → 리액트 화면

3. 데이터셋 기반 DB 구축 실습

3.1. DB 설계 및 생성

1
import sqlite3
2
import pandas as pd
3
4
conn = sqlite3.connect("movies.db")
5
cur = conn.cursor()
6
7
# 영화 테이블
8
cur.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 TEXT
17
)
18
""")
19
20
# 사용자 테이블
21
cur.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 NULL
27
)
28
""")
29
30
# 찜 목록 테이블
31
cur.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
43
conn.commit()
44
print("DB 스키마(Schema(스키마)) 생성 완료")

코드 설명:

  • 8~19행: movies 테이블. 영화 기본 정보를 저장한다.
  • 22~28행: users 테이블. 회원 정보를 저장한다.
  • 31~41행: favorites 테이블. 사용자와 영화를 연결하는 찜 목록이다.
  • 41행: UNIQUE(user_id, movie_id) — 같은 영화를 두 번 찜할 수 없다.

3.2. 데이터셋 기반 DB 구축 실습

1
# 영화 데이터 삽입
2
movies_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
]
14
cur.executemany(
15
"INSERT INTO movies (title,director,genre,year,rating,country) VALUES (?,?,?,?,?,?)",
16
movies_data
17
)
18
19
# 사용자 데이터 삽입
20
import hashlib
21
def hash_pw(pw):
22
return hashlib.sha256(pw.encode()).hexdigest()
23
24
users_data = [
25
("김철수", "kim@test.com", hash_pw("pass1234")),
26
("이영희", "lee@test.com", hash_pw("pass1234")),
27
("박지민", "park@test.com", hash_pw("pass1234")),
28
]
29
cur.executemany("INSERT INTO users (name,email,password) VALUES (?,?,?)", users_data)
30
31
# 찜 목록 데이터
32
fav_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
]
37
cur.executemany("INSERT INTO favorites (user_id,movie_id) VALUES (?,?)", fav_data)
38
39
conn.commit()
40
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. 다양한 조회 실습

1
# 장르별 평균 평점
2
df1 = pd.read_sql("""
3
SELECT genre AS 장르, COUNT(*) AS 편수, ROUND(AVG(rating),2) AS 평균평점
4
FROM movies
5
GROUP BY genre
6
ORDER BY 평균평점 DESC
7
""", conn)
8
print("=== 장르별 통계 ===")
9
print(df1)
10
11
# 사용자별 찜한 영화 목록
12
df2 = pd.read_sql("""
13
SELECT u.name AS 사용자, m.title AS 영화, m.genre AS 장르
14
FROM favorites AS f
15
INNER JOIN users AS u ON f.user_id = u.id
16
INNER JOIN movies AS m ON f.movie_id = m.id
17
ORDER BY u.name, m.title
18
""", conn)
19
print("\n=== 찜 목록 ===")
20
print(df2)

5. 파이썬 FastAPI(패스트에이피아이) 서버 구축

5.1. FastAPI 설치

Terminal window
1
pip install fastapi uvicorn
  • fastapi(패스트에이피아이): 파이썬 API 서버 프레임워크이다.
  • uvicorn(유비콘): FastAPI 서버를 실행하는 ASGI(에이에스지아이) 서버이다.

5.2. main.py 작성

backend/main.py
1
from fastapi import FastAPI
2
from fastapi.middleware.cors import CORSMiddleware
3
import sqlite3
4
5
app = FastAPI()
6
7
# CORS(코스) 설정 — 리액트(localhost:5173)에서 접근 허용
8
app.add_middleware(
9
CORSMiddleware,
10
allow_origins=["http://localhost:5173"],
11
allow_methods=["*"],
12
allow_headers=["*"],
13
)
14
15
def get_conn():
16
conn = sqlite3.connect("movies.db")
17
conn.row_factory = sqlite3.Row # 딕셔너리 형태로 결과 반환
18
return conn
19
20
# 전체 영화 목록
21
@app.get("/api/movies")
22
def 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}")
38
def 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")
48
def 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.rating
53
FROM favorites AS f
54
INNER JOIN movies AS m ON f.movie_id = m.id
55
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")
63
def 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. 서버 실행

Terminal window
1
cd backend
2
uvicorn main:app --reload
  • main:appmain.py 파일의 app 객체를 실행한다.
  • --reload — 코드 수정 시 서버를 자동으로 재시작한다.

브라우저에서 http://localhost:8000/docs를 열면 API 문서(Swagger(스웨거))를 확인할 수 있다.


6. 리액트와 API 연동

6.1. 리액트 프로젝트 생성

Terminal window
1
cd frontend
2
npm create vite@latest . -- --template react
3
npm install
4
npm run dev

6.2. MovieList.jsx — 영화 목록 컴포넌트

frontend/src/MovieList.jsx
1
import { useEffect, useState } from "react";
2
3
export default function MovieList() {
4
const [movies, setMovies] = useState([]);
5
const [genre, setGenre ] = useState("");
6
7
useEffect(() => {
8
const url = genre
9
? `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 최종 구성

frontend/src/App.jsx
1
import MovieList from "./MovieList";
2
3
export default function App() {
4
return (
5
<div>
6
<MovieList />
7
</div>
8
);
9
}

7. 활용 Plus — 찜하기 기능 연동

1
// 찜하기 버튼 기능
2
async 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 시 중복이 발생하면 오류를 발생시켜 데이터 무결성을 보장한다.