이 게시글은
김준태 강사님의DRF - Basic강의를 듣고 작성하였습니다.
📢 수업 목표
DRF로 간단한 블로그 만들기
📌 글마다 카테고리를 할당할 수 있도록 카테고리 모델을 작성
- 글을 작성할 때, 존재하지 않는 카테고리를 지정하면 안되기 때문에,
이미 존재하는 카테고리 중에서만 카테고리를 선택할 수 있도록 해야함.- 그래서 우선
카테고리를 저장해놓기 위한 모델/시리얼라이저를 작성.- 그리고
글 모델에는카테고리 필드를 하나 추가하고, 지금 만든테고리 모델의 데이터만 받을 수 있도록 수정(카테고리 모델에 없는 데이터는 절대 지정할 수 없도록)
blog/models.py
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
description = models.TextField(max_length=255)
def __str__(self): # 이 코드는 admin 페이지에서 데이터를 object() 형태가 아니라 이름으로 표시할 수 있도록 바꿔주는 코드입니다.
return f"{self.name}"
class Article(models.Model):
category = models.ManyToManyField(Category, blank=True) # () 안의 Category 는 위 Category 모델을 명시한 것입니다!
title = models.CharField(max_length=100)
content = models.TextField()
created_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
하나의 글에 category 를 여러 개 지정할 수 있도록 하기 위해서, ManyToManyField 를 사용
- Article 의
category 필드에는 카테고리 모델의 데이터 그 자체가 들어가는 것이 아니고, 카테고리 모델 중에서 어떤 데이터인지 구별하기 위한 pk 만 들어가게 된다고 생각하시면 됩니다!
📌 모델을 수정했다면 꼭 migration 이 필요
Terminal 창
$ poetry run python manage.py makemigrations
$ poetry run python manage.py migrate
📌 serializer 도 추가
blog/serializers.py
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["name", "description"]
📌 admin에 Category 모델 등록
blog.admin.py
from django.contrib import admin
from blog.models import Article, Category
# Register your models here.
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ["name", "get_description"]
@admin.display(description="카테고리 안내")
def get_description(self, obj):
return obj.description[:30]
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ["title", "get_content"]
@admin.display(description="내용")
def get_content(self, obj):
return obj.content[:30]
📌 서버 실행 > admin 페이지 접속 > 카테고리 추가

- 지금은 글을 표시할 때 제목, 내용, 글쓴시각, 수정시각 만 표시하게 됨.
- 모델을 수정해서 카테고리가 추가된 만큼, 카테고리도 표시하도록 시리얼라이저를 수정!!
📌 serializers.py 수정
blog/serializers.py
from rest_framework import serializers
from .models import Article, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["name", "description"]
class ArticleSerializer(serializers.ModelSerializer):
# article 을 표시할 때 카테고리 객체도 직렬화해서 보기 위한 설정
# 이게 없다면 카테고리 이름이 아니라, 그냥 id 숫자로만 표시되게 됨
category = CategorySerializer(many=True, required=False, read_only=True)
class Meta:
model = Article
fields = ["title", "content", "category"]
- serializer 안에서는 단순히 어떤 내용을 표시할지 뿐만 아니라,
해당 내용(필드)을 표시할 때 정확히 어떻게 표시할 건지도 명시 가능category 는 여러 개 지정 가능하기 때문에, 데이터가 여러개 들어올 수 있다는 것을 알려주기 위해서many=True옵션을 명시해야 함.
📌 create 함수 작성
그런데 위처럼만 작성하게 되면, 사용자로부터 카테고리를 입력받아서 글을 작성하는 과정이 조금 복잡해지게 됨.
- 카테고리를 받아서 글이 저장되는 과정을 조금 더 간단히 만들기 위해서, create 함수를 추가
create 함수의 역할
- 지금 create 함수는 유저로부터 카테고리 번호를 받고, 글에 해당 번호의 카테고리를 지정해주기 위해서 작성!!
foreign key (ManyToMany)가 포함된 모델의 데이터를 저장할 때면 보통 create 함수를 커스텀해서 사용!!
article 시리얼라이저에 아래 내용을 추가할텐데, 이 함수는 views 에서 serializer.save() 시에 실행될 함수임.
- 만약 create 함수가 없다면 자동으로 db에 저장하는 과정을 수행하지만, 있다면 해당 함수의 내용대로 db에 저장하는 과정이 수행됨.
def create(self, validated_data): # is_valid 이후에 save 함수가 실행되므로, 현재 유저가 작성해서 들어온 데이터를 validated_data 라고 해서 받습니다.
get_categories = validated_data.pop("get_categories", []) # 유저가 "{title: 제목, content: 내용, get_categories: [카테고리번호들]}" 형태로 데이터를 보냈다면, 여기서 get_categories 부분만 추출합니다.
article = Article(**validated_data) # 해당 내용을 토대로 article 객체를 만듭니다.
article.save() # 해당 객체를 db 에 저장합니다.
article.category.add(*get_categories) # 저장된 그 글에 대해서 유저가 보낸 카테고리 번호에 맞는 category 를 지정합니다.
return article
blog/serializers.py
ArticleSerializer완성본
from rest_framework import serializers
from .models import Article, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["name", "description"]
class ArticleSerializer(serializers.ModelSerializer):
category = CategorySerializer(many=True, required=False, read_only=True) # article 을 표시할 때 카테고리 객체도 직렬화해서 보기 위한 설정 (이게 없다면 카테고리 이름이 아니라, 그냥 id 숫자로만 표시되게 됨)
get_categories = serializers.ListField(required=False) # 유저한테 리스트 형태로 카테고리를 입력받을 수 있도록, 추가한 필드
class Meta:
model = Article
fields = ["title", "content", "category", "get_categories"]
def create(self, validated_data): # views 에서 serializer.save() 실행 시 아래 코드가 실행됨 (이 create 함수가 없으면 자동으로 알아서 저장하고, 있으면 여기 있는 내용대로 저장과정을 수행함)
get_categories = validated_data.pop("get_categories", [])
article = Article(**validated_data)
article.save()
article.category.add(*get_categories)
return article
📌 Category 모델 생성 직후
- 게시글마다 등록된 카테고리가 없는 상황
📌 POST 요청
get_categories라는키:밸류 쌍을 하나 더 추가해서 Post 요청
📌 GET 요청
blog/로 접속하여 등록된 모든 글 조회
blog/<int:pk>로 접속하여 특정 글에 대해 조회
- 1번, 3번에 해당하는 카테고리가 글에 잘 할당되어서, 표시되는 것을 확인 가능
📌 models.py 에 comment 모델을 추가
- 댓글은 무조건
특정한 글에 달리는 것이기 때문에,
foreign key 형태로 Article 모델과 연결하는 article 필드를 추가
blog/models.py
class Comment(models.Model):
content = models.CharField(max_length=100)
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
created_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
ManyToManyField vs ForeignKey
ManyToManyField는N:N관계일 때,ForeignKey는1:N관계일 때 사용하는 필드.
related_name
related_name: Article 쪽에서 Comment 모델 객체에 접근할 때 쓰는 이름- 지금은 Comment 모델에
어떤 글에 달린 댓글인지 그 글 번호를 저장하고 있으나,
결국에 우리는 글을 보여줄 때 댓글도 함께 보여줘야 합니다.- 그러려면 글을 가져오는 과정 (Article 쪽) 에서 댓글까지 함께 가져와야 하는데,
그때 해당 이름이 사용되는 것
📌 serializer 작성
blog/serializers.py
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["content", "article"]
extra_kwargs = {'article': {'write_only': True}}
- 보통 블로그에서 댓글을 볼 때는 특정한 글 하단 부분에 내용만 표시 (content) 되고,
해당 댓글이 달린 글 번호 (article) 까지 함께 표시하지는 않기 때문에,
'article'에'write_only': True옵션을 걸어서 글 번호는 표시하지 않도록 설정
- comments = CommentSerializer(many=True, read_only=True) 부분과 함께
필드에 comments 가 추가된 것을 제외하고는 달라진 것이 없음.
blog/serializers.py
class ArticleSerializer(serializers.ModelSerializer):
category = CategorySerializer(many=True, required=False, read_only=True)
get_categories = serializers.ListField(required=False)
comments = CommentSerializer(many=True, read_only=True) # comment 객체를 직렬화해서 보기 위한 설정
class Meta:
model = Article
fields = ["id", "title", "content", "category", "get_categories", "comments"] # 필드에 comments 추가
def create(self, validated_data):
get_categories = validated_data.pop("get_categories", [])
article = Article(**validated_data)
article.save()
article.category.add(*get_categories)
return article
views.py 내부의 BlogDetail 클래스에다가 댓글 작성을 위한 함수를 추가
- 서버주소/blog/1/ 로 POST 요청을 보내면, 해당 1번 글에 댓글이 달리도록 작성하는 것!!
우선 1번 글이 진짜로 존재하는지 확인하고 → 댓글 내용을 직렬화한 후에 → 해당 1번 글과 댓글을 연결하면서 db 에 저장하도록 할 것임.
blog/views.py
# comment api 를 따로 분리하기도 하는데, 이번에는 간단하게 여기서 post 를 보내도록 하겠습니다
# 프로젝트의 규모가 커진다면 posts 라는 앱, comments 라는 앱으로 분리해서 각각 진행하시는게 좋습니다
def post(self, request, pk):
article = Article.objects.get(pk=pk) # 우선 실제로 존재하는 글에 대해서 댓글을 달고자 하는 것인지 확인
comment_serializer = CommentSerializer(data=request.data, partial=True) # partial=True 라고 하면, 일부 데이터가 없어도 validation 을 통과합니다.
if comment_serializer.is_valid():
comment_serializer.save(article=article) # 유저가 작성한 댓글을 db 에 저장할 때, 앞서 db 에서 찾은 article 과 연결하면서 저장함
return Response({"message": "정상"}, status=status.HTTP_200_OK)
return Response(comment_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
→ 우선은 조금 더 간단한 방식으로 진행한 것이고, 실제로는 지금 같은 경우에 partial=True 를 쓰지는 않음.
- 예외처리도 조금 부족하게 되어있기 때문에, 더 정확한 방식에 대한 공부가 필요함.
admin 페이지에서 확인을 좀 더 편하게 하기 위해 설정 추가blog/admin.py
from django.contrib import admin
from blog.models import Article, Category, Comment
# Register your models here.
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ["name", "get_description"]
@admin.display(description="카테고리 안내")
def get_description(self, obj):
return obj.description[:30]
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ["title", "get_content"]
@admin.display(description="내용")
def get_content(self, obj):
return obj.content[:30]
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = [ "get_article", "get_content"]
@admin.display(description="댓글")
def get_content(self, obj):
return obj.content[:30]
@admin.display(description="게시글 제목")
def get_article(self, obj):
return obj.article.title
POST메서드로 댓글 등록
GET메서드로 정상 등록 여부 확인