Django 댓글 시스템 만들기

Kangjik Kim·2025년 1월 23일
0

blog 애플리케이션을 사용자들이 게시글에 댓글을 달 수 있도록 확장해보자.
댓글 시스템을 만들기 위해선 다음과 같은 요소들이 필요하다.

  • 게시글의 사용자 댓글을 저장하는 모델
  • 댓글을 작성해서 제출하고 데이터를 검증할 수 있는 폼
  • 폼을 처리하고 DB에 새로운 댓글을 저장하는 뷰
  • 게시글 상세 템플릿에 포함할 수 있는 댓글 목록과 새로운 댓글 추가를 위한 템플릿

댓글 모델 만들기

게시글의 사용자 댓글을 저장하는 모델을 만들어보자.

models.py에 아래의 모델을 추가했다.

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    name = models.CharField(max_length=80)
    email = models.EmailField()
    body = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    active = models.BooleanField(default=True)

    class Meta:
        ordering = ['created']
        indexes = [
            models.Index(fields=['created']),
        ]

    def __str__(self):
        return f"{self.post} 게시글의 {self.name}의 댓글"

각 댓글을 단일 게시글과 연결하기 위해 Foreign Key 필드를 추가했다.
댓글은 하나의 게시글에 작성되고, 각 게시글은 여러 개의 댓글이 있을 수 있기 때문에 이러한 다대일 관계는 Comment 모델에서 정의된다.

related_name 속성을 사용해 관련된 객체에서 다시 이 객체로의 관계에 사용되는 속성의 이름을 지정할 수 있다. comment.post 를 사용해서 댓글 객체의 해당 게시글을 조회하고, post.comments.all() 을 사용해 게시글과 관련된 모든 댓글을 조회할 수 있다.

related_name 속성을 정의하지 않으면 쟝고는 소문자로 모델의 이름에 _set 을 붙여서 (comment_set) 관련 객체와 모댈 객체 관계의 이름으로 사용한다.

댓글 상태를 제어하기 위해 부울 필드 active 를 정의했다. 이 필드를 사용하면 관리 사이트에서 부적절한 댓글을 수동으로 비활성화 할 수 있다. 기본 값은 default=True 로 모든 댓글이 기본적으로 활성화됨을 나타낸다.

댓글이 생성된 날짜와 시간을 저장하기 위해 create 필드를 정의했는데, auto_now_add를 사용하면 객체를 생성할 때 날짜가 자동으로 저장된다. 모델의 Meta 클래스에 ordering=['created'] 를 추가해 댓글을 기본적으로 시간순으로 정렬했다. 그리고 created 필드에 대한 인덱스를 오름차순으로 추가했다.
이렇게 하면 created 필드를 통해 DB 조회와 조회된 결과를 정렬하는 성능이 향상된다.

관리 사이트에 댓글 추가

다음으로 관리 사이트에 새로운 모델을 추가해보자.

blog 앱의 admin.py파일을 열고 Comment 모델을 가져와 ModelAdmin 클래스를 추가하자.

from .models import Post, Comment

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ['name', 'email', 'post', 'created', 'active']
    list_filter = ['active', 'created', 'updated']
    search_fields = ['name', 'email', 'body']

이제 관리사이트에 접속하면 아래와 같이 Comments 모델이 표시되고, 추가를 클릭할 경우 새로운 댓글을 추가하기 위한 폼이 표시된다.

이제 관리 사이트를 사용해서 Comment 인스턴스들을 관리할 수 있다.

모델에서 폼 만들기

사용자가 블로그 게시글에 댓글을 달 수 있도록 폼을 작서앻야 한다.
쟝고에는 폼을 만드는데 사용할 수 있는 두 가지 기본 클래스 FormModelForm이 있다.
사용자가 이메일로 게시글을 공유할 수 있게 만들 때는 Form 클래스를 사용했었다.
이번에는 기존 Comment 모델을 활용하고 동적으로 폼을 만들기 위해 ModelForm 을 사용할 것이다.

forms.py파일을 편집해 아래의 코드를 추가한다.

from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['name', 'email', 'body']

모델에서 폼을 만들려면 Meta 클래스에서 폼을 빌드할 모델을 지정하기만 하면 된다.
ModelForm 을 사용하면 쟝고가 모델을 검사하고 해당 폼을 동적으로 만든다.
모델의 각 필드 유형에는 해당하는 기본 폼 필드 유형이 존재한다. 폼 유효성 검사를 위해 모델 필드의 속성을 고려할 필요가 있다. 기본적으로 쟝고는 모델에 포함된 각 필드에 대한 폼 필드를 만드는데 fields 속성을 사용해 폼에 포함할 필드를 쟝고에 명시적으로 알리거나 exclude 속성을 사용해 제외할 필드를 정의할 수 있다.

CommentForm 에서는 name, email, body 필드를 명시적으로 포함하고 있다.

뷰에서 ModelForms 처리하기

이메일로 게시글을 공유할 때 HTTP 메서드로 구분해 동일한 뷰를 사용해 폼을 표시하고, 제출된 데이터를 처리했었다. 댓글의 경우 게시글 상세 페이젱 댓글 폼을 추가하고 폼 입력 값들을 처리하기 위한 별도의 뷰를 만들어야 한다. 폼을 처리하는 새로운 뷰는 댓글이 DB에 저장된 다음에야 사용자가 게시글 상세 뷰로 돌아갈 수 있도록 한다.

views.py파일에 아래의 코드를 추가해 만들어보자.

from django.views.decorators.http import require_POST

@require_POST
def post_comment(request, post_id):
    post = get_object_or_404(Post, id=post_id, status=Post.Status.PUBLISHED)
    comment = None
    form = CommentForm(data=request.POST)
    if form.is_valid():
        comment = form.save(commit=False)
        comment.post = post
        comment.save()
    return render(request, 'blog/post/comment.html', {'post': post, 'form': form, 'comment': comment})

request 객체와 변수 post_id 를 매개 변수로 취하는 post_comment 뷰를 정의했다.
쟝고에서 제공하는 require_POST 데코레이터를 사용해 이 뷰는 POST 요청만 허용하게 한다.
쟝고에서는 뷰에 허용되는 HTTP 메서드를 제한할 수 있다.
다른 HTTP 메서드로 뷰에 접근하게 될 경우 쟝고에서 HTTP 405 오류가 발생한다.

이 뷰의 동작은 아래와 같다.

  1. get_object_or_404() 함수를 사용해 id로 게시된 게시글을 조회한다.
  2. 초기값 None으로 comment 변수를 정의하고, 이 변수는 Comment 객체가 생성될 때 그 객체를 저장하는 데 사용된다.
  3. 수신된 POST 데이터를 사용해 폼을 인스턴스화하고 is_valid() 메서드를 사용해 유효성을 검사한다. 폼이 유효하지 않으면 유효성 검사 오류를 포함해 템플릿이 렌더링된다.
  4. 폼 값이 유효할 경우 폼의 save() 메서드를 호출해 새로운 Comment 객체를 만들고 이를 comment 변수에 할당한다. comment = form.save(commit=False)
  5. save() 메서드는 폼이 연결된 모델의 인스턴스를 생성하고 DB에 저장한다.
    commit=False 로 설정하고 호출하면 모델 인스턴스가 생성되지만, 데이터베이스에 저장되지 않는다. 이를 이용해 객체를 최종적으로 저장하기 전에 수정할 수 있다.
    1. save() 메서드는 ModelForm에는 사용할 수 있지만, Form 인스턴스는 연결된 모델이 없어 사용할 수 없다.
  6. 작성한 댓글의 게시글을 지정한다.
    comment.post = post
  7. save() 메서드를 호출해 새로운 댓글을 DB에 저장한다.
    comment.save()
  8. blog/post/comment.html 템플릿을 렌더링하고 템플릿 context에 post, form, comment 객체를 전달한다.

이제 이 뷰의 URL 패턴을 추가해 보자.

path('<int:post_id>/comment/', views.post_comment, name='post_comment'),

댓글 폼용 템플릿 만들기

댓글 폼의 템플릿은 아래의 두 곳에서 쓰이게 된다.

  1. 사용자가 댓글을 게시할 수 있도록 하기 위한 post_detail 뷰와 연결된 게시글 상세 템플릿
  2. 폼에 오류가 있을 경우 폼을 다시 표시하기 위한 post_comment 뷰와 연결된 게시글 댓글 템플릿

폼 템플릿을 만들고 나서 {% include %} 템플릿 태그를 사용해 다른 두 템플릿에서 불러들이자.
그 후 templates/blog/post/ 디렉토리에 새로운 디렉토리 include/ 를 만들자.
이 디렉토리에 comment_form.html 이라는 새로운 파일을 생성한다.

<h2> 새로운 댓글 작성하기 </h2>
<form action="{% url 'blog:post_comment' post.id %}" method="post">
    {{ form.as_p }}
    {% csrf_token %}
    <input type="submit" value="댓글 작성하기">
</form>

comment_form.html에 위와 같이 입력했다.

이 템플릿에서는 {% url %} 템플릿 태그를 사용해 동적으로 HTML <form> 엘리먼트를 처리하는 action URL을 만든다. 이 폼은 POST 메서드로 제출되기 때문에 CSRF 보호를 위해 {% csrf_token %} 을 포함시킨다.

templates/blog/post/ 디렉토리에 새로운 comment.html 파일을 만들어 보자.

{% extends "blog/base.html" %}
{% block title %}댓글 작성하기{% endblock %}
{% block content %}
{% if comment %}
    <h2> 댓글이 추가되었습니다. </h2>
    <p><a href="{{ post.get_absolute_url }}">게시글로 돌아가기</a></p>
{% else %}
    {% include "blog/post/includes/comment_form.html"%}
{% endif %}
{% endblock %}

이 템플릿은 두 가지 다른 분기를 다룬다.

  • 제출된 폼 데이터가 유효할 경우 comment 변수에 생성된 댓글 객체가 담기고 성공 메시지가 표시된다.
  • 제출된 폼 데이터가 유효하지 않을 경우 comment 변수는 None이 된다.이 경우 댓글 폼을 표시한다.
    {% include %} 템플릿 태그를 사용해 이전에 생성한 comment_form.html 템플릿을 불러온다.

게시글 상세 뷰에 댓글 추가하기

views.py의 post_detail 뷰를 편집해 보자.

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, status=Post.Status.PUBLISHED, slug=post,
                             publish__year=year, publish__month=month, publish__day=day)
    comments = post.comments.filter(active=True)
    form = CommentForm()

    return render(request, 'blog/post/detail.html', {'post': post, 'comments': comments, 'form': form})

post_detail 뷰에 추가한 코드는 다음과 같다.

  • 게시글에 대한 모든 활성 댓글을 조회하기 위해 QuerySet을 추가했다.
    comments = post.comments.filter(active=True)
  • 이 쿼리셋은 post 객체를 사용해 작성되는데, Comment 모델에 대한 쿼리셋을 직접 작성하는 대신 post 객체를 활용해 관련 Comment 객체들을 조회한다.
    Comment 모델에서 Post 모델을 향한 ForeignKey 필드를 정의할 때 related_name 속성을 사용해 게시글과 관련된 Comment 객체들을 comments로 명명했었다.
  • form = CommentForm() 로 댓글 폼의 인스턴스도 만들었다.

게시글 상세 템플릿에 댓글 추가하기

blog/post/detail.html 을 편집해 게시글의 총 대슬 수, 댓글 목록, 댓글 추가 폼을 추가하자.

게시글의 총 댓글 수 표시 기능을 먼저 추가해보자.

{% with comments.count as total_comments %}
    <h2>
        {{total_comments}} comment{{total_comments|pluralize}}
    </h2>
{% endwith %}

템플릿에서 쟝고 ORM을 사용해 comments.count() 쿼리셋을 실행한다.
쟝고 템플릿 언어는 메서드 호출에 괄호를 사용하지 않는다.

그리고 {% with %} 태그를 사용하면 {% endwith %} 태그까지 템플릿에서 사용할 수 있는 새로운 변수에 값을 할당할 수 있다. with 템플릿 태그는 DB 사용이나 비용이 많이 드는 메서드를 여러 번 호출하는 것을 방지하는데 유용하다.

total_comments 값에 따라 “comments” 라는 단어의 복수형 접미사를 표시하기 위해 pluralizer 템플릿 필터를 사용한다. 템플릿 필터는 적용되는 변수의 값을 입력으로 사용해 계산된 값을 반환한다.
pluralize 템플릿 필터는 값이 1이 아닌 경우 문자 “s”가 포함된 문자열을 반환한다.
코드 내에서 텍스트는 게시글의 활성화된 댓글의 수에 따라 0 comments, 1 comment, N comments로 렌더링된다.

이제 게시글 상세 템플릿에 활성화된 댓글의 목록을 추가해 보자.

{% for comment in comments %}
    <div>
        <p>
            Comment {{forloop.counter}} by {{comment.name}}
            {{comment.created}}
        </p>
        {{comment.body|linebreaks}}
    </div>
{% empty %}
    <p>댓글이 없습니다.</p>
{% endfor %}

게시글 댓글 관련 연산을 반복하기 위해 {% for %} 템플릿 태그를 추가했다.
댓글 목록이 비어 있으면 사용자에게 이 게시글에 달린 댓글이 없음을 알리는 메시지를 표시한다.
각 반복시 반복 횟수를 가지는 {{ forloop.counter }} 변수와 함께 댓글들을 나열하는데, 각 댓글에는 댓글을 단 사용자 이름, 날짜, 댓글 본문이 표시된다.

마지막으로 이 템플릿에 댓글 폼을 추가하자.

{% include "blog/post/includes/comment_form.html" %}

브라우저에 접속에 아무 게시글의 상세 페이지에 들어가보자.

위와 같이 댓글 입력 폼을 확인할 수 있다.

유효한 데이터로 댓글 폼을 채우고 댓글 작성하기 버튼을 클릭하면 아래와 같은 페이지가 표시된다.

게시글로 돌아가기 링크를 누르면 게시글 세부 정보 페이지로 리디렉션되고 다음과 같이 방금 추가한 댓글을 볼 수 있다.

게시글에 댓글을 하나 더 추가하면 아래와 같이 게시글 내용 아래에 시간순으로 표시된다.

관리자 페이지로 이동해 댓글 하나를 편집해 ACTIVE 상태를 체크 해제해보자.

그 후 게시글 상세 뷰로 돌아오면 비활성화된 댓글이 표시되지 않고, 게시글 총 댓글 수에도 포함되지 않는 것을 볼 수 있다.


)

0개의 댓글