React, DRF API를 이용한 velog 따라 만들어보기 2장

大 炫 ·2020년 9월 10일
8

Velog Clone

목록 보기
2/8
post-thumbnail

시작하기 앞서

이번에는 DRF의 JWT(JSON Web Token)의 인증방식을 통해 로그인과 회원가입 , (참조URL)
user의 모델정보에 profile을 확장해서 추가하는법 , (참조URL)
profile을 확장하는 중 ImageField사용법과 front에서 FormData()의 활용 , (참조URL)
전체적인 backend의 세팅이 끝났다면 front로 가져오는 useEffect의 활용 , (참조URL)
또는 backend로 데이터를 보내는법 fech의 활용 , (참조URL)
그리고 선택사항으로 oauth의 react-google-login을 활용한 소셜로그인까지 (참조URL)
전체적인 코드를 많이 참고한 git의 (참조URL)

상당히 많은 도움이 된 곳들 위주로만 넣었는데 꼭 읽어보길 바란다.

backend

세팅

1장에서 생성한 리액트폴더인 frontend와 나란하게 우리는 backend라는 폴더를 만들자
그 후 안에 python가상환경을 실행할건데(python이 깔려있다는 전제)

python3 -m venv env(가상환경이름)
(가상환경이름)env\Scripts\activate

을 통한 가상환경 설치 및 실행까지 시켰다면 Django를 설치하고 필요한 프레임워크, 라이브러리를 빠르게 설치해보겠다.

python -m pip install --upgrade pip
pip install django
pip install djangorestframework
pip install djangorestframework-jwt
pip install django-cors-headers
pip install Pillow

각각 토큰발행을 위한 DRF의 djangorestframework-jwt,
리액트의 http://localhost:3000/ Django의 http://localhost:8000/ 각각의 다른 서버를 연결하는 django-cors-headers,
ImageField를 쓰기위한 Pillow 까지
설치를 마쳤다면 startproject ,startapp 을 통해 api와 api로 호출시킬 app을 각각 만들겠다.

django-admin startproject api .
python manage.py startapp user

그리고 따로 DB를 설정하지 않았기 때문에 sqlite파일을 생성하도록 아래 명령어를 실행하겠다.

python manage.py migrate

여기까지 잘 따라왔다면 backend의 폴더 구성은 아래와 같다 !

이제 추가해준 항목들을 api/setting.py 파일을 사용할 수 있게 수정과 추가를 해보겠다.

backend/api/settings.py

"""
Django settings for api project.

Generated by 'django-admin startproject' using Django 3.1.1.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path
import datetime #56에서 시작되는 JWT_AUTH 의 토큰 유효기간을 설정하기 위한 datetime import
import os # 아래에 작성한 image가 추가될 때 경로를 설정해주기 위한 os import 

BASE_DIR = Path(__file__).resolve().parent.parent

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') #개발자가 관리하는 파일들 

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #사용자가 업로드한 파일 관리

# Build paths inside the project like this: BASE_DIR / 'subdir'.



# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xt!de@_*w0!y*@x!bqsb6%zr*tmhh+z2qjoaqwru4vq1@-d-fa'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

APPEND_SLASH = False # 추가 안해줄시 기본값이 True인데 그 경우 urls.py에서 경로설정시 주소 끝에 /를 붙이고 
#해당경로로 /를 붙이지 않고 접속시 페이지를 찾을 수 없기때문에 리다이렉트를 시켜 자동으로 /를 붙여서 경로를 찾는다.
#이 경우 문제가 될 수 있기때문에 false로 값을 지정해줬다.

CORS_ORIGIN_WHITELIST = ['http://localhost:3000'] #아까 설치한 corsheaders로 해당 서버와 연결할 서버의 url을 작성해준모습

# Application definition

REST_FRAMEWORK = { # 추가
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',  #인증된 회원만 액세스 허용
        'rest_framework.permissions.AllowAny',         #모든 회원 액세스 허용
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': ( #api가 실행됬을 때 인증할 클래스를 정의해주는데 우리는 JWT를 쓰기로 했으니
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication', #이와 같이 추가해준 모습이다.
    ),
}

JWT_AUTH = { # 추가
   'JWT_SECRET_KEY': SECRET_KEY,
   'JWT_ALGORITHM': 'HS256',
   'JWT_VERIFY_EXPIRATION' : True, #토큰검증
   'JWT_ALLOW_REFRESH': True, #유효기간이 지나면 새로운 토큰반환의 refresh
   'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=30),  # Access Token의 만료 시간
   'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=3), # Refresh Token의 만료 시간
   'JWT_RESPONSE_PAYLOAD_HANDLER': 'api.custom_responses.my_jwt_response_handler'
}

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user', # 추가
    'rest_framework', # 추가
    'rest_framework_jwt', # 추가
    'corsheaders', # 추가
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',     # 추가
    'django.middleware.common.CommonMiddleware', # 추가
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'api.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'api.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'Asia/Seoul' # 후에 작성한 게시물에 날짜를 표현하기 위해서 
USE_TZ = False  #Use_TZ를 false로 설정해서 우리나라 시간을 가져온 모습이다.

USE_I18N = True

USE_L10N = True




# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'

중간중간 추가해준 사항에 대한 주석을 달아놨으니 해당사항은 주석을 통해 확인하면 좋겠다.
여기까지 완료하고 runserver를 해주어 문제없이 실행되는 모습이 나와야만 얼추 setting에 관한 추가사항은 끝이라고해도 무방하다 !

python manage.py runserver

문제없이 실행되어 서버에 방문한 모습

이제 우리는 아까 만든 app인 user를 통해 token을 발급받는 user의 모델과 그 모델인 user의 profile을 확장하는 모델을 backend/user/models.py를 통해 정의해보도록 하겠다.

backend/user/models.py

from django.db import models

# Create your models here.
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    user_pk = models.IntegerField(blank=True)
    email = models.EmailField(max_length=500, blank=True)
    mygit = models.CharField(max_length=50, blank=True)
    nickname = models.CharField(max_length=200, blank=True)
    photo = models.ImageField(upload_to="profile/image", default='red.jpg')
    myInfo = models.CharField(max_length=150, blank=True)

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance, user_pk=instance.id)


@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

조금만 보아도 우리는 user의 profile을 설정하는 class Profile을 볼 수는 있지만 user의 모델을 username, password 등으로 정의한 model은 찾아볼 수 없다 !
이는 user의 모델의 경우

from django.contrib.auth.models import User

django의 내장 auth model로 부터 정의된 User를 Import 시켰기 때문에 따로 작성안해준 이유이다 !

그리고 User를 Ctrl + 마우스클릭 을통해 User의 정의를 들어가보면

class User(AbstractUser):

Class User를 확인할 수 있는데 여기서 또 Abstract를 확인할 수 있다. 전에 Java에서 상속개념을 배울 때 추상의 Abstract를 배웠는데

추상 클래스는 상속을 강제하기 위한 것이다. 즉 부모 클래스에는 메소드의 시그니처만 정의해놓고 그 메소드의 실제 동작 방법은 이 메소드를 상속 받은 하위 클래스의 책임으로 위임하고 있다.
-생활코딩-

즉 User를 정의해놨지만 이를 상속받을 user Class를 니 맛대로 다양하게 활용은 니가해라 ~
라는 개념으로 이해하면 편할 듯 하다.

모델을 정의할 때 쓰는 Field의 경우 많은 종류가 있는데 이는 이곳에서 참조하고 더 필요한 경우 구글링을 권장한다.
특이점의 경우 OneToOneField가 있는데 이처럼 1대1 방식이 있는 반면 다른 다양한 방식도 존재하더라.
ImageField의 경우 default='red.jpg' 으로 설정해준것은 user를 생성할 때 profile에 기본이미지로 red.jpg를 선택한 것 이고 해당 이미지의 경로는 backend/media/red.jpg로 이미지를 해당경로에 위치시킨다면 user를 생성할 때 기본이미지가 적용되는 것을 확인 할 수 있을 것이다.
(이미지를 back에서 업로드 또는 front에서 업로드 해주면 media폴더자동으로 생성되는데 아직 아무것도 안했기때문에 직접 생성해 주었다.)

이제 모델을 정의했으면 우리는 DB에 table을 만들고 적용을 시켜야한다.
이 때 makemigrations, migrate가 등장한다.

python manage.py makemigrations
python manage.py migrate

정확한 표현은 아니지만 table을 만드는 과정을 makemigrations, 만든것을 적용하는 과정을 migrate 라고 이해하면 편할 듯 하다.
정확한 의미가 궁금하다면 늘 검색해보길 권장한다.

명령어를 입력하고나면 폴더의 구성은 아래와 같다

따로 만들지 않아도 user/migrations/0001_initial.py 가 생성된 것을 확인할 수 있으며 model정의시 기본키를 따로 주지 않았기때문에 id가 추가된것을 확인 할 수 있다!
항상 모델을 설정한 뒤 즉시 makemigrations, migrate를 해주는게 정신건강에 이로울 것이다

여기까지 하고 admin.py를 설정해주면 우리가 지금껏 한 것들을 눈으로 확인 할 수 있다!!

backend/user/admin.py

from django.contrib import admin

# Register your models here.
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from .models import Profile


class ProfileInline(admin.StackedInline):
    model = Profile
    can_delete = False
    verbose_name_plural = "profile"


class UserAdmin(BaseUserAdmin):
    inlines = (ProfileInline,)


admin.site.unregister(User)
admin.site.register(User, UserAdmin)

이렇게만 설정해도 우리는 backend/api/urls.py의 기본적으로 amdin페이지의 url을 정의해 놓은

urlpatterns = [
    path('admin/', admin.site.urls),
]

덕분에 http://localhost:8000/admin/ 접속시 관리자페이지로 접속할 수 있다 !
username과 password는

python manage.py createsuperuser

라는 실행명령어를 통해 username, email , password를 정하고 superuser를 생성할 수 있다
이제 다시

python manage.py runserver

을통해 서버를 실행시키고 admin페이지로 이동한 뒤 설정한 superuser의 username과 password를 입력하면 우리는 관리자같은 아래와 같은 페이지로 접속이 가능하다 !

admin 접속시 우리가 생성한 superuser의 1대1방식으로 생성한 profile까지 함께 볼 수 있는 수정도 가능한 db의 workbench같은 GUI를 확인할 수 있다.

이제 우리는 back과 front를 연결시킬 준비를 해야한다. 때문에 아직 세팅해줘야 할 파일이 더 남았으니
이렇게 생성된 model을 front와 주고받을 때 서로 다른 언어를 쓰기때문에 서로가 이해할 수 있는 언어로 변환을 해줘야한다. 이 때 쓰이는것이 바로 serializers.py 사전적의미로는 직렬변환기이다.
그리고 그것이 해당하는 페이지에서 어떤식으로 보여지는 views.py 마지막으로 views의 url을 정의하는것이 바로 urls.py
여기까지하면 기본적인 django가 front와 주고받을 준비를 마친것이라고 봐도 무방하다 !

backend/user/serializers.py (직접 작성해줘야 한다)

from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
from django.contrib.auth.models import User
from .models import Profile

class UserSerializer(serializers.ModelSerializer):

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


class UserSerializerWithToken(serializers.ModelSerializer):

    token = serializers.SerializerMethodField()
    password = serializers.CharField(write_only=True)

    def get_token(self, obj):
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

        payload = jwt_payload_handler(obj)
        token = jwt_encode_handler(payload)
        return token

    def create(self, validated_data):
        password = validated_data.pop('password', None)
        instance = self.Meta.model(**validated_data)
        if password is not None:
            instance.set_password(password)
        instance.save()
        return instance

    class Meta:
        model = User
        fields = ('token', 'username', 'first_name', 'last_name', 'email', 'password')

#profile 
class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ('id', 'user_pk', 'email', 'nickname', 'user', 'photo', 'mygit', 'myInfo')

따로 이코드에 대해서는 공부하지 않았는데 좀 읽다보면 대략적인 그림은 get_token할 때 jwt이벤트 헨들러같은것을 정의해준거같고 매개변수로는 front에서 get_token할 수 있도록 self에는 get할 수 있는 해당접속url의 경로, obj는 토큰을 얻을 유저의 정보(username이나 password)라고 분석했다. 또 create_token을 할 때 validated_data는 username이나 password를 통해 확인된 user에게 token을 만들어 줘야하기때문에 validated_data라는 객체를 매개변수로 받는다는 듯 했다.

views.py

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib.auth.models import User
from rest_framework import permissions, status, generics
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer, UserSerializerWithToken, ProfileSerializer
from .models import Profile

# from google.oauth2 import id_token
# from google.auth.transport import requests


@api_view(['GET'])
def current_user(request):

    serializer = UserSerializer(request.user)
    return Response(serializer.data)


class UserList(APIView):

    permission_classes = (permissions.AllowAny,)

    def post(self, request, format=None):
        serializer = UserSerializerWithToken(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class ProfileUpdateAPI(generics.UpdateAPIView):
    lookup_field = "user_pk"
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

여지껏 많은 DRF의 import가 있었지만 views.py가 압도적으로 DRF의 혜택을 많이 받는 녀석이라는 사실이 코드에서 드러난다.

from rest_framework import permissions, status, generics
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView

등등 만약 DRF가 없었다면 우리는 create, get, post, delete 등등 serializer부터 쭉 하나하나 class, method들을 정의해줬어야 할 텐데 엄청난 특권을 누리고있다는 생각이 새삼 든다..
아직 back과 front사이에서 확실한 선택을 못했지만 back으로 전향하게된다면 나는 DRF없이 고생해서 작성하는 serializer와 view들을 작성해보는 경험(사서고생하는)들을 충분히 해보고싶다는 생각도 있다 !

backend/user/urls.py (직접작성)

from django.urls import path
from .views import current_user, UserList, ProfileUpdateAPI

urlpatterns = [
    path('', UserList.as_view()),
    path('current/', current_user),
    path("auth/profile/<int:user_pk>/update/", ProfileUpdateAPI.as_view()),
]

backend/api/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token
from .views import validate_jwt_token
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path("admin/", admin.site.urls),

    path('validate/', validate_jwt_token),
    path('login/', obtain_jwt_token),
    
    path('verify/', verify_jwt_token),
    path('refresh/', refresh_jwt_token),
    
    path('user/', include('user.urls')),
]
urlpatterns += \
    static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

과거에는 path경로를 쓰지않고 api/urls.py에 url을 직접 전부 입력하는 방식을 쓰더라. 지금은 path에서 include('user.urls')을 통해 user라는 app안에서 urls.py를 다시 작성함으로써 user가아닌 다른 app을 생성할 시 api/urls.py를 훨씬 직관적으로 볼 수 있다는 사실을 확인할 수 있다 !
url 코드가 궁금하다면 찾아보길 바란다 !

아 그리고 작성완료시 터미널창에서

ImportError: Could not import 'api.custom_responses.my_jwt_response_handler' for API setting 'JWT_RESPONSE_PAYLOAD_HANDLER'. ModuleNotFoundError: No module named 'api.custom_responses'.

와같은 에러를 뿜어내는데 여기서

backend/api/views.py (직접작성)

from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
# from ./user/serializers import UserSerializer

@api_view(['GET'])
def validate_jwt_token(request):

    try:
        token = request.META['HTTP_AUTHORIZATION']
        data = {'token': token.split()[1]}
        valid_data = VerifyJSONWebTokenSerializer().validate(data)
    except Exception as e:
        return Response(e)

    return Response(status=status.HTTP_200_OK)

backend/api/custom_responses.py (직접작성)

from user.serializers import UserSerializer

def my_jwt_response_handler(token, user=None, request=None):
    return {
        'token': token,
        'user': UserSerializer(user, context={'request': request}).data
    }

위 코드들을 추가해준다면 아까 터미널에서 No module named 'api.custom_responses' 하던 에러가 사라지는것을 볼 수 있다.(정확히 따라했는데도 에러가난다면 가끔 적용이 느리게 될 때도 있으니 우선 서버를 종료시켰다가 다시 실행시켜보는 것을 권장한다!)

서버가 에러없이 잘 실행된다면 back에서 로그인과 회원가입을 할 준비를 완벽하게 끝냈다.
마지막으로 우리가 지정해준 url로 들어가보자.

당황하지말자. 우리는 http://localhost:8000 , api/urls.py에서 ""빈문자열에 해당하는 url을 정의한적이 없기때문에 페이지를 찾을 수 없다는 404페이지가 당연하다 !
아래 api/urls.py에서 정의한 url 패턴을 보여주는데 그중 우리는 user라는 페이지에 접속해보자.

user list를 볼 수 있는 /user/페이지에 들어왔지만 유저목록이 보이지않는다. 빨간색으로 된 줄을 읽어보면

"detail": "Method \"GET\" not allowed."

즉 user list들을 볼 수있는 get메서드가 허용되지 않는다는 내용이다.

Allow: POST

허용된 메서드는 POST, 즉 유저리스트에 유저를 추가시키는 것은 허용된다는 소리다 !
어찌보면 당연한 소리다. 관리자를 제외하고는 user list를 볼 수 없는게 맞기때문에 아예 get 메서드를 허용하지 않았고 이는 우리가 아까 작성한 djnago의 AbstractUser, 거기에 token을 발급, 소멸, 재발급 의 기능을 가진 DRF jwt의 기능을 가져다 쓰면서 이런 설정이 가능하게 된것이다.
그럼 우리가 만든 프로필은 볼 수 있을까 ? 프로필에 해당하는 url로 접속해보겠다 !
superuser에 해당하는 user_pk가 1임을 우리는 admin페이지에서 확인할 수 있었고 user_pk에 해당하는 1을 넣어서 접속해 보았다.

http://localhost:8000/user/auth/profile/1/update/

superuser에 해당하는 user_pk가 1임을 우리는 admin페이지에서 확인할 수 있었고 user_pk에 해당하는 1을 넣어서 접속해 보았다.

profile을 보려고 접속했지만 권한을 확인할 수 없다는 문구가 나온다.
superuser의 프로필이라서 안되는 것인가 ? 그렇다면 amdin으로 접속해서 user를 add해서 test1이라는 일반유저를 생성해 보고 user_pk가 id값과 일치하는 2를 확인하고 해당경로로 다시 접속해보겠다.


역시 권한이 없다고 나오는데 jwt를 사용할때 우리는 토큰을 인증받아야 자신의 user정보를 열람할 수 있는 권한을 부여한다고 설정했었다. 그래서 토큰이 없는 지금은 어떤 유저의 정보도 열람할 수 없는것이고 1대1로 생성된 userProfile또한 열람할 수 없는 것이다 !
이 권한을 views.py에서 권한없이도 열람할 수 있는 코드를 추가할수도 있지만 이는 3~4장에서 다룰 예정이니까 참고바란다 !
마지막으로 모든 과정을 따라했다면 backend의 폴더 구성은 다음과 같다.

2장 마치며

여기까지가 2장의, DRF jwt를 이용한 back의 세팅이었고 다음 3장에서는 데이터 요청과 데이터를 보내는 2장의 처음에서 참조URL에서 다룬 useEffect, fetch, then 등 react의 문법이 주가 될것이고, 선택사항으로 소셜로그인기능인 react-google-login까지 알아볼 예정이다 !
만약 에러가 난다면 에러메세지404, 401, 500페이지 에러등을 잘 읽고 구글링한다면 충분히 무난하게 여기까지 올 수 있을것이라 생각한다 !

profile
대현

0개의 댓글