본문 바로가기

TIL

23.05.15 TIL

팀 프로젝트의 발표 준비 및 문서 최신화, 피드백 받기,KPT회고작성을 하였다.

1. 문서 최신화

ERD:

User와 Article간에 좋아요, 북마크 관계가 추가되었고, 유저간의 팔로우 관계가 many to many로 추가되었다.

작성자와 작성된 글을 필드로 가지는 Comment 테이블도 추가되었다.

API:
API 명세서

[API 명세서

A new tool for teams & individuals that blends everyday work apps into one.

www.notion.so](https://www.notion.so/f40bf5bef3a94a21a9df6588f21e39c2)

북마크 기능등 추가된 기능들에 대한 API문서가 최신화되어 결과보고서에 반영되었다.

2. 기능 구현 결과 정리

체크리스트

  • DRF 사용 - 프론트/백 엔드 분리 구현
    • 프론트엔드 레포지토리와 백엔드 레포지토리를 분리해서 두가지 레포지토리를 사용해야 합니다.
  • 프론트엔드
    • 자바스크립트의 fetch를 이용해서 백엔드와 restful하게 통신해주세요
  • 회원기능
  • CRUD 기능
    • 피드 페이지
    • 게시글 작성 페이지
    • 상세 게시글 페이지
    • 마이 페이지
    • 댓글 기능
    • 좋아요, 북마크 기능
    • 팔로우, 팔로워 기능
    • 팔로우한 상대 게시물 확인 기능
    • 프로필에 사진 업로드 기능
    • 많아지는 게시글을 나눠보기 위한 페이지 기능(pagination)
    • 회원가입시 이메일 인증 기능 추가
    • 비밀 번호 찾기 기능
  • 배포
    • 프론트엔드 : AWS S3
    • 백엔드 : AWS EC2

백엔드(추가 기능)

  • 배포된 백엔드에 로드밸런서를 적용해보세요
  • https를 적용해보세요
  • 배포된 프론트엔드와 백엔드에게 도메인을 주세요. 이때 백엔드에겐 api.~~~.com 을 주고 프론트에~~~.com을 주는 것이 좋습니다.

기능 정리

모델

class User(AbstractBaseUser):
    username = models.CharField(
        max_length=32,
        unique=True,
    )
    email = models.EmailField(
        verbose_name="email address",
        max_length=255,
        unique=True,
    )
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    followings = models.ManyToManyField(
        "self",
        symmetrical=False,
        related_name="followers",
        blank=True,
    )

유저모델이다. username과 password로 인증한다.

email 인증, 비밀번호 찾기 기능을 위해 email 필드도 추가하였으나 해당 기능은 완성하지 못했다.

class Profile(models.Model):
    """
    프로필 모델입니다.
    이미지와 bio, 작성시간과 수정시간을 필드로 가진다.
    User와 1대1관계
    """

    class Meta:
        db_table = "profile"

    username = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
    image = models.ImageField(blank=True, upload_to="%Y/%m/")
    bio = models.TextField(blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

유저의 자기소개 정보를 위한 프로필 모델이다. 프로필 이미지 추가가 가능하다.

class Article(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=30)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    likes = models.ManyToManyField(User, related_name="like_articles")
    bookmarks = models.ManyToManyField(User, related_name="bookmarked_articles")

    def __str__(self):
        return str(self.title)

작성글 모델이다. 좋아요, 북마크 기능을 ManyToMany로 구현하였다.

class Comment(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comment_set")
    content = models.TextField(max_length=50)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


    def __str__(self):
       return str(self.content)

댓글 기능을 위한 모델이다.

주요기능 소개(뷰/시리얼라이저/js)

회원가입

class UserSerializer(serializers.ModelSerializer):
    """
    유저를 생성하거나 유저정보를 조회하기 위해 쓰이는 serializer이다.
    생성할 땐 password,password2,username,email을 받고
    조회할땐 username, email을 표시한다.

    """

    # 입력할 때만 쓰이는 password2 시리얼라이저필드 새로 정의
    password2 = serializers.CharField(style={"input_type": "password"}, write_only=True)

    class Meta:
        model = User
        fields = "__all__"
        extra_kwargs = {
            # write_only : 해당 필드를 쓰기 전용으로 만들어 준다.
            # 쓰기 전용으로 설정 된 필드는 직렬화 된 데이터에서 보여지지 않는다.
            "password": {"write_only": True},  # default : False
        }

    def create(self, validated_data):
        """
        부모 클래스의 create를 오버라이딩한다.
        검증이후 실행됨
        """
        # 저장할 때는 모델에 password2 필드가 없으므로 제거
        del validated_data["password2"]
        user = super().create(validated_data)
        user.set_password(validated_data["password"])  # 비밀번호 저장(해쉬)
        user.save()
        new_profile = Profile()
        new_profile.username = user
        new_profile.save()
        return user

    def validate(self, attrs):
        """
        부모 클래스의 validate 오버라이딩
        passwor와 password2가 일치하는지 확인
        이외 나머지 검증은 부모 클래스의 validate에 맡김
        """
        password_validation(attrs.get("password"), attrs.get("password2"))
        if not re.match(USERNAME_REGEX, attrs.get("username")):
            raise serializers.ValidationError(
                {
                    "username": "길이 6자리 ~32자리, 알파벳으로 시작하고 알파벳 대소문자와 숫자, 특수기호 -,_,@ 로 이루어져야합니다."
                }
            )
        return super().validate(attrs)

회원가입은 UserSerializer를 이용해 진행된다. UserSerializer는 User모델 시리얼라이저이며 create 메소드를 오버라이딩하여 User를 저장할 때 Profile도 생성하여 저장한다. 또한 validate를 오버라이딩하여 두 비밀번호 입력의 일치여부와 비밀번호의 유효성검사, username의 유효성 검사를 한다.

로그인

class MyTokenObtainSerializer(TokenObtainPairSerializer):
    """
    토큰 발급시 사용되는 serializer입니다.
    기본 제공되는 것을 상속하여 메소드 오버라이딩하여 사용합니다.
    email과 username을 payload에 담습니다.
    """

    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # Add custom claims
        token["username"] = user.username
        token["email"] = user.email

        return token

로그인은 DRF에서 제공하는 TokenObtainPairSerializer를 커스텀하여 구현하였다. 비밀번호와 username을 입력받으면 회원정보와 일치하는지 확인하고 access 토큰과 refresh 토큰을 제공한다.

로그인 후 로그인 유지(front,verify,refresh)

토큰로그인은 refresh token(유효기간 2일)이 유효한 기간동안 브라우저에 토큰이 남아있다면 다시 로그인 할 필요없이 access 토큰(유효기간 20분)이 갱신되어야 한다.

이를 위해 다음과 같은 js 로직이 작성되었다.

async function get_access_token() {
    const token = localStorage.getItem('access')
    if (token == null) {

        return null
    }
    else {
        const response = await fetch(`${backend_base_url}/user/verify/`, {
            method: "POST",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({ "token": token })
        })
        if (response.status == 200) {
            return token
        }
        else {
            return await refresh()
        }
    }
}

이 함수는 호출되었을 때 localstorage에 저장된 엑세스 토큰을 불러와 백엔드의 token verify API를 통해 토큰이 유효한지 검사한다. 유효하다면 해당 토큰을 그대로 반환하고 유효하지 않을 시 refresh 함수를 호출한다.

async function refresh() {
    const refresh_token = localStorage.getItem("refresh")
    if (refresh_token == null) {
        return null
    }
    const response = await fetch(`${backend_base_url}/user/refresh/`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
            "refresh": refresh_token
        })
    })
    if (response.status == 401) {
        alert("로그아웃되었습니다 다시 로그인해주세요")
        localStorage.removeItem("payload")
        localStorage.removeItem("access")
        localStorage.removeItem("refresh")
        window.location.href=`${frontend_base_url}`
        return null
    }
    const response_json = await response.json();
    const jsonPayload = await parse_payload(response_json.access);
    localStorage.setItem("payload", jsonPayload);
    localStorage.setItem("access", response_json.access);
    return localStorage.getItem("access")
}

refresh 함수는 refresh 토큰을 token refresh API로 보내 새 access 토큰을 받고 반환하거나, 유효한 토큰이 아닐 시 로그아웃(localstorage삭제)시킨다.

parse_payload함수는 access 토큰으로부터 payload를 파싱한다.

Article C.R.U.D.

class ArticleView(generics.ListCreateAPIView):
    """
    APIview에서는 페이지네이션 기능을 사용할 수 없었습니다, 그렇기에 generics를 사용했습니다.
    ListCreateAPIView는 GET요청일 때 조회, POST요청일 때 새로운 데이터를 생성하는 역할을 합니다.
    def get의 역할을 ListCreateAPIView에서 지원하기에 따로 지정해 줄 필요가 없습니다.
    queryset을 통해 article의 내용을 가져옵니다.
    필요한 것을 명시해주면 간단히 get 요청을 구현할 수 있습니다.
    order_by를 이용하여 최신 게시글을 가장 먼저 출력합니다.
    """

    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    paginations_class = ArticlePagination
    serializer_class = ArticleListSerializer
    queryset = Article.objects.all().order_by("-created_at")

    def post(self, request, *args, **kwargs):
        """
                CreateAPIView의 post를 오버라이딩 되었습니다.
        *args, **kwargs를 추가하여 오류를 예방하고
        코드의 유연성을 높였습니다.
        data 부분에는 게시글의 title,content를 받아 valid 작업을 받습니다.
        context에는 request요청을 한 유저의 정보를 받습니다. author_id를 저장하기 위해 사용되었습니다
        """
        serializer = ArticleCreateSerializer(
            data=request.data, context={"request": request}
        )
        if serializer.is_valid():
            serializer.save()
            return Response(
                {"message": "작성완료"},
                status=status.HTTP_201_CREATED,
            )
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

게시글의 작성과 전체조회는 pagination을 적용하기 위해 generics.ListCreateAPIView를 상속하여 사용하였다. 또한 order_by 메소드를 사용해 최신순으로 글을 정렬하였다. 작성의 경우 context 인자를 이용해 필드를serializer가 save 할 때 빈 필드가 생기지 않게 했다.

class ArticleDetailView(APIView):
    """
    ArticleDetailView에서는 게시글 상세보기, 게시글 수정, 게시글 삭제를 수행합니다.
    """

    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

    def get(self, request, article_id):
        article = Article.objects.get(id=article_id)
        serializer = ArticleSerializer(article)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request, article_id):
        article = Article.objects.get(id=article_id)
        serializer = ArticleCreateSerializer(article, data=request.data)
        self.check_object_permissions(self.request, article)
        if serializer.is_valid():
            serializer.save()
            return Response({"message": "수정완료"}, status=status.HTTP_200_OK)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, article_id):
        article = Article.objects.get(id=article_id)
        self.check_object_permissions(self.request, article)
        article.delete()
        return Response({"message": "삭제완료"}, status=status.HTTP_204_NO_CONTENT)

게시글 수정, 삭제의 경우 작성자의 요청만이 허용되어야 하므로 커스텀 permission을 적용하고 그것을 self.check_object_permissions로 불러와 작성자만 수정/삭제가 가능하도록 구현하였다.

아래는 IsOwnerOrReadOnly의 코드이다.

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    BasePermission을 상속
    게시글의 작성자가 request.user와 같은 경우 PUT,PATCH,DELETE 메서드에 대한 권한을 허용
    이외의 요청자일 경우 읽기 권한만 허용
    """

    message = "권한이 없습니다"

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Instance must have an attribute named `author`.
        return obj.author == request.user

시리얼라이저의 methodfield를 통해 article과 연결된 bookmark 유저수, like 유저수, 댓글 수를 함께 프론트 엔드로 전달한다. 또한 목록 조회를 요청할 때와 상세 조회를 요청할 때 보내는 데이터에 차이를 두기위해 상속을 이용하였다.

class ArticleSerializer(serializers.ModelSerializer):
    created_at = serializers.DateTimeField(format="%m월%d일 %H:%M", read_only=True)
    author = serializers.SerializerMethodField()
    author_id = serializers.SerializerMethodField()
    likes_count = serializers.SerializerMethodField()
    comment_count = serializers.SerializerMethodField()

    def get_comment_count(self, obj):
        return obj.comment_set.count()

    def get_author(self, obj):
        return obj.author.username

    def get_author_id(self, obj):
        return obj.author.id

    def get_likes_count(self, obj):
        return obj.likes.count()

    class Meta:
        model = Article
        fields = "__all__"

class ArticleListSerializer(ArticleSerializer):

    class Meta:

        model = Article
        fields = (
            "id",
            "author_id",
            "title",
            "author",
            "likes_count",
            "comment_count",
            "created_at",
        )

Pagination

스크린샷 2023-05-15 오후 1.00.36.png

기본 페이지네이션 코드:

# articles.paginations
class ArticlePagination(PageNumberPagination):
    """
    page_size는 한 페이지에 몇 개의 게시글을 담을지 결정합니다.
    page_query_param은 페이지의 이름을 나타냅니다.
    ex)http://127.0.0.1:8000/article/?page=2 를 사용하면 2페이지로 이동합니다.
    여기서 param을 "mypage"라고 지정한다면
    http://127.0.0.1:8000/article/?mypage=2 라고 사용해야합니다.
    max_page_size는 최대 페이지 수를 결정합니다.
    """

    page_size = 10
    page_query_param = "page"
    max_page_size = 100

이번 프로젝트에서는 여러 구간에 pagination이 적용되었으며 각 API의 특성에 따라 조금씩 다르게 구현되었다.

  • 전체 글 리스트:
class ArticleView(generics.ListCreateAPIView):
    ...
    paginations_class = ArticlePagination
    serializer_class = ArticleListSerializer
    queryset = Article.objects.all().order_by("-created_at")
        ...

post 요청도 함께 처리해야하므로 ListCreateAPIView를 사용하였다. 불러와야할 queryset이 항상 같으므로 queryset속성으로 지정하여 가져온다.

  • 피드 글 리스트(팔로우한 유저가 작성한 글 만), 북마크 글 리스트(사용자가 북마크한 글만)
class FeedView(generics.ListAPIView):
    permission_classes = [permissions.IsAuthenticated]
    paginations_class = ArticlePagination
    serializer_class = ArticleListSerializer

    def get_queryset(self):
        articles = Article.objects.select_related("author").filter(
            author__in=self.request.user.followings.all()
        )
        return articles.order_by("-created_at")

class BookmarkListView(generics.ListAPIView):
    ...
    def get_queryset(self):
        articles = self.request.user.bookmarked_articles.all()
        return articles.order_by("-created_at")

전체글 조회의 경우와 달리 오직 get method만을 받아들이므로 ListAPIView을 사용해 구현하였다. queryset이 로그인한 유저에 의해 달라지므로, get_queryset 메소드를 오버라이딩 하여 request.user를 이용해 queryset을 형성하였다.

  • 작성 글 리스트(프로필페이지를 소유한 사용자가 작성한 글만)
class ProfileArticleView(generics.ListAPIView):
    paginations_class = ArticlePagination
    serializer_class = ArticleListSerializer

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(
            get_object_or_404(User, id=kwargs.get("user_id"))
            .article_set.all()
            .order_by("-created_at")
        )

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

위의 두 경우와 달리 queryset이 path variable에 의해 달라진다. path variable은 get 메서드에 kwargs로 전달되고 이는 다시 list 메서드에 전달된다. get_queryset 메서드에는 전달되지 않으므로 list 메서드를 오버라이딩 하여 쿼리셋을 kwargs를 이용해 가져오도록 구현하였다.

Comment C.R.

class CommentView(APIView):
    """
    댓글을 작성하는 공간입니다.
    GET 요청을 처리하여 특정 게시물의 댓글 데이터를 반환하는 기능을 담당합니다.
    get() 메소드는 특정 게시물에 대한 댓글 데이터를 조회하여 시리얼라이즈한 후,
    해당 데이터를 JSON 형식으로 응답으로 반환합니다.
    """

    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get(self, request, article_id):
        article = Article.objects.get(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, context={"request": request}
        )
        if serializer.is_valid():
            serializer.save(author=request.user, article_id=article_id)
            return Response(
                {"message": "작성완료"},
                status=status.HTTP_201_CREATED,
            )
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CommentCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ("content",)
class CommentSerializer(serializers.ModelSerializer):

    created_at = serializers.DateTimeField(format="%m월%d일 %H:%M", read_only=True)
    author = serializers.SerializerMethodField()

    def get_author(self, obj):
        return obj.author.username

    class Meta:
        model = Comment
        fields = "__all__"

댓글을 작성하고 조회하는 기능으로, 작성과 조회에 사용하는 시리얼라이저를 분리하여 구현하였다. 또한 조회시 작성자의 id(username)을 잘 표시하도록 methodField를 사용하였다.

3. 배포

배포는 AWS EC2, route53, CloudFront, S3, ELB(elastic load balancer)를 사용하여 진행하였다.

EC2 인스턴스는 Nginx, Gunicorn, Django로 구성되어 있으며 80번 포트를 listening 한다.

server {
    listen 80;
    server_name your_domain your_ip;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/ubuntu/{루트폴더이름}/{프로젝트이름}.sock;
    }

    location /static {
            root /home/ubuntu/{루트폴더이름}/;
    }

    location /media {
            root /home/ubuntu/{루트폴더이름}/;
    }

}

ELB는 추후 인스턴스를 늘려 scaling을 확보할 수 있고, 도메인을 제공하고 SSL 인증서를 적용, 포트포워딩(443→80)하여 인스턴스의 내부(nginx)에서 포워딩 할 필요 없이 HTTPS통신이 가능하게 한다.

클라우드 프론트는 front-end를 배포하는 S3에 도메인과 SSL인증서를 적용한 연결을 가능하게한다.

Django에서는 ClouldFront의 도메인으로부터 오는 요청에 대해 CORS를 허용하여야한다.

# settings.py
CORS_ALLOWED_ORIGINS = ["https://<cloudfront의 도메인주소>"]

3. 피드백 정리

  1. README 작성하기 - 제품의 포장지와 같이 프로젝트의 목적과 기능을 소개하는 역할을하므로 반드시 있어야 어필에 사용할 수 있으며 내 결과물을 알리는데 매우 중요하다. 따라서 README는 다른 문서화와 함께 꾸준히 최신화하면서 작성하도록한다.
  2. Serializer 적극사용 - 입력값 검증 등은 Serializer에서 처리하고 view에서는 하지 않는 것이 좋다.
  3. packing/unpacking - 적극 사용시 코드를 간결하게 만드는 데에 도움이 된다.
  4. 테스트 코드를 잘 작성해두면 안전한 코드를 비교적 편리하게 작성할 수 있다.(테스트 자동화)
  5. 비슷한 기능을 하는 View를 세부 기능별로 여러개 작성할 필요 없다. 코드를 더 간결하게 짤 수 있도록 고민해보도록 하자.
  6. Queryset.values(), Queryset.values_list()

values()는 쿼리셋의 값을 딕셔너리 형태로 반환한다.

Post.objects.values().filter(id__lt=8)
# <QuerySet [{'id': 5, 'title': 'post #1'}, {'id': 6, 'title': 'title #1'}, {'id': 7, 'title': 'title #2'}]>

만약 아래와 같이 values() 메소드에 인자로서 필드명을 넣으면 {필드: 값}의 형태로 가져올 수 있다.

Post.objects.filter(id__lt=8).values('title')
# <QuerySet [{'title': 'post #1'}, {'title': 'title #1'}, {'title': 'title #2'}]>

values_list()는 쿼리셋의 값을 튜플 형태로 반환한다.

Post.objects.filter(id__lt=8).values_list()
# <QuerySet [(5, 'post #1'), (6, 'title #1'), (7, 'title #2')]>

values_list()에 인자로서 필드를 넣으면 해당 필드의 값만 튜플로 반환한다.

Post.objects.filter(id__lt=8).values_list('title')
# <QuerySet [('post #1',), ('title #1',), ('title #2',)]>

values_list()에는 필드명 이외에 flat이라는 인자를 사용할 수 있다. True로 지정시 튜플이 아닌 리스트로 필드의 값을 반환하며 필드가 여러개일때 사용할 수 없다.

Post.objects.filter(id\_\_lt=8).values\_list(flat=True)

# <QuerySet \[5, 6, 7\]>

Post.objects.filter(id\_\_lt=8).values\_list('title', flat=True)

# <QuerySet \['post #1', 'title #1', 'title #2'\]>

4. KPT

K:

  1. 독스트링 작성하기 - 독스트링을 작성함으로서 해당 함수와 클래스가 무엇이 필요한지, 어떤 기능을 하는지 표시하여 코드를 작성하는데 유용하게 사용할 수 있었다. 다만, 독 스트링을 작성하기 위한 컨벤션은 따로 지정하지 않아 형식이 통일 되지 않은 점이 아쉬웠다. 다음에는 독 스트리 컨벤션도 같이 알아봐야겠다.
  2. 적극적으로 코드 리뷰 - 서로 풀 리퀘스트를 올리고 적극적으로 리뷰를 요청하고 피드백을 주고받는 과정을 통해 코드에 대한 이해도를 높이는데 긍정적으로 작용한 것 같다.
  3. serializer.validate 메서드의 활용과 permission의 활용을 통한 view 간소화 - 검증은 시리얼라이저에서, 로그인이나 사용자에 따른 권한 설정은 permission에서 처리하여 view를 간소화하여 좀더 읽기 편한 코드로 작성한것 같다.

P:

  1. readme가 갱신되지 않음 - 최신 결과물을 반영하는 readme파일이 부재하여 프로젝트가 어떤 목적과 기능을 가지는지 소개할 수 없다. SA문서등을 최신화 할때 함께 변경사항들을 반영해가며 readme를 작성했다면 좋았을 것 같다. readme작성을 포함해서 문서화 과정을 좀더 자주, 체계적으로 해야할 필요가 있는 것 같다.
  2. 과도한 API 분리 - 전체 article 로드, 구독한 글만 로드, 북마크한 글만 로드 등 비슷하지만 조금씩 다른 기능들을 구현하기 위해 여러개의 API를 사용하였는데 그로인해 코드가 복잡해졌다. 유사한 기능을 구현하기 위해 여러개의 API를 만드는 것을 지양하고 최대한 하나의 view 에서 해결 할 수 있는지 고민해보아야 할 것이다.

T:

  1. 소셜 로그인 구현, 이메일 인증 - 다른 팀에서는 많이들 시도한 것 같은데, 아직 나는 해보지 못하였다. 다음 프로젝트 때는 반드시 구현해 보고 싶다.
  2. 백 오피스 구현 - 이번 프로젝트의 이름이었더 백 오피스(관리자페이지)의 구축에는 도전하지 못했다. 다음 프로젝트때에는 만들어보고 싶다.
  3. DB 분리 - 배포시 웹 애플리케이션 서버와 DB는 분리되어있어야 안전하고 서비스의 가용성도 좋아지므로 다음부터는 DB를 분리해보고 싶다.
  4. 주기적 문서화 및 형식을 갖춘 토의 - 자주 소통하긴 했지만 형식과 명확한 목적을 가진 토의는 적었고 문서의 갱신은 드물었다. 이러한 부분을 개선하여 프로젝트를 진행해 보고 싶다.

레퍼런스

https://jinmay.github.io/2020/05/25/django/django-values-and-values-list/

 

[Django]values와 values_list의 차이

Django ORM 최적화 중 하나로서 필요한 필드의 값만 가져오기 위해 values()와 values_list()를 사용한다. 각 메소드의 결과는 어떻게 생겼고 차이점이 무엇인지 정리해본다. values()쿼리셋의 값을 딕셔너

jinmay.github.io

 

'TIL' 카테고리의 다른 글

23.05.16 TIL  (0) 2023.05.17
23.05.12~14 TIL  (0) 2023.05.17
23.05.11 TIL  (0) 2023.05.12
23.05.10 TIL  (0) 2023.05.11
23.05.09 TIL  (0) 2023.05.10