FastAPI로 완벽한 블로그 만들기: 방문자 분석
Ethan Miller
Product Engineer · Leapcell

이전 게시물(https://leapcell.io/blog/build-a-perfect-blog-with-fastapi-full-text-search-for-posts)에서는 블로그에 전문 검색 기능을 통합하여 훌륭한 콘텐츠를 더 쉽게 찾을 수 있도록 했습니다.
이제 블로그의 기능이 더욱 풍부해지고 콘텐츠가 늘어남에 따라 자연스럽게 새로운 질문이 생깁니다. 어떤 게시물이 독자들 사이에서 가장 인기가 있을까요?
독자의 관심을 이해하면 더 높은 품질의 콘텐츠를 만드는 데 도움이 될 수 있습니다.
따라서 이번 튜토리얼에서는 블로그에 기본적이면서도 매우 중요한 기능인 방문자 추적을 추가합니다. 각 게시물이 읽힌 횟수를 기록하고 페이지에 조회수를 표시합니다.
Google Analytics와 같은 타사 서비스를 사용하는 것을 고려할 수 있습니다. 하지만 자체적으로 백엔드 기반 추적 시스템을 구축하면 데이터를 더 많이 자체적으로 관리하고 수집하려는 데이터를 사용자 정의할 수 있습니다.
시작해 보겠습니다:
단계 1: 페이지 보기 데이터 모델 생성
1. 데이터베이스 테이블 생성
각 보기가 발생한 시간, 해당 게시물, 향후 심층 분석을 위한 방문자 정보(IP 주소 및 사용자 에이전트 등)를 기록할 pageview
테이블을 생성하기 위해 PostgreSQL 데이터베이스에서 다음 SQL 문을 실행하세요.
CREATE TABLE "pageview" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "postId" UUID REFERENCES "post"("id") ON DELETE CASCADE, "ipAddress" VARCHAR(45), "userAgent" TEXT );
참고: ON DELETE CASCADE
는 게시물이 삭제될 때 관련 페이지 보기 레코드가 모두 자동으로 삭제되도록 합니다.
Leapcell에서 데이터베이스를 생성한 경우,
그래픽 인터페이스를 사용하여 SQL 문을 쉽게 실행할 수 있습니다. 웹사이트의 데이터베이스 관리 페이지로 이동하여 위 문장을 SQL 인터페이스에 붙여넣고 실행하면 됩니다.
2. PageView 엔티티 생성
다음으로 models.py
파일을 열고 PageView
모델을 추가하고 Post
모델을 업데이트하여 양방향 관계를 설정합니다.
# models.py import uuid from datetime import datetime from typing import Optional, List from sqlmodel import Field, SQLModel, Relationship # ... User 클래스 ... 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") # PageView와 일대다 관계 추가 page_views: List["PageView"] = Relationship(back_populates="post") # ... Comment 클래스 ... class PageView(SQLModel, table=True): id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True) createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False) ipAddress: Optional[str] = Field(max_length=45, default=None) userAgent: Optional[str] = Field(default=None) # Post 테이블에 연결되는 외래 키 정의 postId: uuid.UUID = Field(foreign_key="post.id") # 다대일 관계 정의 post: "Post" = Relationship(back_populates="page_views")
main.py
에서 create_db_and_tables
함수를 구성했으므로 SQLModel이 애플리케이션이 시작될 때 모델 변경 사항을 자동으로 감지하고 데이터베이스 테이블 구조를 업데이트하므로 수동으로 SQL을 실행할 필요가 없습니다.
단계 2: 추적 서비스 구현
코드를 깔끔하게 유지하기 위해 페이지 보기 추적 기능에 대한 새 서비스 파일을 생성합니다.
페이지 보수와 관련된 모든 로직을 처리하기 위해 프로젝트 루트 디렉토리에 새 파일 tracking_service.py
를 만듭니다.
# tracking_service.py import uuid from typing import List, Dict from sqlmodel import Session, select, func from models import PageView def record_view(post_id: uuid.UUID, ip_address: str, user_agent: str, session: Session): """새 페이지 보기를 기록합니다""" new_view = PageView( postId=post_id, ipAddress=ip_address, userAgent=user_agent, ) session.add(new_view) session.commit() def get_count_by_post_id(post_id: uuid.UUID, session: Session) -> int: """단일 게시물의 총 보기 수를 가져옵니다""" statement = select(func.count(PageView.id)).where(PageView.postId == post_id) # 단일 스칼라 값을 반환하는 쿼리에는 .one() 또는 .one_or_none()이 필요합니다 count = session.exec(statement).one_or_none() return count if count is not None else 0 def get_counts_by_post_ids(post_ids: List[uuid.UUID], session: Session) -> Dict[uuid.UUID, int]: """효율성을 위해 여러 게시물의 보기 수를 한 번에 가져옵니다""" if not post_ids: return {} statement = ( select(PageView.postId, func.count(PageView.id).label("count")) .where(PageView.postId.in_(post_ids)) .group_by(PageView.postId) ) results = session.exec(statement).all() # 결과를 {post_id: count} 형식의 사전으로 변환합니다 return {post_id: count for post_id, count in results}
get_counts_by_post_ids
메서드는 SQLModel(SQLAlchemy)의func.count
와group_by
를 사용하여 효율적인GROUP BY
쿼리를 실행합니다. 이는 특히 홈페이지에서 여러 게시물의 보기 수를 표시해야 할 때 각 게시물에 대해 별도의count
쿼리를 실행하는 것보다 훨씬 빠릅니다.
단계 3: 게시물 페이지에 보기 기록 통합
다음으로 방문자가 게시물을 볼 때마다 tracking_service
의 record_view
메서드를 호출해야 합니다. 여기에 가장 적합한 장소는 routers/posts.py
의 get_post_by_id
라우트입니다.
routers/posts.py
를 열고 새 서비스를 가져와 호출합니다.
# routers/posts.py # ... 다른 가져오기 import tracking_service # 추적 서비스 가져오기 # ... @router.get("/posts/{post_id}", response_class=HTMLResponse) def get_post_by_id( request: Request, post_id: uuid.UUID, session: Session = Depends(get_session), user: dict | None = Depends(get_user_from_session), ): post = session.get(Post, post_id) if not post: # 게시물을 찾을 수 없는 경우 처리 return HTMLResponse(status_code=404) comments = comments_service.get_comments_by_post_id(post_id, session) # 보기 기록 (발송 후 잊기) client_ip = request.client.host user_agent = request.headers.get("user-agent", "") tracking_service.record_view(post_id, client_ip, user_agent, session) # 보기 수 가져오기 view_count = tracking_service.get_count_by_post_id(post_id, session) # Markdown 콘텐츠 구문 분석 post.content = markdown2.markdown(post.content) return templates.TemplateResponse( "post.html", { "request": request, "post": post, "title": post.title, "user": user, "comments": comments, "view_count": view_count, # 보기 수를 템플릿에 전달 }, )
단계 4: 프론트엔드에 보기 수 표시
게시물 상세 페이지
이전 단계에서 view_count
를 이미 검색하여 post.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> </article>
블로그 홈페이지
홈페이지의 게시물 목록에도 보기 수를 표시하려면 get_all_posts
라우트에 몇 가지 조정을 해야 합니다.
routers/posts.py
업데이트:
# routers/posts.py # ... @router.get("/posts", response_class=HTMLResponse) def get_all_posts( request: Request, session: Session = Depends(get_session), user: dict | None = Depends(get_user_from_session) ): # 1. 모든 게시물 가져오기 statement = select(Post).order_by(Post.createdAt.desc()) posts = session.exec(statement).all() # 2. 모든 게시물의 ID 가져오기 post_ids = [post.id for post in posts] # 3. 보기 수 일괄 가져오기 view_counts = tracking_service.get_counts_by_post_ids(post_ids, session) # 4. 각 게시물 객체에 보기 수 연결 for post in posts: post.view_count = view_counts.get(post.id, 0) return templates.TemplateResponse( "index.html", {"request": request, "posts": posts, "title": "Home", "user": user } ) # ...
마지막으로 templates/index.html
템플릿을 업데이트하여 보기 수를 표시합니다.
<div class="post-list"> {% for post in posts %} <article class="post-item"> <h2><a href="/posts/{{ post.id }}">{{ post.title }}</a></h2> <p>{{ post.content[:150] }}...</p> <small>{{ post.createdAt.strftime('%Y-%m-%d') }} | Views: {{ post.view_count }}</small> </article> {% endfor %} </div>
실행 및 테스트
애플리케이션을 다시 시작합니다:
uvicorn main:app --reload
브라우저를 열고 블로그 홈페이지로 이동합니다.
블로그 목록에서 각 게시물 옆에 "Views: 0"이 표시됩니다.
아티클 상세 페이지로 들어가서 페이지를 몇 번 새로고침하면 해당 게시물의 조회수가 증가한 것을 알 수 있습니다.
결론
이제 FastAPI 블로그에 백엔드 보기 수 추적 시스템을 성공적으로 추가했습니다. 사용자 방문 데이터가 이제 여러분의 손에 달려 있습니다.
이 원시 데이터를 통해 더 심층적인 데이터 작업 및 분석을 수행할 수 있습니다. 예를 들면 다음과 같습니다:
- 중복 제거: 특정 시간 창(예: 하루) 내 동일 IP 주소에서의 여러 방문을 단일 보기로 계산합니다.
- 봇 필터링:
User-Agent
를 분석하여 검색 엔진 크롤러의 방문을 식별하고 필터링합니다. - 데이터 대시보드: 차트를 사용하여 게시물 보기 추세를 시각화하는 비공개 페이지를 만듭니다.
데이터는 여러분의 손에 있으므로 이러한 탐색은 여러분에게 맡깁니다.
Leapcell에서 블로그를 배포하는 경우, Leapcell은 이미 무료로 제공되는 웹 분석 기능을 자동으로 사용 설정했습니다.
Leapcell의 웹 분석에는 유용하고 강력한 방문자 분석 기능이 많이 포함되어 있습니다. 이를 사용하면 직접 개발하는 수고 없이 방문자 행동에 대한 기본 분석을 쉽게 수행할 수 있습니다.