#Day24-DataBase FastAPI + React 로그인/세션 + DB 연동 하는 프로젝트 만들어 보기

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

1. 웹 인증 방식 개요

웹에서 인증하는 방식은 크게 세 가지가 있다.

세션(Session)

쿠키(Cookie)

JWT(Json Web Token)

세션(Session)은 서버가 사용자 정보를 메모리에 저장하고, 클라이언트는 세션 키만 쿠키로 들고 있는 구조다.
그래서 로그인 여부를 서버가 판단할 수 있다.

2. FastAPI에서 세션 처리

FastAPI에서 세션을 사용하려면 별도 미들웨어가 필요하다.

미들웨어 설치:

SessionMiddleware

애플리케이션에 추가:

app.add_middleware(SessionMiddleware, secret_key="임의의키")

세션 사용 흐름:

request.session["login_id"] = 사용자아이디

이 값이 들어 있으면 로그인된 상태,
없으면 로그인되지 않은 상태라고 판단한다.

로그아웃:

request.session.clear()

3. 로그인 과정의 서버 처리 흐름

로그인이 들어왔을 때 서버는 다음 순서로 처리한다.

클라이언트가 입력한 id, password를 request로 받는다.

DB에서 해당 id/pass가 맞는지 검사한다.

일치하면

request.session["login_id"] = id
return "OK"

일치하지 않으면

return "FAIL"

결과는 React에서 받아 화면을 바꾼다.

4. 로그인 상태 확인(check session)

홈 화면을 띄울 때 클라이언트는 서버에 한 번 확인 요청을 보낸다.

GET /check_login

서버는 세션에 값이 설정돼 있는지 검사한다.

세션이 존재하면:

return "OK"

세션이 없으면:

return "FAIL"

이 값에 따라 React에서 로그인이 되어 있는지, 로그아웃 버튼을 보여줄지 등을 결정한다.

5. React에서의 인증 처리 구조

React는 다음과 같은 흐름으로 화면 상태를 유지한다.

페이지 로딩 시 useEffect에서 /check_login 호출

서버가 OK → loggedIn = true
서버가 FAIL → loggedIn = false

상태에 따라 화면에 로그인/회원가입 또는 로그아웃 버튼 표시

로그인 버튼 클릭 시
fetch로 /login 호출하여 결과 확인
성공 → 홈으로 이동
실패 → 알림 표시

로그아웃 시
fetch(/logout) 후
상태값 loggedIn을 false 변경

인증 상태는 UI를 변화시키는 중요한 기준이 된다.

FastAPI + React 통신 주의점

React에서 fetch 요청을 보낼 때 세션 정보를 유지하려면 다음 옵션이 필요하다.

credentials: "include"

이 옵션이 없으면 세션 쿠키가 전달되지 않아 로그인 상태가 사라진다.

7. DB 세션과 FastAPI 세션 혼동 주의

DB 트랜잭션은 MySQL 내부의 세션
FastAPI 세션은 HTTP 세션

두 기능은 서로 완전히 다른 영역이다.

EX):

Workbench에서 INSERT 후 commit을 안 하면 다른 프로그램에서는 보이지 않는다.

FastAPI에서 INSERT 후 commit을 하면 그때 비로소 DB에 반영된다.

강의에서 여러 학생들이 “DB에서 안 보인다”는 문제는 대부분 commit을 하지 않은 경우였다.

8. 회원가입 구현 흐름

회원 가입은 다음 흐름으로 진행된다.

React에서 사용자 정보를 form 형태로 JSON 전송

FastAPI에서 데이터를 받아 DB INSERT

INSERT 성공 → "OK" 반환
실패 → "FAIL" 반환

React에서 성공 여부를 알람으로 보여주고 페이지 이동

DB 컬럼이 NOT NULL인데 값을 안 넣으면 INSERT가 실패할 수 있다.
그러면 컬럼 제약조건을 수정해야 한다.

EX):

ALTER TABLE users MODIFY name VARCHAR(50) NULL;

9. 로그인한 사용자 정보를 게시판 등에서 사용하기

로그인한 사람의 정보를 작성자(author)로 자동 채워 넣을 수 있다.

예를 들어 게시판 글쓰기에서:

서버는 request.session["login_id"] 를 가져와

author = login_id

이 값을 DB에 저장한다.

React에서는 작성자 input은 readonly로 만들고
자동으로 로그인 아이디를 보여주는 식으로 구현한다.

10. DB 트랜잭션 개념 (중요)

트랜잭션은 작업 단위를 한 덩어리로 묶는 개념이다.

작업이 모두 성공해야 commit
중간에 하나라도 실패하면 rollback

EX):

주문을 저장하고

매출 기록을 저장하고

마일리지를 적립하는 3단계가 있을 때

2단계에서 실패했다면
1단계도 롤백해야 데이터 오류가 없다.

FastAPI에서 트랜잭션을 묶는 방식은:

try:
insert order
insert sales
db.commit()
return "OK"
except:
db.rollback()
return "FAIL"

11.과제

Database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from dotenv import load_dotenv
import os

load_dotenv()

DB_USER = os.getenv("DB_USER", "root")
DB_PASS = os.getenv("DB_PASS", "00000000")
DB_HOST = os.getenv("DB_HOST", "127.0.0.1")
DB_NAME = os.getenv("DB_NAME", "kakao3")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:3306/{DB_NAME}"

engine = create_engine(
    DATABASE_URL,
    echo=True
)

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

Base = declarative_base()


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

models.py

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


class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True, index=True)
    loginid = Column(String(24), nullable=False, unique=True)
    password = Column(String(24), nullable=False)
    name = Column(String(48), nullable=True)
    gender = Column(String(6), nullable=True)
    mobile = Column(String(20), nullable=True)


class Board(Base):
    __tablename__ = "board"

    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(256), nullable=False)
    content = Column(String(5000))
    writer = Column(String(20))
    hit = Column(Integer, default=0)
    created = Column(DateTime, default=datetime.datetime.now)
    updated = Column(
        DateTime,
        default=datetime.datetime.now,
        onupdate=datetime.datetime.now
    )

main.py

from fastapi import FastAPI, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from database import Base, engine, get_db
from models import User, Board
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware

app = FastAPI()

# 세션
app.add_middleware(SessionMiddleware, secret_key="mysecretkey")

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 테이블 생성
Base.metadata.create_all(bind=engine)


# 회원가입 
@app.post("/signup")
async def signup(request: Request, db: Session = Depends(get_db)):
    data = await request.json()
    loginid = data.get("loginid")
    password = data.get("password")
    name = data.get("name")
    gender = data.get("gender")
    mobile = data.get("mobile")

    exist = db.query(User).filter(User.loginid == loginid).first()
    if exist:
        return {"result": "fail", "msg": "이미 존재하는 아이디"}

    user = User(
        loginid=loginid,
        password=password,
        name=name,
        gender=gender,
        mobile=mobile
    )

    db.add(user)
    db.commit()
    db.refresh(user)

    return {"result": "ok", "user_id": user.id}


# 로그인 
@app.post("/login")
async def login(request: Request, db: Session = Depends(get_db)):
    data = await request.json()
    loginid = data.get("loginid")
    password = data.get("password")

    user = db.query(User).filter(
        User.loginid == loginid,
        User.password == password
    ).first()

    if user is None:
        return {"result": "fail", "msg": "로그인 실패"}

    # 세션에 로그인 정보 저장
    request.session["loginid"] = loginid
    request.session["username"] = user.name

    return {"result": "ok", "user": user.name}


# 로그인 상태 체크
@app.get("/checkLogin")
async def check_login(request: Request):
    loginid = request.session.get("loginid")
    username = request.session.get("username")

    if loginid:
        return {"result": "ok", "loginid": loginid, "username": username}

    return {"result": "fail"}


# 로그아웃
@app.post("/logout")
async def logout(request: Request):
    request.session.clear()
    return {"result": "ok"}


# 유저 조회 (덤) 
@app.get("/users")
def get_users(db: Session = Depends(get_db)):
    users = db.query(User).all()
    return users


@app.get("/user/{id}")
def get_one(id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == id).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user


# 게시판 
@app.get("/boards")
def board_list(db: Session = Depends(get_db)):
    boards = db.query(Board).order_by(Board.id.desc()).all()
    return boards


@app.get("/board/{id}")
def board_detail(id: int, db: Session = Depends(get_db)):
    board = db.query(Board).filter(Board.id == id).first()

    if not board:
        raise HTTPException(status_code=404, detail="Not found")

    board.hit += 1
    db.commit()
    db.refresh(board)

    return board


@app.post("/board")
async def board_write(request: Request, db: Session = Depends(get_db)):
    # 로그인 한 사람만 글쓰기
    loginid = request.session.get("loginid")
    if not loginid:
        return {"result": "fail", "msg": "로그인이 필요합니다."}

    data = await request.json()
    title = data.get("title")
    content = data.get("content")

    board = Board(
        title=title,
        content=content,
        writer=loginid
    )
    db.add(board)
    db.commit()
    db.refresh(board)

    return {"result": "ok", "id": board.id}

App.jsx

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./Home";
import Login from "./Login";
import Signup from "./Signup";
import BoardList from "./BoardList";
import BoardDetail from "./BoardDetail";
import Write from "./Write";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/home" element={<Home />} />
        <Route path="/login" element={<Login />} />
        <Route path="/signup" element={<Signup />} />
        <Route path="/boards" element={<BoardList />} />
        <Route path="/board/:id" element={<BoardDetail />} />
        <Route path="/write" element={<Write />} />
      </Routes>
    </BrowserRouter>
  );
}

Home.jsx

import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios";

export default function Home() {
    const [logged, setLogged] = useState(false);
    const [username, setUsername] = useState("");

    useEffect(() => {
        const check = async () => {
            try {
                const res = await axios.get("http://localhost:8000/checkLogin", {
                    withCredentials: true, 
                });

                console.log("checkLogin:", res.data);

                if (res.data.result === "ok") {
                    setLogged(true);
                    setUsername(res.data.username || res.data.loginid);
                } else {
                    setLogged(false);
                    setUsername("");
                }
            } catch (err) {
                console.error("checkLogin error:", err);
                setLogged(false);
            }
        };

        check();
    }, []);

    return (
        <div>
            <h1>HOME</h1>

            {logged ? (
                <>
                    <p>{username} 님 환영합니다!</p>

                    <Link to="/boards">게시판</Link>
                    <br />
                    <Link to="/write">글쓰기</Link>
                    <br />

                    {
                    <button
                        onClick={async () => {
                            await axios.post(
                                "http://localhost:8000/logout",
                                {},
                                { withCredentials: true }
                            );
                            window.location.href = "/"; 
                        }}
                    >
                        LOGOUT
                    </button>
                </>
            ) : (
                <>
                    <Link to="/login">LOGIN</Link>
                    <br />
                    <Link to="/signup">REGISTER</Link>
                </>
            )}
        </div>
    );
}

Write.jsx

import { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";

export default function Write() {
    const navigate = useNavigate();

    const [form, setForm] = useState({ title: "", content: "" });

    const change = (e) => {
        setForm({ ...form, [e.target.name]: e.target.value });
    };

    const save = async () => {
        try {
            const res = await axios.post(
                "http://localhost:8000/board",
                {
                    title: form.title,
                    content: form.content,
                },
                { withCredentials: true } 
            );

            if (res.data.result === "ok") {
                alert("등록 완료");
                navigate("/boards");
            } else {
                alert("등록 실패: " + (res.data.msg || ""));
            }
        } catch (err) {
            console.error(err);
            alert("서버 오류로 등록 실패");
        }
    };

    return (
        <div>
            <h2>글쓰기</h2>

            제목:
            <input name="title" onChange={change} /> <br />
            내용:
            <textarea name="content" onChange={change} /> <br />
            <button onClick={save}>등록</button>
            <button onClick={() => navigate("/boards")}>취소</button>
        </div>
    );
}

Signup.jsx

import { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";

export default function Signup() {
    const navigate = useNavigate();

    const [form, setForm] = useState({
        loginid: "",
        password: "",
        name: "",
        gender: "",
        mobile: "",
    });

    const change = (e) => {
        setForm({ ...form, [e.target.name]: e.target.value });
    };

    const save = async (e) => {
        e.preventDefault();
        const res = await axios.post("http://localhost:8000/signup", form);

        if (res.data.result === "ok") {
            alert("회원가입 완료");
            navigate("/login");
        } else {
            alert("회원가입 실패: " + (res.data.msg || ""));
        }
    };

    return (
        <form onSubmit={save}>
            <h2>회원가입</h2>
            <input
                name="loginid"
                placeholder="로그인 아이디"
                onChange={change}
            />
            <br />
            <input
                name="password"
                type="password"
                placeholder="비밀번호"
                onChange={change}
            />
            <br />
            <input name="name" placeholder="이름" onChange={change} />
            <br />
            <input name="gender" placeholder="성별" onChange={change} />
            <br />
            <input name="mobile" placeholder="휴대폰번호" onChange={change} />
            <br />
            <button type="submit">가입하기</button>
        </form>
    );
}

BoardList.jsx

import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios";

export default function BoardList() {
    const [boards, setBoards] = useState([]);

    useEffect(() => {
        const load = async () => {
            const res = await axios.get("http://localhost:8000/boards", {
                withCredentials: true,
            });
            setBoards(res.data);
        };
        load();
    }, []);

    return (
        <div>
            <h2>게시판</h2>
            <Link to="/write">글쓰기</Link>
            <ul>
                {boards.map((b) => (
                    <li key={b.id}>
                        <Link to={`/board/${b.id}`}>
                            {b.title} / {b.writer} / 조회수 {b.hit}
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

BoardDetail.jsx

import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom";
import axios from "axios";

export default function BoardDetail() {
    const { id } = useParams();
    const [board, setBoard] = useState(null);

    useEffect(() => {
        const load = async () => {
            const res = await axios.get(`http://localhost:8000/board/${id}`, {
                withCredentials: true,
            });
            setBoard(res.data);
        };
        load();
    }, [id]);

    if (!board) return <div>로딩중...</div>;

    return (
        <div>
            <h2>{board.title}</h2>
            <p>작성자: {board.writer}</p>
            <p>조회수: {board.hit}</p>
            <pre>{board.content}</pre>
            <Link to="/boards">목록으로</Link>
        </div>
    );
}

main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Login.jsx

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";

export default function Login() {
    const navigate = useNavigate();

    const [user, setUser] = useState({ loginid: "", password: "" });

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

    const doLogin = async () => {
        try {
            const res = await axios.post(
                "http://localhost:8000/login",
                user,
                { withCredentials: true } 
            );

            console.log("로그인 결과:", res.data);

            if (res.data.result === "ok") {
                alert("로그인 성공");
                navigate("/home");
            } else {
                alert("❌ 아이디 또는 비밀번호가 틀렸습니다.");
            }
        } catch (error) {
            console.log("로그인 중 오류:", error);
            alert("🚨 서버 오류로 로그인 실패");
        }
    };

    return (
        <div>
            <h1>로그인</h1>
            <table>
                <tbody>
                    <tr>
                        <td>ID</td>
                        <td>
                            <input
                                name="loginid"
                                placeholder="아이디"
                                onChange={doChange}
                            />
                        </td>
                    </tr>
                    <tr>
                        <td>Password</td>
                        <td>
                            <input
                                name="password"
                                type="password"
                                placeholder="비밀번호"
                                onChange={doChange}
                            />
                        </td>
                    </tr>
                    <tr>
                        <td colSpan="2">
                            <button onClick={doLogin}>로그인</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    );
}

11.2 결과


첫화면 로그인버튼/회원가입 버튼 있음.

회원가입

로그인 하면 뜨는 화면

실제 데이터베이스에도 저장되는것을 볼수있다.

글쓰기화면

실제로 글을쓰면 글 목록에 제목과작성자 그리고 조회수가 뜬다

글 들어가면 보이는 모습

실제 데이터베이스에도 저장된다.

다른 아이디로도 로그인해도 같은결과가 나온다.

11.3 코드 분석

Backend : FastAPI

DB 연결

회원가입

로그인 + 세션 저장

로그인 여부 체크

글쓰기 제한 (로그인한 사람만 가능)

게시판 목록 조회

게시글 상세 조회

Frontend : React

로그인/회원가입 폼

세션 유지 위해 withCredentials: true 사용

로그인 여부 UI 반영

게시판 리스트, 글 상세, 글쓰기

2. 세션 기반 인증 구조

세션 저장

FastAPI는 SessionMiddleware를 쓰면

request.session["loginid"] = loginid

이렇게 하면 브라우저 쿠키에 SessionID가 저장되고 React가 이걸 서버로 다시 보내줘야 로그인 유지된다.

그래서 React 모든 axios 요청에: withCredentials: true
이 코드를 써줘야한다.

3 백엔드 코드 설명 (FastAPI)

main.py 핵심 흐름

1회원가입

2.데이터 받음

3.같은 아이디 있는지 확인

4.DB에 넣고 commit

5.성공하면 { result: "ok" }

로그인
request.session["loginid"] = loginid
request.session["username"] = user.name

이 코드가 실행되면 로그인된 상태가 됨.

로그인 상태 확인

@app.get("/checkLogin")
def check_login(request):
    loginid = request.session.get("loginid")
    username = request.session.get("username")

여기서 세션이 있으면 가능, 없으면 실패...

글쓰기

loginid = request.session.get("loginid")
if not loginid:
    return {"result": "fail", "msg": "로그인이 필요합니다."}

로그인 안 했으면 글쓰기 불가

프론트엔드 코드 설명 (React)

로그인 상태 확인

useEffect(() => {
    axios.get("/checkLogin", {withCredentials: true})
        .then(res => {
            if (res.data.result === "ok") {
                setLogged(true);
                setUsername(res.data.username);
            }
        });
}, []);

페이지 최초 렌더링 시 서버에 로그인 여부 확인

로그인되어 있으면 Home 화면에 "글쓰기" 버튼이 뜸

안 되어 있으면 LOGIN/REGISTER 버튼만 뜸

이게 세션 인증 방식 React 구조의 핵심

const res = await axios.post(
    "/login",
    user,
    { withCredentials: true }
);

여기서 세션 쿠키 저장됨.
그래서 로그인 후 페이지 이동하면 Home에서 정상적으로 logged=true.

axios.post("/board",
    { title, content },
    { withCredentials: true }
)
profile
Start Change Up

0개의 댓글