스파르타 코딩클럽 내일배움캠프 AI 웹개발자양성과정 3회차
2022.10.31. 42일차 - TIL
1. Django 심화 원격 강의
1) 목표
Restful한 API 설계가 가능하다미디어 파일과 스태틱 파일에 대해 이해한다게시글 모델과 조회/업로드를 위한 Serializer를 만들 수 있다이미지를 포함한 게시글 기능을 개발할 수 있다포스트맨으로 백엔드 개발을 하면서 테스팅을 할 수 있다DRF에서 댓글 기능을 개발할 수 있다DRF에서 좋아요 기능을 개발할 수 있다DRF에서 follow 기능을 개발할 수 있다Many-to-Many 관계를 설정하는 경우와 방법, 그리고 Related_name의 사용 방법을 이해한다
2) Restful API
Restful API란 HTTP와 URL 기반으로 자원에 접근할 수 있도록 제공하는 애플리케이션 개발 인터페이스이다. 즉, URL을 통해 자원을 명시하고(django에서는 이런 자원으로 app도 만든다...) HTTP Method(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD 연산을 적용하는 것을 말한다. 그러니 지금 인스타 클론 코딩을 위해 우리가 설정한 자원이 사용자(users)와 게시글(articles)인 것이고, 각각의 app에 대해 view를 작성할 때 http method로 기능을 만드는 것이다. 이번에 추가되는 articles의 urls.py와 views.py는 다음과 같다.
#articles의 urls.py
from django.urls import path
from articles import views
urlpatterns = [
path('', views.ArticleView.as_view(), name='article_view'),
path('<int:article_id>', views.ArticleDetailView.as_view(), name='article_detail_view'),
path('<int:article_id>/comment/', views.CommentView.as_view(), name='comment_view'),
path('<int:article_id>/comment/<int:comment_id>, views.CommentDetailView.as_view()', name='comment_detail_view'),
path('<int:article_id>/like', views.LikeView.as_view(), name='like_view'),
]
from rest_framework.views import APIView
from rest_framework import status, permissions
from rest_framewokrk.response import Response
# 게시글 보기/작성하기
class ArticleView(APIView):
def get(self, request):
pass
def post(self, request):
pass
# 게시글 상세보기/수정하기/삭제하기
class ArticleDetailView(APIView):
def get(self, request, article_id):
pass
def put(self, request, article_id):
pass
def delete(self, request, article_id):
pass
# 댓글 보기/작성하기
class CommentView(APIView):
def get(self, request, article_id):
pass
def post(self, request, article_id):
pass
# 댓글 수정하기/삭제하기
class CommentDetailView(APIView):
def put(self, request, article_id, comment_id):
pass
def delete(self, request, article_id, comment_id):
pass
# 게시글 좋아요
class LikeView(APIView):
def post(self, request, article_id):
pass
3) 게시글의 모델 설계
from django.db import models
from users.models import User
class Article(models.Model):
user=models.ForeignKey(User, on_delete=models.CASCADE)
title=models.CharField(max_length=50)
content=models.TextField()
image=models.ImageField()
created_at = models.DateTimeField(auto_now_add=True)
updateed_at = models.DateTimeField(auto_now=True)
# admin에서 편하게 보기 위해
def __str__(self):
return str(self.title)
auto_now는 django model이 save될 때마다 현재날짜로 갱신되고, auto_now_add는 django model이 최조 저장 시에만 현재날짜를 적용한다.
4) 스태틱과 미디어파일
Static 파일은 개발자가 준비해 사용자에게 보여주는 개발 리소스로서의 정적인 파일로 css, js, image 등이 있다. Media 파일은 사용자가 업로드하는 파일로 FileField, ImageField를 통해 저장된 파일이다. Media 파일의 처리 순서는 다음과 같다.
- HTTPRequest.FILES를 통해 파일 전달
- view or form 로직을 통해 유효성 검증 수행
- FileField/ImageField에 경로 저장(문자열)
- settings.MEDIA_ROOT에 파일 저장
각 변수에 대해 정리하면 다음과 같다.
STATIC_URL : static 파일을 제공할 url, 템플릿 태그 {% static '경로' %}에 의해 참조되는 설정
STATICFILES_DIRS : File System Loader에 의해 참조되는 설정, 프로젝트 전반적으로 사용되는 static 파일
STATIC_ROOT : static 파일들을 복사하여 모아 놓을 경로
MEDIA_URL : media 파일 접근할 때 사용
MEDIA_ROOT : 업로드된 파일이 저장되는 공간
이제 필요한 코드를 수정해보면 다음과 같다.
일단 이미지 처리에 필요한 Pillow를 설치한다.
$pip install Pillow
settings.py에 다음을 추가한다.
DEBUG = True인지 확인
...
STATIC_ROOT = BASE_DIR / "static"
STATIC_URL = "/static/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
urls.py를 수정한다.
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
그리고 image 파일을 효율적으로 처리하기 위해 Article 모델을 조금 수정한다.
class Article(models.Model):
...
image = models.ImageField(blank=True, upload_to="$Y/%m")
...
}
# 이미지가 없는 게시글도 허용하기 위해 blank=True로 설정하고,
# media 폴더에 연/월 형식의 하위 폴더 구조로 관리한다.
5) 게시글 리스트와 게시글 작성
게시글 리스트를 위해 views.py와 serializers.py를 작성해보면 다음과 같다.
# views.py
...
class ArticleView(APIView):
def get(self, request):
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
...
# serializers.py
from rest_framework import serializers
from articles.model import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
# 만약 특정 필드만 반환되기를 원한다면
# 다음과 같이 serializer 정의 후
# views.py에서 해당 serializer 사용
class ArticleListSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("pk", "title", "image", "updated_at", "user")
하지만 반환되는 값을 보면 user는 외래키기 때문에 User의 기본키인 id가 반환되는 것을 볼 수 있다.(우리는 식별 가능한 아이디나 이메일을 알고 싶을 것 같은데...) 이럴때는 SerializerMethodField를 사용한다. serializers.py를 다음과 같이 변경할 수 있다.
class ArticleListSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
#반드시 get_위의변수명 이어야 한다.
def get_user(self, obj):
return obj.user.email
class Meta:
model = Article
fields = ("pk", "title", "image", "updated_at", "user")
게시글 작성을 위해 views.py를 다음과 같이 수정한다.
# views.py
...
class ArticleView(APIView):
...
def post(self, request):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
...
하지만 postman으로 json데이터를 아무리 보내도 에러가 날텐데 그건 우리가 Article 모델에 user 필드를 만들었기 때문이다. 그래서 우리는 앞서서 login으로 access token을 받으면 postman으로 json 데어터를 보낼 때 Headers의 KEY에 Authorization, VALUE에 Bearer access_token값을 함께 넣어 보냈었다. 하지만 코드 실행 시 user 정보가 없다고 뜨는데 이것은 save할 때 user에 저장할 정보가 없기 때문이다. 그래서 이번에는 serializer을 활용해보자. 먼저 views.py를 다음과 같이 수정한다.
# views.py
...
class ArticleView(APIView):
...
def post(self, request):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.data)
else:
return Response(serializer.errors)
이렇게 작성해도 user가 없다고 오류가 발생하는데 이것은 is_valid에서 이미 user 정보가 없어 유효하지 않기 때문에 else문으로 넘어가기 때문이다. 그래서 새로운 serializer을 정의하기로 했다.
# serializers.py
class ArticleCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("title", "image", "content")
그리고 views.py에서 해당 시리얼라이저를 사용한다.
# views.py
...
class ArticleView(APIView):
...
def post(self, request):
serializer = ArticleCreateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
이것은 프론트 정보를 백에 넘겨주는 것이기 때문에 이상이 없다. 이렇게 작성하면 postman의 form-data에서도 title, image, content값을 넣어 article을 생성할 수 있다.
6) 게시글 상세/수정/삭제
게시글 상세보기, 수정하기, 삭제 기능을 만들어보겠다. 우선 views.py를 수정해보자.
# views.py
import rest_framework.generics import get_object_or_404
...
class ArticleDetailView(APIView):
# 게시글 상세보기
def get(self, request, article_id):
# article_id인 게시물 가져오기
# article = Article.objects.get(id=article_id)
article = get_object_or_404(Article, id=article_id)
serializer = ArticleSerializer(article)
return Response(serializer.data, status=status.HTTP_200_OK)
def put(self, request, article_id):
article = get_object_or_404(Article, id=article_id)
# 요청자가 게시글 작성자일 경우에만 삭제 가능
if request.user == article.user:
serializer = ArticleCreateSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save() # 수정이기 때문에 user정보 불필요
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response("권한이 없습니다.", status=status.HTTP_403_FORBIDDEN)
def delete(self, request, article_id):
article = get_object_or_400(Article, id=article_id)
if request.user == article.user:
article.delete()
return Response("삭제되었습니다.", status=status.HTTP_204_NO_CONTENT)
else:
return Response("권한이 없습니다.", status=status.HTTP_403_FORBIDDEN)
7) 댓글 CRUD
댓글 기능을 구현하려고 한다. 먼저 댓글의 model을 만들고 migrate를 하자.
# models.py
...
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comment_set')
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.content)
그리고 comment를 위한 serializer을 추가해보겠다.
# serializers.py
class CommetSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
# fields 값이 한개여도 콤마(,) 필수
class CommentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ("content",)
이제 views.py를 수정한다. 댓글 기능은 게시글 기능을 보고 참고해 작성한다.
# views.py
# 댓글 보기/작성하기
class CommentView(APIView):
def get(self, request, article_id):
# 댓글을 확인할 게시물 가져오기
article = get_object_or_404(Article, id=article_id)
comments = article.comment_set.all()
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, article_id):
serializer = CommentCreateSerializer(data=request.data)
if serializer.is_valid():
# 댓글은 user뿐만 아니라 article_id도 필수이다
serializer.save(user=request.user, article_id=article_id)
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 댓글 수정하기/삭제하기
class CommentDetailView(APIView):
def put(self, request, article_id, comment_id):
comment = get_object_or_404(Comment, id=comment_id)
# 요청자가 댓글 작성자일 경우에만 삭제 가능
if request.user == comment.user:
serializer = CommentCreateSerializer(comment, data=request.data)
if serializer.is_valid():
serializer.save() # 수정이기 때문에 user, article_id 정보 불필요
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response("권한이 없습니다.", status=status.HTTP_403_FORBIDDEN)
def delete(self, request, article_id, comment_id):
comment = get_object_or_400(Comment, id=comment_id)
if request.user == comment.user:
comment.delete()
return Response("삭제되었습니다.", status=status.HTTP_204_NO_CONTENT)
else:
return Response("권한이 없습니다.", status=status.HTTP_403_FORBIDDEN)
다시보니 put과 delete에서 article_id는 별로 필요없어 보인다... Django에서 모델을 설정할 때 ForeignKey 설정해주고 related_name을 설정주는데 이것은 역참조 시 사용될 이름을 지정해주는 역할을 한다. related_name을 지정하지 않으면 기본적으로 tablename_set 형태로 지정된다.
그렇다면 역참조란 무엇일까? 예를 들어보자. 사용자 테이블('sara', 'tom'...)이 있고, 취미 테이블('music', 'exercise'...)이 있다고 하자. 사용자 테이블 내 hobby라는 필드를 만들어 각 사용자 별 취미 정보를 저장하고 싶다면 이것은 정참조가 된다. 이렇게 정의가 되어있을 때 해당 hobby를 참조하고 있는 사용자 정보를 불러오고 싶을 때가 있을 것이다. 이때 바로 역참조를 사용하는 것이고, hobby.related_name명으로 해당 hobby를 참조하고 있는 사용자 테이블의 object를 가져올 수 있다.
아직도 잘 이해가 안가서 더 찾아본 결과, 이렇게 정리하면 될 것 같다. 아래와 같은 model이 있다고 가정하자.
집 model의 사람 column은 '사람' model을 Foreign Key로 지정한 값이다.
이때 참조란 집 model에서 사람(Foreign Key) 정보를 불러들이는 것을 말하고, 역참조란 사람 model이 집 model을 불러들이는 것을 말한다.
그러니까 위 코드에서 comment를 집, article을 사람이라고 대입한다면 comment에서 article 정보를 불러오는 것은 참조, article이 comment를 불러들이는 것을 역참조라고 한다. 결국 특정 article에 대해 comment 정보를 불러오니까 역참조로 comments = article.comment_set.all()이 되는 것이다.(이해하는라 힘들었다....)
8) 좋아요 기능
좋아요 기능을 위해 기존 Article 모델을 수정할 필요가 있다. 다음을 추가한다.
from django.db import models
from users.models import User
class Article(models.Model):
...
likes = models.ManyToManyField(User, related_name="like_articles")
...
이때 related_name을 설정해야 하는 이유는 위에 이미 User를 외래키로 참조하면서 article_set이 사용되었기 때문이다. 참고로 'like_articles'는 사용자 정보에서 좋아요한 글을 참고하고자 할때(역참조) 사용된다.
이제 views.py를 수정한다.
# 게시글 좋아요
class LikeView(APIView):
def post(self, request, article_id):
# 게시글 가져오기
article = get_object_or_404(Article, id=article_id)
# 현재 사용자가 article의 likes 필드에 있다면
if request.user in article.likes.all():
# 해당 사용자를 필드값에서 제거
article.likes.remove(request.user)
return Response("like가 취소되었습니다", status=status.HTTP_200_OK)
# 현재 사용자가 article의 likes 필드에 없다면
else:
# 해당 사용자를 필드값에 추가
article.likes.add(request.user)
return Response("like가 되었습니다", status=status.HTTP_200_OK)
9) 팔로우 기능
팔로우 기능을 위해 기존 User 모델에 아래의 내용을 추가해보자.
class User(AbstractBaseUser):
...
followings = models.ManyToManyField('self', symmetrical=False, related_name='followers')
...
그리고 urls.py에 해당 뷰를 위한 url을 추가한다.
urlpatterns = [
...
path("follow/<int:user_id>/", views.followView.as_view(), name='follow_view'),
]
팔로우 기능을 위한 view를 추가한다.
...
class FollowView(APIView):
def post(self, request, user_id):
# 팔로우할 대상 가져오기
you = get_object_or_404(User, id=user_id)
me = request.user # 현재 사용자
# 현재 사용자가 팔로우 대상의 following에 있다면
if me in you.followers.all():
# 팔로우 대상의 following에서 나를 제거
you.followers.remove(me)
return Response("unfollow 했습니다", status=status.HTTP_200_OK)
# 현재 사용자가 팔로우 대상의 following에 없다면
else:
# 팔로우 대상의 following에서 나를 추가
you.followers.add(me)
return Response("follow 했습니다", status=status.HTTP_200_OK)
이때 주의할 것은 나의 팔로우 정보(followings)가 아니라 상대의 팔로우 정보이기 때문에 followers로 해야된다.
10) 게시글 상세 페이지와 리스트 페이지 serializer 수정
11) 프로필 페이지
12) 피드 기능
참고자료
https://cowimming.tistory.com/117
[Django] 참조 & 역참조
좋아요 기능을 만들기 위해 공부하고 있는데 역참조가 나와서 잠깐 정리!! 다음과 같은 Model이 두 개가 있다고 가정해보자. 집 model은 살고있는 사람, 방의 개수와 같은 정보가 필요하다. 그 중 사
cowimming.tistory.com
'개발일지 > AI 캠프' 카테고리의 다른 글
내일배움캠프 AI - 44일차 TIL, 2022.11.02 (0) | 2022.11.04 |
---|---|
내일배움캠프 AI - 43일차 TIL, 2022.11.01 (0) | 2022.11.04 |
내일배움캠프 AI - 9주차 WIL (0) | 2022.10.31 |
내일배움캠프 AI - 40일차 TIL, 2022.10.27 (0) | 2022.10.28 |
내일배움캠프 AI - 39일차 TIL, 2022.10.26 (0) | 2022.10.26 |