
웹에서 인증하는 방식은 크게 세 가지가 있다.
세션(Session)
쿠키(Cookie)
JWT(Json Web Token)
세션(Session)은 서버가 사용자 정보를 메모리에 저장하고, 클라이언트는 세션 키만 쿠키로 들고 있는 구조다.
그래서 로그인 여부를 서버가 판단할 수 있다.
FastAPI에서 세션을 사용하려면 별도 미들웨어가 필요하다.
미들웨어 설치:
SessionMiddleware
애플리케이션에 추가:
app.add_middleware(SessionMiddleware, secret_key="임의의키")
세션 사용 흐름:
request.session["login_id"] = 사용자아이디
이 값이 들어 있으면 로그인된 상태,
없으면 로그인되지 않은 상태라고 판단한다.
로그아웃:
request.session.clear()
로그인이 들어왔을 때 서버는 다음 순서로 처리한다.
클라이언트가 입력한 id, password를 request로 받는다.
DB에서 해당 id/pass가 맞는지 검사한다.
일치하면
request.session["login_id"] = id
return "OK"
일치하지 않으면
return "FAIL"
결과는 React에서 받아 화면을 바꾼다.
홈 화면을 띄울 때 클라이언트는 서버에 한 번 확인 요청을 보낸다.
GET /check_login
서버는 세션에 값이 설정돼 있는지 검사한다.
세션이 존재하면:
return "OK"
세션이 없으면:
return "FAIL"
이 값에 따라 React에서 로그인이 되어 있는지, 로그아웃 버튼을 보여줄지 등을 결정한다.
React는 다음과 같은 흐름으로 화면 상태를 유지한다.
페이지 로딩 시 useEffect에서 /check_login 호출
서버가 OK → loggedIn = true
서버가 FAIL → loggedIn = false
상태에 따라 화면에 로그인/회원가입 또는 로그아웃 버튼 표시
로그인 버튼 클릭 시
fetch로 /login 호출하여 결과 확인
성공 → 홈으로 이동
실패 → 알림 표시
로그아웃 시
fetch(/logout) 후
상태값 loggedIn을 false 변경
인증 상태는 UI를 변화시키는 중요한 기준이 된다.
FastAPI + React 통신 주의점
React에서 fetch 요청을 보낼 때 세션 정보를 유지하려면 다음 옵션이 필요하다.
credentials: "include"
이 옵션이 없으면 세션 쿠키가 전달되지 않아 로그인 상태가 사라진다.
DB 트랜잭션은 MySQL 내부의 세션
FastAPI 세션은 HTTP 세션
두 기능은 서로 완전히 다른 영역이다.
EX):
Workbench에서 INSERT 후 commit을 안 하면 다른 프로그램에서는 보이지 않는다.
FastAPI에서 INSERT 후 commit을 하면 그때 비로소 DB에 반영된다.
강의에서 여러 학생들이 “DB에서 안 보인다”는 문제는 대부분 commit을 하지 않은 경우였다.
회원 가입은 다음 흐름으로 진행된다.
React에서 사용자 정보를 form 형태로 JSON 전송
FastAPI에서 데이터를 받아 DB INSERT
INSERT 성공 → "OK" 반환
실패 → "FAIL" 반환
React에서 성공 여부를 알람으로 보여주고 페이지 이동
DB 컬럼이 NOT NULL인데 값을 안 넣으면 INSERT가 실패할 수 있다.
그러면 컬럼 제약조건을 수정해야 한다.
EX):
ALTER TABLE users MODIFY name VARCHAR(50) NULL;
로그인한 사람의 정보를 작성자(author)로 자동 채워 넣을 수 있다.
예를 들어 게시판 글쓰기에서:
서버는 request.session["login_id"] 를 가져와
author = login_id
이 값을 DB에 저장한다.
React에서는 작성자 input은 readonly로 만들고
자동으로 로그인 아이디를 보여주는 식으로 구현한다.
트랜잭션은 작업 단위를 한 덩어리로 묶는 개념이다.
작업이 모두 성공해야 commit
중간에 하나라도 실패하면 rollback
EX):
주문을 저장하고
매출 기록을 저장하고
마일리지를 적립하는 3단계가 있을 때
2단계에서 실패했다면
1단계도 롤백해야 데이터 오류가 없다.
FastAPI에서 트랜잭션을 묶는 방식은:
try:
insert order
insert sales
db.commit()
return "OK"
except:
db.rollback()
return "FAIL"
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>
);
}

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

회원가입

로그인 하면 뜨는 화면

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

글쓰기화면

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

글 들어가면 보이는 모습

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

다른 아이디로도 로그인해도 같은결과가 나온다.
FastAPI는 SessionMiddleware를 쓰면
request.session["loginid"] = loginid
이렇게 하면 브라우저 쿠키에 SessionID가 저장되고 React가 이걸 서버로 다시 보내줘야 로그인 유지된다.
그래서 React 모든 axios 요청에: withCredentials: true
이 코드를 써줘야한다.
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": "로그인이 필요합니다."}
로그인 안 했으면 글쓰기 불가
로그인 상태 확인
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 }
)