본문 바로가기

TIL

23.05.12~14 TIL

여러 추가 기능 구현과 JS를 이용한 토큰 리프레시 구현을 하였다.

 

 

1. Pagination과 generics

페이지네이션을 구현하기 위해서는 두가지 방법을 사용할 수 있다.

 

Django에서 지원하는 paginator 사용

from django.core.paginator import Paginator

    ...
    # get view
    all_post = Posting.objects.all()
    all_post = all_post.order_by('-created_at')
    page = int(request.GET.get('page', '1'))
    paginator = Paginator(all_post, 5)
    post_list = paginator.get_page(page)
    # 이후 시리얼라이즈
    ...

paginator.get_page() 메서드는 입력받은 번호의 페이지에 해당하는 객체의 리스트를 반환한다. 이를 시리얼라이저로 처리해 전달할 수 있다.

 

DRF의 generics의 View들 활용하기

generics.ListAPIView나 ListCreateAPIView를 사용하면 pagination을 편리하게 적용할 수 있다.

class ArticleView(generics.ListCreateAPIView):
    """
    ListCreateAPIView는 GET요청일 때 목록으로 조회, POST요청일 때 새로운 데이터를 생성하는 역할을 합니다.
    def get의 역할을 ListCreateAPIView에서 지원하기에 따로 지정해 줄 필요가 없습니다.
    queryset을 통해 article의 내용을 가져옵니다.
    필요한 것을 명시해주면 간단히 get 요청을 구현할 수 있습니다.
    """
    paginations_class = ArticlePagination
    serializer_class = ArticleListSerializer
    queryset = Article.objects.all().order_by("-created_at")

post 요청이 필요없다면 ListAPIView를 사용하면 된다.

페이지네이션 클래스는 따로 지정해주

 

def get_queryset(self):
        articles = self.request.user.bookmarked_articles.all()
        return articles.order_by("-created_at")
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)

또한 query_set을 지정할 때 request나 path_variable을 사용해야한다면 get_queryset 메서드를 오버라이딩해서 커스텀하거나 list메서드를 오버라이딩 하여 커스텀 할 수 있다. 단, 이경우 path_variable을 사용하기보단 request 에서 바로 까볼 수 있는 query string에 해당 값을 전달하는 것이 나을 것 같다.

 

페이지네이션을 위해 해당 클래스를 사용할시 get 요청시 응답 값음 다음의 형태를 띈다.

{
    "count": 17,
    "next": "http://localhost:8000/article/?page=2",
    "previous": null,
    "results": [
        {"title": "ㅁㄴㅇㄹㅁㄴㅁㅇㄹ", "author": "abcdefg"},
        {"title": "asfkweqmrsaf", "author": "abcdefg1"},
        {"title": "asfkweqmrsaf", "author": "abcdefg1"},
        {"title": "asfkweqmrsaf", "author": "abcdefg1"},
        {"title": "asfkweqmrsaf", "author": "abcdefg1"}
    ]
}

 

2. JS - token refresh

유효기간이 짧은 Access Token을 갱신(재발급)하기 위해서는 refresh 토큰을 제출하여 검증 후 access 토큰을 응답으로 받아야한다. 매 요청을 보낼 때 마다 이러한 과정이 발생하면, refresh 토큰의 사용이 사실상 access 토큰과 다를바가 없어지니, 이는 지양해야할 것이다.

따라서 언제 refresh 요청을 보낼지 결정해야한다.

 

방식 1. - 요청을 보낼 때 마다, 프론트에서 exp(만료기간)을 확인해 지났으면 갱신한다.

프론트 엔드에서 비교하여 시기를 정하므로 요청의 횟수를 가장 많이 줄일 수 있지만 형식문제로 인해 제대로 구현하지 못했다.

방식 2. - 요청을 일단 보내고, 401이 뜨면 토큰을 갱신한다.

필요한 요청만 보내므로 요청횟수가 비교적 적지만 토큰이 필요한 요청이 여러개 일 때 중복된 코드를 어떻게 처리해야할지 감을 잡지 못하여 구현하지 못했다.

방식 3. - verify API에 먼저 요청을 보내 401이 뜨면 토큰을 갱신하고 원하는 곳에 요청을 보낸다.

필요한 요청보다 한번 더 보내므로 요청 횟수가 많지만, 함수 하나 만들어서 불러오면 되는 아주 쉬운 구현 난이도를 가지고 있어 이것으로 시도 하였다.

 

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()
        }
    }
}

먼저 verify 에 access토큰을 실어 post 요청을 보낸다. 응답으로 200이 나오면 유효한것이고 아니면 유효하지 않은 토큰이므로 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")
}

 

재발급 요청을 보낼 때 응답이 401 이면 로그아웃처리된다. 아닐경우 새 access 토큰과 페이로드를 파싱해 로컬 스토리지에 저장한다.

 

 

3. EC2로 백엔드 배포

먼저 EC2 인스턴스를 생성한 뒤 다음 명령어를 입력해 시스템 소프트웨어들을 최신화한다.

sudo apt update
sudo apt upgrade

 

이후 다음 명령어를 입력해 python 의 관리를 용이하게 한다.

apt install python3-pip python3-dev python3-venv

 

가상환경을설정하고, 거기서 필요한 패키지들을 requrements.txt에 의해 설치한다.

깃헙에 업로드한 백엔드 어플리케이션을 clone 해온다. 이때, ssh키를 생성하고 이를 등록하여 보안을 확보한다.

ssh-keygen -t rsa -b 4096
cat /home/ubuntu/.ssh/id_rsa.pub

settings.py를 수정한다.

  1.  DEBUG = False : 디버그 모드를 끄면 더이상 요청에 대해 에러 페이지로 응답하지 않는다. 또한 여러 보안 스펙 등이 달라진다고 한다.
  2. AllowHost : EC2의 public IP 주소와 추후 사용할 도메인을 함께 등록해둔다.
  3. CORS_ALLOWED_ORIGINS : EC2 public IP와 앞으로 사용할 프론트엔드의 IP, 도메인등을 등록할 수 있다.
  4. static 과 media 파일 관련 설정이 있는지 확인한다.
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "static"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

이후 gunicorn과 nginx를 설치한다.

  1. gunicorn:
    • gunicorn은 Django와 같은 웹 애플리케이션을 실행하는 서버이다. runserver의 성능적 부족을 해결한다.
    • 여러 개의 동시 요청을 처리하기 위해 여러 프로세스를 사용하며 안정적이고 빠른 서비스를 제공한다.
    • 주로 프로덕션 환경에서 Django 애플리케이션을 실행하는 데 사용한다.
  2.  Nginx:
    • Nginx는 웹 서버로, 정적 파일 서비스와 프록시 서버 기능을 수행한다.
    • 정적 파일 서비스: 웹 사이트의 이미지, CSS, JavaScript와 같은 정적 파일을 빠르게 제공
    • 프록시 서버는 클라이언트 요청을 받아 애플리케이션 서버(예: gunicorn)로 전달
    • 또한 로드 밸런싱과 SSL/TLS 암호화와 같은 기능도 제공한다. 그외 보안, 성능 및 부하 분산을 위해 주로 프로덕션 환경에서 사용된다.
pip install gunicorn
sudo vim /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/{루트폴더이름}
ExecStart=/home/ubuntu/{루트폴더이름}/venv/bin/gunicorn  --access-logfile - --workers 3 --bind unix:/home/ubuntu/{루트폴더이름}/{프로젝트이름}.sock {프로젝트이름}.wsgi:application


[Install]
WantedBy=multi-user.target

위명령어 실행 및 파일작성을 통해 프로젝트명.socket 이 만들어진다. 이수 sudo systemctl status gunicorn 이라고 하면 잘 작동하는 지 확인할 수 있다. .env파일로 시크릿키를 숨긴경우 EnvironmentFiles

 

sudo apt install nginx

sudo vim /etc/nginx/sites-available/drf_project
```jsx
server {
    listen 80;
    server_name your_domain_or_ip;

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

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

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

}
```

위 과정을 통해 Django와 gunicorn, nginx를 연결할 수 있다. 이후 권한 문제로 오류가 발생할 시 다음과 같이 입력한다.

chmod 755 ~/django_deploy/
chmod 755 ~

권한 설정을 이렇게 할 경우 소유자에게 읽기, 쓰기, 실행 권한, 그룹 기타 사용자에게 읽기 실행 권한을 부여받는다.

'TIL' 카테고리의 다른 글

23.05.16 TIL  (0) 2023.05.17
23.05.15 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