나중에 풀스택으로 성장하고 싶다고 말했지만, 이렇게 빨리 백엔드를 배우게 될 줄은 몰랐다.😂
아무튼 지난 시간까지 DB에 존재하는 데이터들로 API 만들어서 프론트엔드에서 사용하는걸 했으니까, 이번에는 한 단계 더 어려운 작업을 해봤다. 실제로 서버와 통신하면서 글 생성, 수정, 삭제 등 CRUD 작업이 되는 게시판 페이지 만들기.😎
이번 글에서는 일단 백엔드 쪽에서 어떤 작업들을 했는지를 기록해보려 한다.✍

데이터베이스 생성
일단 게시판 메인 페이지에서 글 목록이 조회될 수 있도록 DB에 간단하게 게시글을 몇 개 넣어두었다. 그리고 프로배구팀 데이터로 실험할 때부터 같은 가상환경(fastapi/app)에서 작업하고 있어서, 이번 게시판 프로젝트 관련 파일들을 따로 저장하려고 app 하위에 board 디렉토리를 만들었다. 파일명도 앞에 'board_'를 붙여서 통일시켰다.
Fast API와 DB 연동
board_db.py 파일에 Fast API와 MariaDB를 연동시키는 코드를 작성했다.
이건 지난번 프로배구팀 데이터로 연습할 때랑 똑같이 쓰면 되는거라 어렵지 않았다.
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from fastapi import Depends
# DB 접속 정보 설정
DATABASE_URL = "mysql+pymysql://username:password@ip_address:3306/DB_name"
# SQLAlchemy 엔진과 세션 생성
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 의존성 주입 콜백함수
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
그런데 사실 이후에 언급될 model.py와 api.py도 대부분의 코드가 지난번이랑 같았다.
코드들에 대한 자세한 설명은 아래 링크 확인!🙂
https://hjinn0813.tistory.com/145
FastAPI와 MariaDB 연동하기
나는 분명 햇병아리 FE 개발자인데, 이렇게 점점 풀스택이 되어가나보다..😂
hjinn0813.tistory.com
모델 설정하기
board_model.py에 데이터베이스의 기본적인 모델 클래스, 글 생성 모델, 업데이트 모델을 만들었다.
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel
Base = declarative_base()
# 기본 클래스 생성
class Board(Base):
__tablename__ = "board"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
date = Column(DateTime, nullable=False)
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
# 글 생성 모델
class BoardCreate(BaseModel):
name: str
title: str
content: str
# 글 업데이트 모델
class BoardUpdate(BaseModel):
name: str | None = None
title: str | None = None
content: str | None = None
- import 시의 "from pydantic import BaseModel"은 Pydantic의 기본 클래스인 BaseModel을 상속받아 데이터 모델을 만들겠다는 의미이다. BaseModel을 통해 데이터가 올바른 형식인지 유효성 검사를 자동으로 할 수 있고, 코드 작성도 간편해진다. 여기서는 글 생성 모델, 업데이트 모델을 만들기 위해 불러왔다.
→ 글 생성 모델, 업데이트 모델을 따로 만들면 각자가 필요한 속성만 포함하기 때문에 유지보수가 쉬워진다. 또한 해당 작업이 실제로 이뤄질 때 각자가 필요한 것만 가져와서 작동하니까 훨씬 효율적으로 움직이는 코드가 된다. - 기본 클래스에는 존재하는 id와 시간이 글 생성 모델과 업데이트 모델에 없는 이유는, 새로운 게시글을 작성하면 서버에서 자동으로 처리되기 때문에 클라이언트가 굳이 입력할 필요가 없기 때문이다.
- 업데이트 모델에서 name: str | None
→ 이름이 문자열일 수도 있고 none일 수도 있다. 이름은 문자열 값을 가질 수 있지만, 유저가 수정하지 않으면 None이 될 수 있다. Python 3.10부터 도입된 'Union' 타입 힌팅의 간결한 표현. - '= None' → 이름의 기본값이 None이다. 단어 때문에 헷갈릴 수 있는데, None은 값이 없다는 것이 아니라 "사용자가 수정하고 싶지 않다"는 의미다.
엔드포인트 작성 (GET 요청)
board_api.py에 일단 GET 요청을 하는 코드부터 작성했다.
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from fastapi.middleware.cors import CORSMiddleware
from .board_model import Board, BoardCreate, BoardUpdate # 모델 클래스 가져오기
from .board_db import get_db # 의존성 주입
from .crud import create_post, update_post, delete_post # CRUD 함수 가져오기
from datetime import datetime # 날짜 포맷 변경
# 라우터 생성
router = APIRouter()
- import 하는 코드는 대부분 지난번이랑 비슷하니까 여기서만 달라진 부분을 짚어보자면,
board_model.py에서 기본 모델 클래스와 함께 글 생성 모델, 업데이트 모델도 가져왔다. - crud.py 파일을 따로 만들었기 때문에 거기서 정의한 함수들도 가져왔다.
- datetime은 시간, 날짜와 관련된 다양한 기능을 가진 Python 클래스이다. 덕분에 내가 만든 임의 데이터의 순서를 날짜 기준 내림차순으로 바꾸었고, 새로운 글을 작성하면 자동으로 현재 시간이 입력된다.
- 라우터 생성은 여기가 앞서 말했듯이 app 디렉토리에 있는 루트 파일이 아니라 app.board 디렉토리라서, 라우터 설정을 해줘야 정상적으로 동작한다.
@router.get("/board")
def post_lists(db: Session = Depends(get_db)):
posts = db.query(Board).order_by(Board.date.desc()).all() #날짜 기준 내림차순 정렬 (최신글 상위)
for post in posts:
post.date = post.date.strftime("%Y-%m-%d %H:%M:%S") # 날짜 포맷 변경
return posts
@router.get("/board/{post_id}")
def post_detail(post_id: int, db: Session = Depends(get_db)):
posts = db.query(Board).filter(Board.id == post_id).first()
if posts is None:
return {"error": "post not found"}
return posts
- 라우터를 생성했으니까 GET 요청을 보내는 엔드포인트도 router.get()으로 작성한다.
- 전체 글 목록을 가져오는 엔드포인트는 order_by()로 순서 정렬을 시켰고, 날짜가 "2024-11-02T16:34:13" 이런 식으로 중간에 알파벳이 보이길래 그걸 수정하려고 포맷을 변경시켰다. (날짜를 가져와서 문자열로 변경시켰다.)
- 게시판의 디테일 페이지로 이동하는 엔드포인트는 지난번 프로배구팀 데이터로 연습할 때랑 같다.
엔드포인트 작성 (CRUD)
api.py의 끝에는 crud.py 파일에서 정의해놓은 엔드포인트를 작성했다.
CRUD 작업 로직을 crud.py에 작성하고 엔드포인트는 api.py 파일에 작성하는게 일반적인 원칙이라고 한다.
이렇게 작성하면 가독성이 좋아서 유지보수도 편리하다.
# 글 생성
@router.post("/board")
def create_post_endpoint(board: BoardCreate, db: Session = Depends(get_db)):
return create_post(db, board)
# 글 수정
@router.put("/board/{post_id}")
def update_post_endpoint(post_id: int, board: BoardUpdate, db: Session = Depends(get_db)):
return update_post(db, post_id, board)
# 글 삭제
@router.delete("/board/{post_id}")
def delete_post_endpoint(post_id: int, db: Session = Depends(get_db)):
return delete_post(db, post_id)
- POST 요청(생성): 데이터를 새로 생성하는 엔드포인트.
- 클라이언트가 보내준 데이터(board: BoardCreate)를 받아와서 서버에 반영시켜야하기 때문에 board 변수를 사용한다. 그런데 변수 이름이 같으면 에러가 발생할 수 있으므로 이름을 변경시켰다.
- board_db.py 파일에 만들어놓은 의존성 함수 get_db()에서 데이터를 가져와서, 기존에 있는 데이터랑 새로 생성된 글을 함께 전체 목록으로 보여준다.
- PUT 요청(수정): 데이터를 수정하는 엔드포인트.
- 해당 요청은 유저가 글을 수정하고 '확인' 버튼을 클릭한 순간, 클라이언트가 수정된 데이터를 서버로 보내고, 서버에서는 PUT 요청을 받아서 내용을 업데이트하고 변경된 내용을 저장하는 순서로 작동한다. 이것 역시 클라이언트가 보내준 데이터(board: BoardUpdate)를 받아와서 서버에 반영시켜야 하니까 board 변수를 사용했는데, 변수 이름이 같으면 에러가 발생할 수 있어서 이름을 변경했다.
- 여기서는 글의 ID값을 정수형으로 불러와서 어떤 글이 수정됐는지 확인한다.
- 마찬가지로 board_db.py 파일에 있는 의존성 함수 get_db()에서 데이터를 가져와서, 수정된 내용을 보여준다.
- DELETE 요청(삭제): 데이터를 삭제하는 엔드포인트.
- 글의 ID값을 정수형으로 불러와서 어떤 글을 삭제하는지 확인한다.
- 마찬가지로 board_db.py 파일에 만들어놓은 의존성 함수 get_db()에서 데이터를 가져와서, 기존에 있는 데이터에서 post_id를 통해 삭제된 글을 제외하고 전체 목록을 보여준다.
CRUD 로직(동작) 정의
마지막으로 crud.py 파일에 실제로 CRUD 동작이 어떻게 구현될지 정의했다.
from sqlalchemy.orm import Session
from .board_model import Board, BoardCreate, BoardUpdate
from .board_db import get_db
from datetime import datetime
from fastapi import HTTPException
역시 여기서도 import 하면서 시작한다.
- 세션 생성하여 DB와 상호작용하기 위해서 불러왔다.
- board_model.py에서 기본 모델 클래스, 글 생성 모델, 업데이트 모델 불러오기
- board_db.py에 정의해놓은 의존성 함수 get_db() 불러오기
- 새로운 글 생성시에 현재 시간 입력하려고 가져왔다.
- HTTP 오류 응답을 반환해주는 Fast API의 예외 클래스. 데이터가 없거나 잘못된 요청이 들어온 경우에 클라이언트에게 적절한 오류 메시지와 상태코드를 반환해준다.
# POST: 게시글 생성
def create_post(db: Session, board: BoardCreate):
new_board = Board(**board.dict(), date=datetime.now())
db.add(new_board)
db.commit()
db.refresh(new_board)
return new_board
- db는 데이터베이스와 연결된 Session 객체, board는 클라이언트에서 보낸 데이터로 BoardCreate 모델 사용.
- 클라이언트에서 보낸 board 객체를 dict() 메서드를 통해 딕셔너리 형태로 변환하고,
unpack 연산자(**)를 통해 딕셔너리 키-값 쌍을 Board 클래스의 인자로 new_board 변수에 할당한다. - new_board 변수를 add() 메서드를 통해 데이터베이스에 추가한다.
이때 2번에서 new_board에 정의한 내용이 데이터베이스에 반영된다. - commit() 메서드로 데이터베이스에 변경사항을 확정한다. 추가한 데이터를 실제로 DB에 저장하는 역할.
- refresh() 메서드로 방금 추가한 데이터의 최신 상태를 new_board 변수에 다시 불러온다.
데이터베이스에서 새로 생성된 정보를 가져와서 new_board 변수에 반영하는 역할. - 생성된 데이터를 반환한다. 클라이언트는 여기에서 새로 생성된 게시글의 정보를 알 수 있다.
💡 언팩(unpack) 연산자
Python에서 ** 또는 * 기호를 사용하여 데이터 구조의 내용을 꺼내서 함수나 메서드에 전달하는 방식.
별표 2개(**)는 딕셔너리의 키-값 쌍을 함수의 인자로 전달할 때 사용하고,
별표 1개(*)는 리스트나 튜플의 요소를 개별 인자로 전달할 때 사용한다.
예를 들어,
person = {"name": "Alice", "age": 30} 라는 딕셔너리가 있을 때,
print_info(**person) 이라고 한다면,
print_info(name="Alice", age=30) 라고 쓰는 것과 같다.
# PUT: 게시글 수정
def update_post(db: Session, post_id: int, board: BoardUpdate):
exist_post = db.query(Board).filter(Board.id == post_id).first()
if exist_post is None:
raise HTTPException(status_code=404, detail="Post not found")
for key, value in board.dict(exclude_unset=True).items():
setattr(exist_post, key, value)
db.commit()
db.refresh(exist_post)
return exist_post
- db는 데이터베이스와 연결된 세션 객체, post_id는 수정할 게시글의 ID가 정수형이라는 타입 힌트, 클라이언트에서 보낸 board는 수정할 내용을 담고 있는 BoardUpdate 모델.
- 데이터베이스에서 Board 모델 클래스를 쿼리하여 ID가 일치하는 게시글을 찾아서 exist_post라는 변수에 저장한다. 게시글이 존재하면 그 내용을, 없으면 None을 반환한다.
- 만약 exist_post가 None이면, 404 상태 코드와 함께 "Post not found"라는 메시지를 포함하는 HTTP 예외를 발생시킨다.
- board 객체를 딕셔너리 형태로 변환하여, 수정된 필드만 포함된 키-값 쌍을 튜플 형태로 가져온다.
exclude_unset=True는 Pydantic의 BaseModel에서 기본적으로 제공되는 기능으로, 수정하지 않은 필드는 결과에서 제외해서 변경된 값만 선택적으로 반영할 수 있게 해준다.
items()는 딕셔너리의 모든 키-값 쌍을 튜플 형태로 반환해주는 메서드.
setattr 함수를 사용하여 exist_post 객체의 수정된 부분만 실제 게시글에 반영시킨다. - commit() 메서드로 데이터베이스에 변경사항을 확정한다. 이 부분을 통해 수정된 내용이 실제로 데이터베이스에 저장된다.
- refresh() 메서드로 데이터베이스에서 exist_post 변수의 최신 상태를 다시 불러온다.
데이터베이스에서 수정된 정보를 가져와서 변수에 반영시키는 역할. - 최종적으로 수정된 게시글(exist_post)을 반환하여 클라이언트에게 결과를 전달한다.
# DELETE: 게시글 삭제
def delete_post(db: Session, post_id: int):
post = db.query(Board).filter(Board.id == post_id).first()
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
db.delete(post)
db.commit()
return {"detail": "글 삭제 성공!"}
- db는 데이터베이스와 연결된 세션 객체, post_id는 삭제할 게시글의 ID가 정수형이라는 타입 힌트.
- 데이터베이스에서 Board 모델 클래스를 쿼리하여 ID가 일치하는 게시글을 찾아서 post 변수에 저장.
- 만약 post가 None일 경우, 404 상태 코드와 함께 "Post not found"라는 메시지를 포함하는 HTTP 예외를 발생시킨다.
- delete() 메서드를 사용하여 post 변수에 담아놓은 게시글을 데이터베이스에서 삭제한다.
이 메서드는 삭제할 객체를 인자로 받아 해당 객체를 데이터베이스에서 제거한다. - commit() 메서드를 통해 데이터베이스에 변경사항을 확정한다.
이를 통해 게시글이 삭제된 상태가 실제로 데이터베이스에 반영된다. - 최종적으로 "글 삭제 성공!"이라는 메시지를 클라이언트에게 반환하여, 요청이 성공적으로 처리되었음을 알린다.
여기까지가 게시판 페이지와 관련한 백엔드 작업이었다.
처음 CRUD 작업을 해보라고 사수님께 제안을 받았을 때는 왠지 막막하다는 생각이 들었는데, 직접 해보니까 그렇게 많이 어려운 작업은 아니었다. 그래도 역시 백엔드는 쉽지 않음..😅
여기서 설명한 코드들과 연동되어있는 프론트엔드 코드도 작동원리를 살펴봐야하는데, 그건 다음 포스팅에서 다뤄볼 예정!
내가 지금 풀스택으로 코딩을 하고 있다니, 1년전에는 상상도 못한 일이었다..😂
아무튼 앞으로도 열심히 성장해보자!👍
https://hjinn0813.tistory.com/150
DB에 CRUD가 되는 게시판 만들기 - Frontend
지난 시간에 이어 이번에는 실제로 서버와 통신하며 게시글의 생성, 수정, 삭제가 되는 게시판 만들기를 위해 프론트엔드 쪽에서는 어떤 작업을 했는지 기록해보려고 한다.✍본론으로 들어가기
hjinn0813.tistory.com
'💾 Backend > FastAPI' 카테고리의 다른 글
Fast API에서 비밀번호 검증하기 (0) | 2024.11.11 |
---|---|
Fast API에서 비밀번호 해싱하기 (0) | 2024.11.08 |
FastAPI와 MariaDB 연동하기 (0) | 2024.10.30 |
Fast API 가상환경 구축하고 API 만들기 (2) | 2024.10.23 |
Fast API 기본 개념 정리 (1) | 2024.10.11 |