그동안 개발은 조금씩 조금씩 했는데, 하나의 feature
가 완성된 느낌이라 포스팅...
각설하고,
오늘로서 완성된 회원가입/로그인은 React TypeScript, FastAPI, MySQL을 통해 구현되었다.
단계는 다음과 같다.
MySQL
스키마, 테이블 세팅React
Front-end 구축FastAPI
백엔드 구축SignUp
, Login
API 설계, DB save, loadJWT token
을 통한 로그인 세션 유지일단, 매우 간단하게 진행했다.
yacht
스키마에 user_info
테이블을 구성했다.
column으로는 단순하게 id, email, password, friends
로 구성했다.
추후 변경 여부는 70%...?
CREATE TABLE yacht.user_info (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
friends JSON
);
현재는 간단하게 구현했고, 아직 반응형 UI 조차 잡지 않았다.
PWA 최적화를 위해 모바일 반응형은 필수라, 모바일 반응형 공부를 조금 더 하고 잡을 예정
폴더 구조는
src/pages/StartPage.tsx
에서
src/components/Login.tsx
와 /components/SignUp.tsx
를 불러오는 구조로 구축했다.
정말 당연하게도 input
이나, button
은 역시 Mui
일단 로그인/회원가입 form
을 통해 데이터 fetching이 요구되고,
이를 react-query
의 useMutation
과 axios
로 구현했다.
(역시 아는 맛이 편해...)
src/api/instance.ts
import axios from 'axios';
export const axiosInstance = axios.create({
baseURL: 'http://localhost:8000',
headers: {
'Content-Type': 'application/json',
},
});
...
export const signup = async (newUser: {
id: string;
email: string;
password: string;
}) => {
return axiosInstance.post('/signup', newUser);
};
export const checkId = async (id: string) => {
return axiosInstance.get(`/check-id/${id}`);
};
export const login = async (user: { id: string; password: string }) => {
return axiosInstance.post('/login', user);
};
src/components/Login.tsx
import React, { useState } from 'react';
...
export const Login: React.FC<LoginProps> = ({ setIsSignUp }) => {
...
const [formData, setFormData] = useState({
id: '',
password: '',
});
// 로그인 버튼 클릭 시, BE로 보낼 user 데이터
const setAuthState = useSetRecoilState(authState);
// 로그인 유지시킬 recoil value
const mutation = useMutation((user: { id: string; password: string }) =>
login(user));
// login() method를 통해 `/login`으로 user data post
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const { id, password } = formData;
mutation.mutate(
// post
{ id, password },
{
onSuccess: (data) => {
// post return success -> return value
const token = data.data.access_token;
// BE는 로그인을 성공한 유저에게 토큰을 부여한다.
localStorage.setItem('token', token);
// localStorage를 통해 유저의 토큰을 저장한다.
setAuthState({ isLogin: true });
// recoil의 로그인 값을 true로 변경
alert(`WELCOME ${id}`);
navigate('/');
// 로그인 성공 -> 메인화면으로 이동
},
onError: () => {
alert('LOGIN FAIL');
},
},
);
};
...
}
SignUp.tsx
도 이와 유사하게 구축되었다.
src/atoms/authAtom.ts
import { atom } from 'recoil';
const token = localStorage.getItem('token');
const isLogin = !!token;
export const authState = atom<{ isLogin: boolean }>({
key: 'authState',
default: { isLogin },
});
로컬스토리지에서 토큰을 가져와 로그인을 유지 여부를 결정한다.
먼저, FastAPI를 하도 오랜만에 해서 폴더 구축부터 코드 작성까지 대부분을 전지전능 GPT4o님과 함께했다.
main.py
CORS ERROR
핸들링이었다.from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import auth
app = FastAPI()
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 모든 출처 허용
allow_credentials=True,
allow_methods=["*"], # 모든 HTTP 메서드 허용
allow_headers=["*"], # 모든 헤더 허용
)
app.include_router(auth.router)
db.py
MySQL
을 연결하기위해 sqlalchemy
를 활용해 DB를 연결했다..env
파일에 저장해 .gitignore
해두었다.from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv
import os
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL");
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
models/user.py
from sqlalchemy import Column, String, JSON
from app.db import Base
class User(Base):
__tablename__ = 'user_info'
id = Column(String, primary_key=True)
email = Column(String, unique=True, index=True)
password = Column(String)
friends = Column(JSON, nullable=True)
routers/auth.py
router
를 따로 분리하지 않았다.dependencies/security.py
로 분리할 예정[ 주요 기능 ]
bcrypt
를 통한 해시암호화JWT
토큰 생성, 부여...
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# db 연결
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# 패스워드 암호화
def get_password_hash(password):
return pwd_context.hash(password)
# 패스워드 복호화
def verify(plain : str, hashed : str) -> bool:
return pwd_context.verify(plain, hashed)
# 로그인 성공한 유저에게 토큰 부여
def create_access_token(data : dict, expires_delta : timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=60)
to_encode.update({"exp" : expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# login 라우터
@router.post('/login')
def login(user : user_schemas.UserLogin, db: Session = Depends(get_db)):
is_user = db.query(user_models.User).filter(user_models.User.id == user.id).first()
# db에서 넘어온 user data에 해당하는 user를 찾는다.
if not is_user or not verify(user.password, is_user.password):
# 유저가 존재하지 않거나, id, password 쌍이 일치하지 않는다면
raise HTTPException(status_code=400, detail="INVALID ID or Password")
# 400 오류 제공
# 로그인 정보가 올바르다면
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub" : is_user.id}, expires_delta = access_token_expires
)
# 유저 id 정보가 담긴 로그인 토큰 부여, 현재는 60분 기준
return {"access_token" : access_token, "token_type" : "bearer"}
# signup 라우터
@router.post('/signup', response_model= user_schemas.UserOut)
def create_user(user : user_schemas.UserRegister, db : Session = Depends(get_db)):
db_user = db.query(user_models.User).filter(user_models.User.email == user.email).first()
# 겹치는 email 정보가 있는지 확인, 한 유저(email 당)는 하나의 계정만 생성 가능
if db_user:
return HTTPException(status_code=400, detail="Already Joined Email")
# 이미 해당 email로 만든 계정이 있다면 400 오류 제공
# 회원가입한 pw를 암호화
hashed_pw = get_password_hash(user.password)
db_user = user_models.User(id = user.id, email = user.email, password = hashed_pw)
# id, email, 암호화된 pw를 mysql db에 저장
db.add(db_user)
db.commit()
db.refresh(db_user)
# db 최신화
return db_user
# id 중복체크 라우터
@router.get('/check-id/{id}')
def check_id(id:str, db:Session = Depends(get_db)):
db_user = db.query(user_models.User).filter(user_models.User.id == id).first()
# 현재 입력중인 id가 중복인지 확인
# 중복이라면 중복이라고 리턴
return {"isDuplicate" : bool(db_user)}
이런식으로 유효성검사도 잘되고, 회원가입/로그인 모두 잘 동작한다.
alert
는 개인적으로 굉장히 기본 모양을 싫어하기 때문에
sweetAlert
를 통해 변경할 예정
오랜만에 FastAPI를 하니, 굉장히 미숙했다...
GPT가 없었으면 스타벅스에서 멍때리는 원숭이가 될 뻔 했다.
그리고 app/schemas
를 통해 유효성 검사를 진행하면서 프로젝트의 안정성이 높아진다는 것을 알 수 있었다.
그리고 개인적으로 useMutation
과 recoil
이 너무 편리한데,
이거 이전에 나 개발 어떻게했나 싶다.