Django 프로젝트02 - 리액트와 SPA 방식으로 인스타그램 만들기(5)

|

에듀캐스트 장고&리액트 강의를 듣고 정리하는 글이다.

간단한 회원가입 API 만들기

serializers

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

User = get_user_model()

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

    def create(self, validated_data):
        user = User.objects.create(username=validated_data['username'])
        user.set_password(validated_data['password'])
        user.save()
        return user

    class Meta:
        model = User
        fields = ['pk', 'username', 'password']

views

from django.contrib.auth import get_user_model
from django.shortcuts import render
from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView
from .serializers import SignupSeriallizer


class SignupView(CreateAPIView):
    model = get_user_model()
    serializer_class = SignupSeriallizer
    permission_classes = [
        AllowAny,
    ]

200817-200823_TIL

|

8월 20일 (목)

  • 강의를 마저 듣고 있다. 80퍼센트 이상 들으면 6개월 추가 쿠폰준다던데 받았다. 나름의 성취감을 느꼈다.

8월 21일 (금)

  • 친구랑 하는 프로젝트에서 상품 리스트 이미지 사이즈에 대해서 flex를 이용해서 해결해보았다.

8월 22일 (토)

  • 블로그 좀 꾸며보려고 보는데, 사이드메뉴가 어떻게 작동하는지 궁금해서 보았는데, 가상 클래스를 이용해서 활용하는거 같았다. 그렇게 가상 클래스에 알아보다가 선택자들 좀 공부해 보려고 해당 사이트에 들어가서 공부해 보았다.

8월 23일 (일)

  • 강의도 듣고, 동국 푸드를 조금 리뉴얼 중이다. 보기 좋은 떡이 먹기도 좋다는 말이 있듯이 좀더 예쁘게 꾸며볼려고 하는 것이다. 참고 사이트로는 망고플레이트를 참고하려고 한다.

Django 프로젝트02 - 리액트와 SPA 방식으로 인스타그램 만들기(4)

|

에듀캐스트 장고&리액트 강의를 듣고 정리하는 글이다.

CSS Grid와 Ant Design을 활용한 레이아웃

앤트 디자인 설치

yarn add antd

Django 프로젝트02 - 리액트와 SPA 방식으로 인스타그램 만들기(3)

|

에듀캐스트 장고&리액트 강의를 듣고 정리하는 글이다.

react-router-dom을 활용한 라우팅 처리

아래를 설치해주자.

yarn add react-router-dom

리액트의 src폴더 밑에 pages와 components 폴더를 생성해줄건데, pages에는 라우팅을 처리하는 컴포넌트를 넣고 components에는 위의 내용들을 표현해줄 컴포넌트들을 만들어 줄 것이다.

최상위 index.js를 다음과 같이 수정

import React from "react";
import ReactDOM from "react-dom";
import {BrowserRouter} from "react-router-dom";
import "./index.css";
import Root from "pages";

ReactDOM.render(
  <BrowserRouter>
    <Root />
  </BrowserRouter>,
  document.getElementById("root")
);

BrowserRouter로 Root컴포넌트를 감싸줘서 그 밑에서 Route를 이용할 수 있도록 해준다.

pages/index.js를 작성

컴포넌트를 활용해 django의 urls와 최대한 비슷하게 구현해 준다. exact를 안하게 될시, 일치하는 것들이 모든것들이 표시되기에 해주는 것이 좋다. ```javascript import React from "react"; import { Route } from "react-router-dom"; import AppLayout from "components/AppLayout"; import Home from "./Home" import About from "./About" import AccountsRoutes from "./accounts" function Root() { return 최상위 컴포넌트 <Route exact path="/" component={Home} /> <Route exact path="/about" component={About} /> <Route path="/accounts" component={AccountsRoutes} /> ; } export default Root; ``` #### accounts/index.js 함수안에 {match}라는 것이 보일텐데, 상위에서 일치한 url을 갖고와줄 수 있다. ```javascript import React from "react"; import { Route } from "react-router-dom"; import Profile from "./Profile"; import Login from "./Login"; function Routes({match}) { return ( <> <Route exact path={match.url + "/profile"} component={Profile} /> <Route exact path={match.url + "/login"} component={Login} /> </> ); } export default Routes; ```

Django 프로젝트02 - 리액트와 SPA 방식으로 인스타그램 만들기(2)

|

에듀캐스트 장고&리액트 강의를 듣고 정리하는 글이다.

포스팅 목록 API를 구현하고 리액트에서 받아서 표현하기

django 설정

앱을 설치해 후자 python manage.py startapp instagram python manage.py startapp accounts

accounts 설정

accounts.models 파일에 다음과 같이 입력해준다.

from django.db import models
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.mail import send_mail
from django.core.validators import RegexValidator
from django.template.loader import render_to_string
from django.shortcuts import resolve_url


class User(AbstractUser):
    class GenderChoices(models.TextChoices):
        MALE = "M", "남성"
        FEMALE = "F", "여성"

    follower_set = models.ManyToManyField("self", blank=True)
    following_set = models.ManyToManyField("self", blank=True)

    website_url = models.URLField(blank=True)
    bio = models.TextField(blank=True)
    phone_number = models.CharField(
        max_length=13,
        blank=True,
        validators=[RegexValidator(r"^010-?[1-9]\d{3}-?\d{4}$")],
    )
    gender = models.CharField(max_length=1, blank=True, choices=GenderChoices.choices)
    avatar = models.ImageField(
        blank=True,
        upload_to="accounts/avatar/%Y/%m/%d",
        help_text="48px * 48px 크기의 png/jpg 파일을 업로드 해주세요",
    )

    @property
    def name(self):
        return f"{self.first_name} {self.last_name}"

    @property
    def avatar_url(self):
        if self.avatar:
            return self.avatar.url
        else:
            return resolve_url("pydenticon_image", self.username)

    def send_welcome_email(self):
        subject = render_to_string(
            "accounts/welcome_eail_subject.txt", {"user": self,}
        )
        content = render_to_string(
            "accounts/welcome_email_content.txt", {"user": self,}
        )
        sender_email = settings.WELCOME_EMAIL_SENDER
        send_mail(subject, content, sender_email, [self.email], fail_silently=False)


이후 settings파일에 ‘AUTH_USER_MODEL = accounts.User’를 추가해주고 makemigrations와 migrate를 입력해준다.

admin 파일에 아래와 같이 입력해서 admin 페이지에서도 보이게 해준다.

from django.contrib import admin
from .models import User


@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    pass

instagram 설정

import re
from django.conf import settings
from django.db import models
from django.urls import reverse


class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Post(TimeStampedModel):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='my_post_set',
                               on_delete=models.CASCADE)
    photo = models.ImageField(upload_to='instagram/post/%Y/%m/%d')
    caption = models.CharField(max_length=500)
    tag_set = models.ManyToManyField('Tag', blank=True)
    location = models.CharField(max_length=100)
    like_user_set = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True,
                                           related_name='like_post_set')

    def __str__(self):
        return self.caption

    def extract_tag_list(self):
        tag_name_list = re.findall(r"#([a-zA-Z\dㄱ-힣]+)", self.caption)
        tag_list =  []
        for tag_name in tag_name_list:
            tag, _ = Tag.objects.get_or_create(name=tag_name)
            tag_list.append(tag)
        return tag_list

    def get_absolute_url(self):
        return reverse("instagram:post_detail", args=[self.pk])

    def is_like_user(self, user):
        return self.like_user_set.filter(pk=user.pk).exists()

    class Meta:
        ordering = ["-id"]


class Comment(TimeStampedModel):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    message = models.TextField()

    class Meta:
        ordering = ["-id"]


class Tag(TimeStampedModel):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name
  • admin 설정 mark_safe를 통해서 실제 태그를 삽입할 수 있다. 단, 주의할 점으로 함부로 썼다가 누군가가 올린 스크립트문이 실행될 수도 있으니 주의하면서 써야한다. ```python from django.contrib import admin from django.utils.safestring import mark_safe

from .models import Post, Comment, Tag

@admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = [“photo_tag”, “caption”] list_display_links = [“caption”]

def photo_tag(self, post):
    return mark_safe(f"<img src={post.photo.url} style='width: 100px;'/>")

@admin.register(Comment) class CommentAdmin(admin.ModelAdmin): pass

@admin.register(Tag) class TagAdmin(admin.ModelAdmin): pass


## rest_framwwork 사용
이후 rest framework를 설치해서 사용해보도록 하겠다.
> pip install djangorestframework

settings 파일에 rest_framework의 이용은 인증된 유저만 사용할 수 있게 설정해준다.
```python
# 아래 내용 추가
REST_FRAMEWORK = {
    'DEFAULT_PERMission_CLASSES': ["rest_framework.permissions.IsAuthenticated",]
}
# instagram/views.py
from django.shortcuts import render
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from .models import Post
from .serializers import PostSerializer


class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [AllowAny]  # FIXME: 인증 적용


# instagram/serializers
from rest_framework import serializers
from .models import Post


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = "__all__"


# instagram/urls
# DRF의 URL을 등록해준다.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views


router = DefaultRouter()
router.register('posts', views.PostViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]        

react 사용(with axios)

일단 axios를 설치해주자.

yarn add axios

그리고 최상단 폴더에 jsconfig.json이라는 파일을 생성해서 아래처럼 입력해주자. 절대 경로 지정해주는거 같은데 있으면 편하다 한다.

{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

PostList.js 파일을 만들어 아래 처럼 입력해보고 App.js에 등록해주고 실행해보자.

import React, { useEffect } from "react";
import Axios from "axios";
const apiUrl = "http://localhost:8000/api/posts/";

function PostList() {
  // mount 되었을 때 한 번 실행
  useEffect(() => {
    Axios.get(apiUrl)
      .then((response) => {
          console.log("loaded response:", response)
      })
      .catch((error) => {
        // error.response;
      });
    console.log("mounted");
  }, []);
  return (
    <div>
      <h1>PostList</h1>
    </div>
  );
}

export default PostList;

그러면 다음과 같은 에러를 만나볼 수 있다. 이것은 CORS에러인데 서로 다른 호스트로부터 요청이 들어와서 그런 것이다. 같은 localhost인데? 할수도 있지만 포트까지 치면 엄연히 다른 호스트이다.

Access to XMLHttpRequest at ‘http://localhost:8000/api/posts/’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

해결법으로 일단 pip install django-cors-headers를 설치해주고 settings에 아래 내용을 추가해주자.

INSTALLED_APPS = [
    'corsheaders',
]
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
]
CORS_ORIGIN_WHITELIST = ["http://localhost:3000"]

이후 다시 js 파일들을 작성해주자. map같이 순회하는 경우에는 컴포넌트들을 구분해주기 위해 식별할 수 있는key를 넣어주어야 한다. 아래의 예시에서는 post.id를 넣었다.

// PostList.js
import React, { useEffect, useState } from "react";
import Axios from "axios";
import Post from "Post";

const apiUrl = "http://localhost:8000/api/posts/";

function PostList() {
  const [postList, setPostList] = useState([]);
  useEffect(() => {
    Axios.get(apiUrl)
      .then((response) => {
        const { data } = response;
        console.log("loaded response:", response);
        setPostList(data);
      })
      .catch((error) => {
        // error.response;
      });
    console.log("mounted");
  }, []);
  return (
    <div>
      <h2>PostList</h2>
      {postList.map((post) => (
        <Post post={post} key={post.id} />
      ))}
    </div>
  );
}

export default PostList;

// Post.js
import React from "react";

function Post({ post }) {
  const { caption, location, photo } = post;
  return (
    <div>
      <img src={photo} alt={caption} style= />
      {caption}, {location}
    </div>
  );
}

export default Post;