Study/CSSU

[CSSU] 장고 튜토리얼

MuviSsum 2021. 8. 22. 00:08

 

네 번째 주제 - 장고 튜토리얼

 

Django Tutorial

MTV패턴 기본 Django가 아닌 DRF를 대상으로 설명한다.

코드 포매터 Black 사용, AWS EC2 사용

시작

가상환경 생성 및 실행

# 파이썬 설치 및 버전확인
python -V
# 가상환경 생성
python -m venv venv
# 가상환경 실행
source venv/Scripts/activate

장고 설치 및 프로젝트 생성

# 설치
pip install Django
# 버전확인
python -m django --version
# 프로젝트 생성
django-admin startproject mysite
# 프로젝트로 이동
cd mysite
# 장고 실행 확인
python manage.py runserver
# http://localhost:8000/로 접속

장고 기본 설정 및 DRF 세팅

# DRF 설치
pip install djangorestframework
# API 문서 마크다운화(설치 안해도 되지만 권장)
pip install markdown
# 장고 필터 기능 - 레퍼런스 참고(굳이 설치 X), 설치한다면 INSTALLED_APPS 추가 요망
pip install django-filter

# ------ 여기 불확실 ------ 안 되는 경우 있음.
# 장고의 여러 확장 기능(설치 권장)
pip install django-extensions
# django-extensions의 관계도 출력 기능을 사용하기 위해
pip install pyparsing pydot
# 출력 방법
python manage.py graph_models --pydot -a -g -o my_project_visualized.png

setting .py

# 순서대로 하면 줄 위치가 딱 맞다.

# 28줄, 모든 사용자가 들어올 수 있도록 CORS랑 다른 거다^^
ALLOWED_HOSTS = ['*']

# language, timezone 변경 106줄 위치
LANGUAGE_CODE = "ko-kr"

TIME_ZONE = "Asia/Seoul"

# INSTALLED_APPS에 추가 33줄
INSTALLED_APPS = [
    # basic django
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # third party
    "rest_framework",
    "django_extensions",
]

# 맨 밑줄에 추가할 것들
# 추가 해도 되는데, 기본 장고 auth 사용하면 안 해도 됨.
REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ]
}

# ----- 아직 불확실 ----- 안되는 경우 있음.
# DB 관계도 출력용
GRAPH_MODELS = {
  'all_applications': True,
  'group_models': True,
}

urls.py

# DRF 패스 추가
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api-auth/", include("rest_framework.urls")),
]

유저

앱 생성 및 JWT 설치

# 유저 앱 생성
python manage.py startapp accounts
# JWT 설치
pip install djangorestframework-jwt

앱 등록

settings.py

# 33번째 줄, 앱 추가
INSTALLED_APPS = [
    # app
    "accounts",
    # basic django
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # third party
    "rest_framework",
    "django_extensions",
]

# 유저 커스텀 모델 생성 및 JWT 맨 밑줄에 추가
# Custom User Model
AUTH_USER_MODEL = "accounts.User"

# JWT
import datetime

JWT_AUTH = {
    "JWT_EXPIRATION_DELTA": datetime.timedelta(days=1),
}
# restframework 설정 부분도 있는데, 디폴트 쓰면 된다.
# JWT 설정하는 부분, 다 디폴트 값이 들어가 있고, 혹시나 시크릿 키를 이용할 때나 좀 더 커스텀하고 싶을 때 사용해보자.
JWT_AUTH = {
    'JWT_SECRET_KEY': SECRET_KEY,
    'JWT_ALGORITHM': 'HS256',
    'JWT_ALLOW_REFRESH': True,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28),
}

새로운 앱(accounts)에서 새로운 accounts/urls.py 생성

from django.urls import path
from . import views

from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token

urlpatterns = [
    # views에서는 signup만 만듬. 왜냐하면? jwt 토큰으로 로그인해서 사용할 거기 때문.
    path("signup/", views.signup),
    path("login/", obtain_jwt_token),
    path('login/verify/', verify_jwt_token),
    path('login/refresh/', refresh_jwt_token),
]

새로운 앱의 urls.py를 기본 프로젝트 urls.py에 include 설정

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),
    path("api-auth/", include("rest_framework.urls")),
]

이제 accounts/views.py 작성

@api_view(['POST'])
def signup(request):
    password = request.data.get('password')
    password_confirmation = request.data.get('password_confirmation')

    if password != password_confirmation:
        return Response({'error': '비밀번호가 일치하지 않습니다.'}, status=status.HTTP_400_BAD_REQUEST)

    serializer = UserSerializer(data=request.data)

    if serializer.is_valid(raise_exception=True):
        user = serializer.save()
        user.set_password(request.data.get('password'))
        user.save()

    return Response(serializer.data, status=status.HTTP_201_CREATED)

현재 위의 상태로 볼 때, serializer가 없다.

accounts/serializers.py 새로 생성 후, 작성

from rest_framework import serializers
from django.contrib.auth import get_user_model

User = get_user_model()

class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ('username', 'password',)

원래라면 그냥 이렇게만하고 모델을 작성 안해도 가능하다.

하지만 커스텀 user를 settings.py에서 작성했기 때문에 모델에 user를 작성한다.

커스텀 안 할거면 아예 세팅에서 삭제하거나 모델 클래스를 pass로 작성한다.

accounts/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    pass

admin페이지에서 user를 볼 수 있도록 설정

accounts/admin.py

from django.contrib import admin
from .models import User

admin.site.register(User)

마이그레이션

python manage.py makemigrations accounts
python manage.py migrate accounts
# 처음 세션을 만들어주기 위해
python manage.py migrate

수퍼유저 생성 및 실행, 확인

# 유저 이름, 이메일, 비밀번호, 비밀번호 확인 입력
python manage.py createsuperuser
# 실행
python manage.py runserver

실행했으면 signup을 해보자.

localhost:8000/accounts/signup으로 들어가서 밑의 샘플로 값을 보낸 후,

User signup 샘플

{
    "username": "taewan",
    "password": "asd123asd",
    "password_confirmation": "asd123asd"
}

localhost:8000/admin으로 위에서 생성했던 수퍼유저를 통해 접속해서 회원가입이 잘되어 있는지 확인한다.

배포

AWS EC2 기준

엘라스틱 IP로 EC 연결 후, 보안 그룹에서 8000포트를 열어준다.

EC2에 접속하여 도커와 도커 컴포즈를 다운받는다.

가상환경을 설치했던 루트폴더에서 pip freeze > requirements.txt를 통해 필요한 디펜던시를 저장해 둔다.

도커 파일 작성

FROM python:3.8
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY ./mysite /app
EXPOSE 8000

도커 컴포즈 파일 작성

version: '3.9'

services:
  django:
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - ./mysite/:/app/
    command: ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
    restart: always
    ports:
      - 8000:8000

해당 파일들이 있는 폴더에서 docker-compose up을 하면 된다.

(해당 파일들은 위에서 가상환경을 설치했던 처음 루트폴더에 위치한다. 바꾸어도 상관없지만 yml, 도커파일 안 내용을 조금 수정해야한다.)

여담으로 도커 컴포즈 파일 작성 안하고 command를 도커파일로 옮기고 도커 파일 빌드 후, 포트 옵션과 restart 폴리시를 설정하고 도커컨테이너를 올리면 똑같다.

그 후, 도커 컨테이너가 올라갔으면 docker exec -it {컨테이너 이름} bash를 통해 컨테이너에 접속하여 cd app으로 이동한다. 이동 후, python3 manage.py makemigrations, python3 manage.py migrate를 통해 DB를 만들어준다.

이제 엘라스틱 IP의 퍼블릭 IP를 통해 접속해본다. IP:8000/admin 접속 성공하면 굳!

DB

기본적인 Mysql을 사용하려 한다.

# django와 mysql 연결하기 위한 라이브러리
pip install mysqlclient

settings.py

# 지금까지 따라왔다면 82줄에 입력, 원래는 sqlite가 존재했던 자리이다.
# ※주의※ HOST에 http://는 붙이지 않는다.
DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': '{DB 이름}',
        'USER': '{유저 이름, 루트 쓰면 root}',
        'PASSWORD': '{유저의 패스워드}',
        'HOST': '{DB URL, local환경은 localhost}',
        'PORT': '3306',
    }
}

이제 확인해보자.

# 중간에 더 많은 모델이 들어가고 makemigrations를 못했다면 makemigrations도 먼저 해주기.
python manage.py migrate
# 확인
python manage.py runserver

위에서 만든 유저들은 DB에 의해 날라 갔을거니까 새로 다시 만들어서 확인해 보면 된다.

API (ForienKey)

이제 사전 작업이 끝났다고 볼 수 있다.

제대로된 CRUD API를 만들어 보자.

community app 생성

# 커뮤니티 앱 생성
python manage.py startapp community

settings.py

# 앱 등록 33줄
INSTALLED_APPS = [
    # app
    "accounts",
    "community",
    # basic django
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # third party
    "rest_framework",
    "django_extensions",
]

accounts에서 했듯이 url 분리를 한다.

urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),
    path("community/", include("community.urls")),
    path("api-auth/", include("rest_framework.urls")),
]

이렇게 각 메서드를 모아서 작성하는게 좋은 건 아니다.

그런데 음.. 공식문서에는 그렇게 작성이 되어있는 경우가 몇개 있긴한데, 이건 가독성과 코드량, 그리고 회사마다의 차이일 것 같다.

테스트를 하기 위해서는 아마... 다 따로 작성하지 않을까 예상해본다.

(확실한 건 아님!! 참고만 하세요!!)

현재는 튜토리얼이니까 넘어가자.

또한 이렇게 패스변수로 넣는 경우와 파라미터로 넣는 경우가 있다.

pk값의 경우, 난 패스로 넣는 것을 선호한다.

community/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.article_CR),
    path('<int:pk>/', views.article_UD),
    path('<int:pk>/comment/', views.comment_CR),
    path('<int:pk>/comment/<int:comment_pk>/', views.comment_D),
]

확인을 할때 JSON 웹토큰 어노테이션(@authentication_classes([JSONWebTokenAuthentication]))은 없애서 확인하자.

프론트엔드를 만들어서 연결하면서 확인할 때는 상관없는데, 장고의 브라우저API를 사용할 때는 웹 토큰을 헤더로 넣어서 보내야하기 때문에 번거롭다.

community/views.py

from django.shortcuts import get_object_or_404

from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import api_view

from rest_framework.decorators import authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

from .serializers import ArticleSerializer, CommentSerializer
from .models import Article, Comment

# Create your views here.


@api_view(["GET", "POST"])
@authentication_classes([JSONWebTokenAuthentication])
@permission_classes([IsAuthenticated])
def article_CR(request):
    if request.method == "GET":
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)
    else:
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save(user=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)


@api_view(["PUT", "DELETE"])
@authentication_classes([JSONWebTokenAuthentication])
@permission_classes([IsAuthenticated])
def article_UD(request, pk):
    article = get_object_or_404(Article, pk=pk)

    if not request.user.articles.filter(pk=article.pk).exists():
        return Response({"detail": "권한이 없습니다."})

    if request.method == "PUT":
        serializer = ArticleSerializer(article, data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data)
    else:
        article.delete()
        return Response({"id": pk})


@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def comment_CR(request, pk):
    article = get_object_or_404(Article, pk=pk)

    if request.method == "GET":
        comments = Comment.objects.filter(article=article)
        serializer = CommentSerializer(comments, many=True)
        return Response(serializer.data)
    else:
        serializer = CommentSerializer(data=request.data)

        if serializer.is_valid(raise_exception=True):
            serializer.save(user=request.user, article=article)
            return Response(serializer.data, status=status.HTTP_201_CREATED)


@api_view(["DELETE"])
@authentication_classes([JSONWebTokenAuthentication])
@permission_classes([IsAuthenticated])
def comment_D(request, pk, comment_pk):
    comment = get_object_or_404(Comment, pk=comment_pk)

    if not request.user.comments.filter(pk=comment_pk).exists():
        return Response({"detail": "권한이 없습니다."})

    comment.delete()
    return Response({"id": comment_pk})

related_name을 통해 각 모델에서 다른 모델로 연결할 수 있다.

ForiegnKey의 경우, related_name을 설정안하면 자동으로 각 모델이름의 뒤에 _set이 붙여져서 만들어진다.

밑의 경우에서 comment가 article이랑 연결될 때, related_name을 설정하지 않았다면 comment_set이 된다.

community/models.py

from django.db import models
from django.conf import settings


class Article(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="articles")
    article_title = models.CharField(max_length=100)
    content = models.TextField()
    showArticle = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.article_title

class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="comments")
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments")
    content = models.CharField(max_length=200)
    created_at = models.DateField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)

    def __str__(self):
        return self.content

여기서 보낼 값을 설정할 수 있다.

*※주의※ 받는 값이 아니라, 보내는 값이다. *

community/serializers.py

from rest_framework import serializers
from .models import Article, Comment


class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ("id", "article_title", "showArticle", "content")


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ("id", "content")

community/admin.py

from django.contrib import admin
from .models import Article, Comment


admin.site.register(Article)
admin.site.register(Comment)

자, 작성 다 했으면 마이그레이션 해준다.

python manage.py makemigrations community
python manage.py migrate community

실행해서 밑의 예제를 작성하고 추가 및 읽어보자

브라우저 API에서 확인 시, views.py에서 웹토큰 어노테이션 빼고 실행하거나 헤더에 토큰 추가 필수(위 views.py에서 말했던 부분이다)

article

{
    "article_title": "아티클입니다.",
    "content": "컨텐트입니다."
}

comment

{
    "content": "컨텐트입니다."
}

추가 모델 연결

community/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("", views.article_CR),
    path("<int:pk>/", views.article_RUD),  # 함수이름 변경
    path("<int:pk>/comment/", views.comment_CR),
    path("<int:pk>/comment/<int:comment_pk>/", views.comment_D),
    path("<int:pk>/comment_article/<int:comment_pk>/", views.comment_article_reverse),  # 추가
]

community/views.py

# 원래 있던 article_UD -> article_RUD로 변경 및 GET 메서드 추가
@api_view(["GET", "PUT", "DELETE"])
@permission_classes([IsAuthenticated])
def article_RUD(request, pk):
    article = get_object_or_404(Article, pk=pk)

    if request.method == "GET":
        serializer = ArticleSerializer(article)
        data = {
            "article": serializer.data,
            "comments": CommentSerializer(article.comments.all(), many=True).data,        # 이부분
        }

        return Response(data)

    if not request.user.articles.filter(pk=article.pk).exists():
        return Response({"detail": "권한이 없습니다."})

    if request.method == "PUT":
        serializer = ArticleSerializer(article, data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data)
    else:
        article.delete()
        return Response({"id": pk})

# 맨 밑에 추가
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def comment_article_reverse(request, pk, comment_pk):
    comment = get_object_or_404(Comment, pk=comment_pk)

    return Response(ArticleSerializer(comment.article).data)                            # 이부분

보면 comment에서는 comment.article 이렇게 가져오고 article에서는 article.comments.all() 이렇게 가져온다.

1:N관계이기 때문에 연결관계를 생각하면 왜 그런지 알 수 있다.

그리고 여러 데이터를 보낼 때 새로운 딕셔너리로 감싸서 보낼 수 있는 것을 확인할 수 있다.

ManyToManyField

models.py

class Subscriber(models.Model):
    name = models.TextField()

    def __str__(self):
        return f'{self.pk}번 구독자 {self.name}'


class Channel(models.Model):
    title = models.TextField()
    subscribers = models.ManyToManyField(Doctor, related_name='channels')
    def __str__(self):
        return f'{self.pk}번 채널 {self.title}'

모델에서 ManyToManyField를 설정하는 것이 생각보다 쉽다. 원래라면 Subscriber와 Channel사이에 중계테이블이 있어야 한다. 하지만 장고에서는 ManyToMany를 통해서 자동으로 만들어 주기 때문에 신경 쓰지 않아도 된다. 이렇게 설정 후에 ForiegnKey에서 사용했던 체이닝을 통해 가져오면 된다.

ex) channel.subscribers.all()

레퍼런스

장고 공식문서

https://docs.djangoproject.com/ko/3.2/intro/

장고 필터

https://www.bedjango.com/blog/how-use-django-filter/

필터 체이닝 주의점

https://www.hacksoft.io/blog/django-filter-chaining

User에서 에러 날 경우나 다른 DB와 충돌할 경우

  • First of all comment out/undo AUTH_USER_MODEL in settings.py if you had changed that.
  • Secondly comment out/undo your django app that contains new AUTH_MODEL from INSTALLED_APPS in settings.py.
  • Now you should be able to undo migrations for auth, admin, contenttypes and sessions as:
python manage.py migrate admin zero
python manage.py migrate auth zero
python manage.py migrate contenttypes zero
python manage.py migrate sessions zero

DRF

https://www.django-rest-framework.org/

DRF_auth 설정

https://stackoverflow.com/questions/38623002/how-do-i-login-to-the-django-rest-browsable-api-when-i-have-a-custom-auth-model

반응형

'Study > CSSU' 카테고리의 다른 글

[CSSU] SQL 정리  (0) 2021.06.25
[CSSU] JWT(JSON Web Token)  (0) 2021.05.07
[CSSU] HTTP request Methods  (0) 2021.04.07