
이전 게시물(https://leapcell.io/blog/build-a-perfect-blog-with-fastapi-full-text-search-for-posts)에서는 블로그에 전문 검색 기능을 통합하여 훌륭한 콘텐츠를 더 쉽게 찾을 수 있도록 했습니다.
이제 블로그의 기능이 더욱 풍부해지고 콘텐츠가 늘어남에 따라 자연스럽게 새로운 질문이 생깁니다. 어떤 게시물이 독자들 사이에서 가장 인기가 있을까요?
독자의 관심을 이해하면 더 높은 품질의 콘텐츠를 만드는 데 도움이 될 수 있습니다.
따라서 이번 튜토리얼에서는 블로그에 기본적이면서도 매우 중요한 기능인 방문자 추적을 추가합니다. 각 게시물이 읽힌 횟수를 기록하고 페이지에 조회수를 표시합니다.
Google Analytics와 같은 타사 서비스를 사용하는 것을 고려할 수 있습니다. 하지만 자체적으로 백엔드 기반 추적 시스템을 구축하면 데이터를 더 많이 자체적으로 관리하고 수집하려는 데이터를 사용자 정의할 수 있습니다.
시작해 보겠습니다:
각 보기가 발생한 시간, 해당 게시물, 향후 심층 분석을 위한 방문자 정보(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 인터페이스에 붙여넣고 실행하면 됩니다.

다음으로 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을 실행할 필요가 없습니다.
코드를 깔끔하게 유지하기 위해 페이지 보기 추적 기능에 대한 새 서비스 파일을 생성합니다.
페이지 보수와 관련된 모든 로직을 처리하기 위해 프로젝트 루트 디렉토리에 새 파일 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쿼리를 실행하는 것보다 훨씬 빠릅니다.
다음으로 방문자가 게시물을 볼 때마다 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, # 보기 수를 템플릿에 전달
},
)
이전 단계에서 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 블로그에 백엔드 보기 수 추적 시스템을 성공적으로 추가했습니다. 사용자 방문 데이터가 이제 여러분의 손에 달려 있습니다.
이 원시 데이터를 통해 더 심층적인 데이터 작업 및 분석을 수행할 수 있습니다. 예를 들면 다음과 같습니다:
User-Agent를 분석하여 검색 엔진 크롤러의 방문을 식별하고 필터링합니다.데이터는 여러분의 손에 있으므로 이러한 탐색은 여러분에게 맡깁니다.
Leapcell에서 블로그를 배포하는 경우, Leapcell은 이미 무료로 제공되는 웹 분석 기능을 자동으로 사용 설정했습니다.
Leapcell의 웹 분석에는 유용하고 강력한 방문자 분석 기능이 많이 포함되어 있습니다. 이를 사용하면 직접 개발하는 수고 없이 방문자 행동에 대한 기본 분석을 쉽게 수행할 수 있습니다.

X에서 저희를 팔로우하세요: @LeapcellKR
관련 글: