FastAPI로 완벽한 블로그 만들기: 게시물 태그 추가하기
Ethan Miller
Product Engineer · Leapcell

이전 글에서 블로그에 방문자 추적 기능을 추가하여 각 게시물의 인기도를 시각적으로 확인할 수 있게 되었습니다.
블로그가 꽤 완성된 것처럼 보이지만, 뭔가 빠진 것 같습니다. 블로그에 이미 많은 게시물이 있는데, 사용자들이 길을 잃을 수도 있습니다... 그렇다면 사용자는 자신이 관심 있는 주제를 어떻게 빠르게 찾을 수 있을까요?
맞습니다. 이제 블로그에는 태그 기능이 필요합니다.
태그는 콘텐츠를 구성하고 분류하는 고전적인 방법입니다. 각 게시물에 키워드(예: "FastAPI", "Python", "Database")를 할당함으로써, 독자는 특정 주제와 관련된 모든 게시물을 쉽게 찾을 수 있습니다.
다음 두 개의 튜토리얼에서는 블로그 시스템에 완전한 태그 기능을 추가할 것입니다. 이번 튜토리얼에서는 먼저 기본 부분을 구현합니다. 즉, 게시물 생성 시 태그 설정 지원 및 게시물 페이지에 태그 표시입니다.
1단계: 태그 데이터 모델 생성
태그 기능을 구현하려면 새로운 Tag
모델이 필요하며 Post
와 Tag
간의 관계를 설정해야 합니다. 하나의 게시물은 여러 개의 태그를 가질 수 있으며, 하나의 태그는 여러 게시물과 연결될 수 있습니다. 이것은 전형적인 다대다(Many-to-Many) 관계입니다.
SQLModel (및 대부분의 ORM)에서 다대다 관계를 구현하려면 Post
와 Tag
간의 페어링 관계를 저장하는 추가적인 "연결 테이블(link table)"이 필요합니다.
1. 모델 파일 업데이트
models.py
파일을 열어 Tag
및 PostTagLink
모델을 추가하고 Post
모델을 업데이트합니다.
# models.py import uuid from datetime import datetime from typing import Optional, List from sqlmodel import Field, SQLModel, Relationship # ... User, PageView, Comment 클래스 ... class PostTagLink(SQLModel, table=True): post_id: Optional[uuid.UUID] = Field( default=None, foreign_key="post.id", primary_key=True ) tag_id: Optional[uuid.UUID] = Field( default=None, foreign_key="tag.id", primary_key=True ) class Tag(SQLModel, table=True): id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True) name: str = Field(unique=True, index=True) posts: List["Post"] = Relationship(back_populates="tags", link_model=PostTagLink) class Post(SQLModel, table=True): id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True) title: str content: str createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False) comments: List["Comment"] = Relationship(back_populates="post") page_views: List["PageView"] = Relationship(back_populates="post") # Tag와의 다대다 관계 추가 tags: List["Tag"] = Relationship(back_populates="posts", link_model=PostTagLink)
코드 설명:
- 고유한
name
필드를 가진Tag
모델을 생성했습니다. post_id
와tag_id
두 개의 필드만 있는PostTagLink
모델을 생성했습니다. 이 두 필드는 각각post
및tag
테이블을 가리키는 외래 키 역할을 하며, 함께 기본 키를 형성합니다.Post
및Tag
모델 모두에서Relationship
필드(tags
및posts
)를 추가했습니다. 핵심은link_model=PostTagLink
매개변수로, SQLModel에게 이 두 모델 간의 관계가PostTagLink
중간 테이블을 통해 유지된다는 것을 알려줍니다.
2. 데이터베이스 테이블 생성
새로운 모델 두 개를 추가했으므로 해당 테이블을 데이터베이스에 생성해야 합니다. PostgreSQL 데이터베이스에서 다음 SQL 문을 실행합니다.
-- tag 테이블 생성 CREATE TABLE "tag" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "name" VARCHAR UNIQUE NOT NULL ); -- link 테이블 post_tag_link 생성 CREATE TABLE "posttaglink" ( "post_id" UUID REFERENCES "post"("id") ON DELETE CASCADE, "tag_id" UUID REFERENCES "tag"("id") ON DELETE CASCADE, PRIMARY KEY ("post_id", "tag_id") );
참고: SQLModel은 PostTagLink
를 posttaglink
테이블로 자동 매핑합니다. 테이블 이름이 올바른지 확인하세요.
Leapcell에서 데이터베이스를 생성한 경우,
해당 웹사이트의 데이터베이스 관리 페이지로 이동하여 위의 문장들을 SQL 인터페이스에 붙여넣고 실행하기만 하면 그래픽 인터페이스를 통해 SQL 문을 쉽게 실행할 수 있습니다.
2단계: 태그 비즈니스 로직 구현
코드를 깔끔하게 유지하기 위해 태그 기능을 위한 새로운 서비스 파일을 생성할 것입니다.
프로젝트 루트 디렉토리에 tags_service.py
라는 새 파일을 생성합니다.
# tags_service.py import uuid from typing import List from sqlmodel import Session, select from models import Tag def find_or_create_tags(tag_names: List[str], session: Session) -> List[Tag]: """ 태그 이름을 기반으로 태그 엔티티를 찾거나 생성합니다. """ tags = [] if not tag_names: return tags # 기존 태그 조회 statement = select(Tag).where(Tag.name.in_(tag_names)) existing_tags = session.exec(statement).all() tags.extend(existing_tags) existing_tag_names = {tag.name for tag in existing_tags} # 생성해야 할 태그 찾기 new_tag_names = [name for name in tag_names if name not in existing_tag_names] # 새 태그 생성 for name in new_tag_names: new_tag = Tag(name=name) session.add(new_tag) tags.append(new_tag) # 효율성을 위해 한 번 커밋 session.commit() # 새로 생성된 태그의 ID를 가져오기 위해 새로고침 for tag in tags: if tag.id is None: session.refresh(tag) return tags
이 find_or_create_tags
함수는 태그 이름 목록을 받아 데이터베이스를 쿼리하고, 기존 태그의 엔티티를 반환하며, 존재하지 않는 태그의 경우 새 레코드를 생성합니다.
3단계: 게시물 라우트에 태그 로직 통합
이제 게시물 생성 라우트를 수정하여 태그 데이터를 수락하고 처리해야 합니다.
routers/posts.py
를 열고 새 서비스를 가져온 다음 create_post
라우트를 업데이트합니다.
# routers/posts.py # ... 다른 import 문 ... import tags_service # 태그 서비스 가져오기 # ... @router.post("/posts", response_class=HTMLResponse) def create_post( title: str = Form(...), content: str = Form(...), tags: str = Form(""), # 새 tags 폼 필드 추가 session: Session = Depends(get_session), user: dict = Depends(login_required) ): # 1. Post 객체 생성 new_post = Post(title=title, content=content) # 2. 태그 처리 if tags: # 쉼표로 구분된 문자열을 태그 이름 목록으로 파싱 tag_names = [name.strip() for name in tags.split(',') if name.strip()] # 태그 엔티티 찾기 또는 생성 tag_objects = tags_service.find_or_create_tags(tag_names, session) # 게시물에 태그 연결 new_post.tags = tag_objects # 3. 게시물 저장 session.add(new_post) session.commit() return RedirectResponse(url="/posts", status_code=302) # ...
create_post
함수에 tags
매개변수를 추가했으며, 이는 폼에서 쉼표로 구분된 태그 문자열을 받게 됩니다. 그런 다음 이 문자열을 파싱하고, tags_service
를 호출하여 Tag
객체 목록을 가져온 다음, new_post.tags
에 할당합니다. SQLModel의 Relationship
덕분에 new_post
를 커밋하면 SQLModel이 posttaglink
연결 테이블의 레코드를 자동으로 처리합니다.
4단계: 프론트엔드 표시
마지막 단계는 템플릿 파일을 수정하여 사용자가 태그를 입력하고 게시물 페이지에서 태그를 볼 수 있도록 하는 것입니다.
새 게시물 페이지
templates/new-post.html
을 열고 콘텐츠 텍스트 영역 아래에 태그 입력 필드를 추가합니다.
{% include "_header.html" %} <form action="/posts" method="POST" class="post-form"> <div class="form-group"> <label for="content">Content</label> <textarea id="content" name="content" rows="10" required></textarea> </div> <div class="form-group"> <label for="tags">Tags (comma-separated)</label> <input type="text" id="tags" name="tags" placeholder="e.g. fastapi, python, tutorial" /> </div> <button type="submit">Submit</button> </form> {% include "_footer.html" %}
게시물 상세 페이지
templates/post.html
을 열고 게시물 콘텐츠 아래에 태그 목록을 표시할 섹션을 추가합니다.
<article class="post-detail"> <h1>{{ post.title }}</h1> <small>{{ post.createdAt.strftime('%Y-%m-%d') }} | Views: {{ view_count }}</small> <div class="post-content">{{ post.content | safe }}</div> {% if post.tags %} <div class="tags-section"> <strong>Tags:</strong> {% for tag in post.tags %} <a href="/tags/{{ tag.id }}" class="tag-item">{{ tag.name }}</a> {% endfor %} </div> {% endif %} </article>
게시물에 태그가 있는지 확인하기 위해 {% if post.tags %}
를 사용합니다. 태그가 있다면 post.tags
목록을 반복하고 각 태그를 링크로 렌더링합니다. 이 링크는 아직 클릭할 수 없습니다. 다음 글에서 구현할 것입니다.
실행 및 테스트
애플리케이션을 다시 시작합니다.
uvicorn main:app --reload
로그인 후 "새 게시물" 페이지로 이동합니다. 새 태그 입력 필드가 보일 것입니다.
Python, Tutorial
과 같이 쉼표로 구분된 태그를 입력한 다음 제출합니다.
제출 후 게시물 상세 페이지로 이동하면 게시물의 태그가 성공적으로 표시되는 것을 볼 수 있습니다.
이제 블로그에서 태그 생성 및 표시를 지원합니다. 하지만 사용자는 여전히 태그별로 게시물을 필터링할 수 없습니다. 다음 튜토리얼에서 이 기능을 구현할 것입니다.