# Day20 My SQL + FastAPI + React 까지 사용

D0-$ANG ₩0N·2025년 12월 4일
post-thumbnail

1. SQLAlchemy에서 DELETE / UPDATE 패턴

DELETE 패턴

먼저 찾고 (SELECT 역할)

그 다음 지우고

commit()으로 반영

menu = db.query(Menu).filter(Menu.id == id).first()

if not menu:
    # 404 같은 예외 처리
    raise HTTPException(status_code=404, detail="Menu not found")

db.delete(menu)
db.commit()

UPDATE 패턴

조건에 맞는 레코드를 SELECT로 하나 꺼냄

필드를 수정

commit()으로 반영

menu = db.query(Menu).filter(Menu.id == id).first()

if not menu:
    raise HTTPException(status_code=404, detail="Menu not found")

menu.name = new_name
menu.price = new_price

db.commit()
db.refresh(menu)  

INSERT/UPDATE 후에는 보통 refresh() 한 번 해주는 패턴.

오토 커밋 vs 수동 커밋

DB 세션 만들 때 보통 autocommit=False로 설정함.

이 경우 INSERT / UPDATE / DELETE 후에 commit()을 반드시 호출해야 실제 DB에 반영됨.

ORM 코드 예:

db.delete(obj)
db.commit() # 이게 있어야 실제로 삭제 반영

만약 오토커밋을 True로 하면:

commit()을 직접 안 써도 되지만

트랜잭션 제어(롤백 등)가 어려워지고

성능/안전성 측면에서 손해일 수 있음.
오토커밋이 아니면 commit()을 반드시 호출해야 한다.
ORM은 SELECT → 객체 가져오기 → 필드 수정/삭제 → commit() 이 기본 패턴
DELETE/UPDATE도 결국 “조건으로 찾고, 그 결과를 조작”하는 과정이다.
ORM마다 문법은 다르지만, 기본 개념은 “조건으로 검색 후, 결과를 조작”이라는 것.

2. 정규화(Normalization), 조인(Join)

예시

테이블에 이렇게 저장했다고 가정:

NameClub
제라드축구부
트라웃야구부
르브론농구부

축구부 --> 배구부로 이름을 바꾸고 싶으면?

UPDATE member
SET dept = '배구부'
WHERE dept = '축구부';

레코드가 수십만 개면 업데이트 부하 + 장애 발생 시 데이터 불일치 위험.

해결법: 테이블 분리 (정규화)

club,name 정보를 따로 분리:

Club 테이블

IDClub
1축구부
2야구부
3농구부

Name 테이블

IDName
1제라드
2트라웃
3르브론

Member 테이블 (정규화 후)

IDNameClub_ID
1제라드1
2트라웃2
3르브론3

이제 축구부 이름만 바꾸고 싶으면:

UPDATE Club
SET Club = '배구부'
WHERE ID = 1;

멤버 테이블은 손댈 필요가 없고 성능과 일관성이 올라간다.

2-1.위에서 본 Primary Key & Foreign Key 관계

Club.ID → Primary Key

Name.Club_ID → Club.ID를 참조하는 Foreign Key

그렇다면 PK랑 FK는 무엇인가??

2-2.PK(Primary Key)

PK(프라이머리 키)는 데이터베이스의 각 행(row)을 고유하게 식별하는 데 사용되는 하나 이상의 컬럼(column)이다.

PK가 될수있는 조건은
유일함(Unique)
테이블 내 모든 행을 구별하는 유일한 기준이기때문
NULL 불가
반드시 값을 가져야 하며, NULL 값을 허용하지 않는다
테이블당 1개 (또는 복합키)
한 테이블에는 하나의 PK만 정의할 수 있습니다.

2-3.FK(Foreign Key)

어떤 테이블의 PK가
다른 테이블에서 데이터처럼 사용될 때 그 컬럼을 FK라고 부른다.

2-4. JOIN이 필요한 이유

정규화 이후에는
Name 테이블에는 club이름이 직접 없기 때문에
사람 이름 + Club 이름을 함께 보려면 JOIN이 필요하다.

SQL JOIN 예시

SELECT n.Name, c.Club
FROM Name n
JOIN Club c
    ON n.ID = c.ID;

JOIN 결과표

NameClub
제라드축구부
트라웃야구부
르브론농구부

정규화는 업데이트,삭제 시 성능,일관성 문제를 해결하기 위해 테이블을 쪼개는 과정이다.테이블을 나눴기 때문에 다시 합쳐서 보고 싶을 때는 JOIN이 필요하다.

3.CRUD 설계 개념

CRUD 기능 구성 요약

기능MethodURL설명
전체 조회GET/users리스트 조회
단일 조회GET/users/:idUpdate 모드 진입
등록POST/usersInsert
수정PUT/users/:idUpdate
삭제DELETE/users/:idDelete

React에서는 Insert 화면과 Update 화면을 따로 만들기보다 하나의 컴포넌트로 통합하는 것이 일반적이다.
id 값이 있으면 Update 모드, id 값이 없으면 Insert 모드로 동작하도록 설계한다.

예시
/write 새 회원 등록
/write/:id 기존 회원 수정

페이지 이동은 document.location 대신 useNavigate() 훅을 사용해야 한다.
React Router가 내부 history를 관리하므로 강제 이동 방식은 권장되지 않는다.

useState로 입력 필드 관리하기

입력 필드가 여러 개 있을 때, 각각 useState를 여러 번 선언하지 않고 하나의 객체로 묶어서 관리한다.

const initValue = {
  name: "",
  gender: "",
  mobile: ""
};

const [user, setUser] = useState(initValue);

하나의 onChange로 모든 입력 처리하기

React에서는 이벤트 객체에 name, value가 포함된다. 이를 이용해 공용 onChange 핸들러를 작성한다.

const onChange = (e) => {
    const { name, value } = e.target;
    setUser(prev => ({
        ...prev,
        [name]: value
    }));
};

이 방식의 장점
입력 필드가 10개든 20개든 onChange 한 개로 모두 처리 가능
확장성과 유지보수성이 높음

라디오 버튼 처리 방식

<input 
  type="radio" 
  name="gender" 
  value="male" 
  checked={user.gender === "male"} 
  onChange={onChange}
/>

<input 
  type="radio" 
  name="gender" 
  value="female" 
  checked={user.gender === "female"} 
  onChange={onChange}
/>

Insert vs Update 조건 분기

id 값이 있으면 Update 요청을 보내고, 없으면 Insert 요청을 보낸다.

if (!user.id) {
    await axios.post("/users", user);   // Insert
} else {
    await axios.put(`/users/${user.id}`, user);   // Update
}

useEffect로 최초 실행 제어하기

Update 모드일 때 기존 데이터를 불러오는 흐름

const { id } = useParams();

useEffect(() => {
    if (id) {
        fetchUser();
    }
}, []);

빈 배열을 넣으면 최초 렌더링 시 한 번만 실행된다.

페이지 이동은 useNavigate 사용

const navigate = useNavigate();
navigate("/users");

document.location.href 사용은 React Router 내부 제어 구조를 깨뜨리므로 비권장.

4.기본 구조 예시 실습

--------파이썬-----------
main.py

from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from database import engine, Base, get_db
from models import User

app = FastAPI()

origins = [
    "http://localhost:5173",
    "http://127.0.0.1:5173"
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Base.metadata.create_all(bind=engine)

@app.get("/user")
async def getAll(db: Session = Depends(get_db)):
    return db.query(User).all()

@app.get("/user/{id}")
async def getOne(id: int, db: Session = Depends(get_db)):
    return db.query(User).filter(User.id == id).first()

@app.post("/user/{name}/{gender}/{mobile}")
async def insert(name: str, gender: str, mobile: str, db: Session = Depends(get_db)):
    user = User(name=name, gender=gender, mobile=mobile)
    db.add(user)
    db.commit()
    db.refresh(user)
    return {"message": "ok"}

@app.put("/user/{id}/{name}/{gender}/{mobile}")
async def update(id: int, name: str, gender: str, mobile: str, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == id).first()
    user.name = name
    user.gender = gender
    user.mobile = mobile
    db.commit()
    return user

@app.delete("/user/{id}")
async def delete(id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == id).first()
    db.delete(user)
    db.commit()
    return {"message": f"deleted id={id}"}

models.py

from sqlalchemy import Column, Integer, String
from database import Base

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)  # Auto_increment 기본 설정
    name = Column(String(48), nullable=False)
    gender = Column(String(6))
    mobile = Column(String(20))

database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

DATABASE_URL = "mysql+pymysql://root:00000000@localhost:3306/kakao3"

engine = create_engine(DATABASE_URL)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

--------리액트-----------
Userlist.jsx

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import './mystyle.css'
axios.defaults.baseURL = "http://127.0.0.1:8000";

export default function UserList() {

    const [users, setUsers] = useState([])
    const navi = useNavigate()

    const doUpdate = (e) => {
        navi(`/write/${e.target.id}`)
    }

    const doDelete = async (e) => {
        if (!confirm("정말로 삭제할까요?")) return

        try {
            await axios.delete(`/user/${e.target.id}`)
        } catch (error) {
            console.log(error)
        }

        doGet()
    }

    const doGet = async () => {
        try {
            const res = await axios.get('/user')
            setUsers(res.data)
        } catch (error) {
            console.log(error)
        }
    }

    useEffect(() => { doGet() }, [])

    return (
        <>
            <button className='btn' onClick={() => navi("/write")}>추가</button>
            <br /><br />

            <table className="list-table">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Name</th>
                        <th>Gender</th>
                        <th>Mobile</th>
                        <th>수정 / 삭제</th>
                    </tr>
                </thead>

                <tbody>
                    {users.map(user => (
                        <tr key={user.id}>
                            <td>{user.id}</td>
                            <td>{user.name}</td>
                            <td>{user.gender}</td>
                            <td>{user.mobile}</td>
                            <td>
                                <button id={user.id} onClick={doUpdate}>수정</button> &nbsp;
                                <button id={user.id} onClick={doDelete}>삭제</button>
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </>
    )
}

Write.jsx

import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import axios from 'axios'
import './mystyle.css'

export default function Write() {

    const initValue = { name: '', gender: '', mobile: '' }
    const [user, setUser] = useState(initValue)
    const navi = useNavigate()
    const { id } = useParams()

    // -----------------------------
    // 입력 변경
    // -----------------------------
    const doChange = (e) => {
        const { name, value } = e.target
        setUser(x => ({ ...x, [name]: value }))
    }

    // -----------------------------
    // 등록
    // -----------------------------
    const doInsert = async () => {
        if (user.name === '') {
            alert("이름을 입력해야 합니다.")
            return
        }

        try {
            await axios.post(`/user/${user.name}/${user.gender}/${user.mobile}`)
            alert("등록 완료!")
            setUser(initValue)
            navi("/user")
        } catch (error) {
            console.log(error)
        }
    }

    // -----------------------------
    // 수정
    // -----------------------------
    const doUpdate = async () => {
        try {
            await axios.put(`/user/${id}/${user.name}/${user.gender}/${user.mobile}`)
            alert("수정 완료!")
            navi("/user")
        } catch (error) {
            console.log(error)
        }
    }

    // -----------------------------
    // 조회 (수정 모드일 때만 사용)
    // -----------------------------
    const fetchUser = async (uid) => {
        try {
            const res = await axios.get(`/user/${uid}`)
            setUser(res.data)
        } catch (error) {
            console.log(error)
        }
    }

    // -----------------------------
    // 화면 로딩 시 id 있으면 데이터 조회
    // -----------------------------
    useEffect(() => {
        if (!id) return

        // effect 내부에서 setState 사용 → ESLint 경고 방지용 async wrapper
        (async () => {
            await fetchUser(id)
        })()
    }, [id])

    return (
        <div>
            <h2>{id ? "회원 수정" : "회원 등록"}</h2>

            <table className="write-table">
                <tbody>
                    <tr>
                        <td>Name</td>
                        <td>
                            <input
                                type="text"
                                name="name"
                                value={user.name}
                                onChange={doChange}
                            />
                        </td>
                    </tr>

                    <tr>
                        <td>Gender</td>
                        <td>
                            <input
                                type="radio"
                                name="gender"
                                value="male"
                                checked={user.gender === 'male'}
                                onChange={doChange}
                            /> Male

                            <input
                                type="radio"
                                name="gender"
                                value="female"
                                checked={user.gender === 'female'}
                                onChange={doChange}
                            /> Female
                        </td>
                    </tr>

                    <tr>
                        <td>Mobile</td>
                        <td>
                            <input
                                type="text"
                                name="mobile"
                                value={user.mobile}
                                onChange={doChange}
                            />
                        </td>
                    </tr>

                    <tr>
                        <td colSpan="2" style={{ textAlign: "center" }}>
                            {id
                                ? <button onClick={doUpdate}>수정</button>
                                : <button onClick={doInsert}>등록</button>}
                            &nbsp;
                            <button onClick={() => navi("/user")}>취소</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    )
}

App.jsx

import { BrowserRouter, Routes, Route } from "react-router-dom";
import UserList from "./Userlist";
import Write from "./Write";

export default function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/user" element={<UserList />} />
                <Route path="/write" element={<Write />} />
                <Route path="/write/:id" element={<Write />} />
            </Routes>
        </BrowserRouter>
    );
}

결과사진


http://localhost:5173/user 화면

추가번튼을 누르면 이쪽으로 이동http://localhost:5173/write

등록하면 등록완료! 라고 메세지

삭제하면 정말로 삭제할까요! 메세지 나오고 확인 누르면

삭제된다

3번 훌리오 마르티네즈를 수정하고싶으면 수정버튼을 눌러서

수정칸에 들어가서 수정을하면

수정을 누르면 수정완료가 뜨고

수정된것을 볼수있다

profile
Change Up

0개의 댓글