[DRF - React] JWT인증 방식의 서비스에서 로그인 된 유저 이름 화면에 띄우기

느려도 꾸준한 발걸음·2024년 7월 18일
0
post-thumbnail

1. 상황 설명

로그인을 하면, 사용자의 이름을 포함한 상쾌한 인사 메세지를 건네주면 좋을 것 같습니다.

아래와 같은 느낌으로 말이죠.


디자이너들이 보면 기겁할 디자인이지만,
저희는 디자이너가 아니니 로직에만 집중해 보겠습니다.

저의 경우, 프론트엔드는 React, 백엔드는 DRF(Django Rest Framework)를 사용하고 있으며, 사용자 인증은 JWT를 사용하였습니다.

로그인 된 유저의 정보를 프론트엔드 단에서 접근하기 위한 방법을 절차화 하면 다음과 같습니다.

  1. 로컬 스토리지에 저장된 엑세스 토큰을 취득
  2. 사용자 정보를 DB에서 꺼내오는 백엔드 View에 GET요청 - 요청 헤더의 Authorization key에 취득한 엑세스 토큰 전달

2. 전제 조건

해당 포스팅의 내용은, 독자 여러분이 아래의 것들을 모두 구현해둔 상태라고 가정하고 진행됩니다. 아래의 내용 중 하나라도 구현이 누락된 것이 있다면, 코드는 돌아가지 않습니다.

  1. simple JWT 연동 및 토큰 관련 설정
  2. 유저 모델 DB연동

3. 구현

3-1. axiosInstance 파일 생성

저의 경우 프로젝트에서 axios를 많이 사용하고,
장고 백엔드에서 요청을 처리할 모든 엔드포인트들이 api로 시작하기에,

중복되는 코드의 반복적 작성 방지 차원에서 로컬호스트 주소/api/ 를 baseURL로서 포함하는 axios객체를 정의해 사용하겠습니다.

아래는 프로젝트의 최상단 디렉토리에 생성한 axios.js파일의 소스코드입니다.

import axios from "axios";

const baseURL = "http://127.0.0.1:8000/api/";

const axiosInstance = axios.create({
  baseURL: baseURL,
  timeout: 5000,
  headers: {
    //django 설정 파일에서 simple jwt header type에 JWT주었음
    Authorization: localStorage.getItem("access_token")
      ? "JWT " + localStorage.getItem("access_token")
      : null,
    "Content-Type": "application/json",
    accept: "application/json",
    // responseType: "json",
  },
});

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async function (error) {
    const originalRequest = error.config;

    if (typeof error.response === "undefined") {
      alert("서버 장애가 발생했습니다. 빠른 시일 내에 해결하겠습니다");
      return Promise.reject(error);
    }

    if (
      error.response.status === 401 &&
      originalRequest.url === baseURL + "token/refresh/"
    ) {
      window.location.href = "/login/";
      return Promise.reject(error);
    }

    if (
      error.response.data.code === "token_not_valid" &&
      error.response.status === 401 &&
      error.response.statusText === "Unauthorized"
    ) {
      const refreshToken = localStorage.getItem("refresh_token");

      if (refreshToken) {
        const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));

        // exp date in token is expressed in seconds, while now() returns milliseconds:
        const now = Math.ceil(Date.now() / 1000);
        console.log(tokenParts.exp);

        if (tokenParts.exp > now) {
          return axiosInstance
            .post("/token/refresh/", { refresh: refreshToken })
            .then((response) => {
              localStorage.setItem("access_token", response.data.access);
              localStorage.setItem("refresh_token", response.data.refresh);

              axiosInstance.defaults.headers["Authorization"] =
                "JWT " + response.data.access;
              originalRequest.headers["Authorization"] =
                "JWT " + response.data.access;

              return axiosInstance(originalRequest);
            })
            .catch((err) => {
              console.log(err);
            });
        } else {
          console.log("Refresh token is expired", tokenParts.exp, now);
          window.location.href = "/login/";
        }
      } else {
        console.log("Refresh token not available.");
        window.location.href = "/login/";
      }
    }

    // specific error handling done elsewhere
    return Promise.reject(error);
  }
);

export default axiosInstance;

이제, axios대신 axiosInstance를 import해 사용하겠습니다.
이렇게 함으로 인해 axios의 요청 엔드포인트에 적어야 할 내용이 간단해집니다.

const baseURL = "http://127.0.0.1:8000/api/";

위와 같이 baseURL을 설정하였기에 axiosInstance.get("user") 로 요청을 보내는 것은http://127.0.0.1:8000/api/user 엔드포인트로 요청을 보내는 것입니다.

가독성 좋은 코드를 작성하기 훨씬 좋은 환경을 만들어 둔 것 같습니다.

3-2. 백엔드 view, serializer 및 라우팅

django의 user app 안에, 프론트엔드 단으로 유저의 정보를 보내줄 뷰를 생성해주었습니다.

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import UserSerializer

#--------로그인 로그아웃 코드 생략--------

class CurrentUserView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        serializer = UserSerializer(request.user)
        return Response(serializer.data)

시리얼라이저는 아래와 같이 간단히 만들어주겠습니다.
아래의 코드에서 TravelCommunityUser 는 제가 생성한 커스텀 유저 클래스입니다.

from rest_framework import serializers
from user.models import TravelCommunityUser

#--------기타 시리얼라이저 코드 생략 --------------

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = TravelCommunityUser
        fields = "__all__"  # 필요한 필드를 추가하세요.

이제, 해당 app 내의 urls.py에 해당 뷰를 연동해주어야 합니다.

from django.urls import path
from .views import CustomUserCreate, BlacklistTokenView, CurrentUserView

urlpatterns = [
    path('register/', CustomUserCreate.as_view(), name="create_user"),
    path('logout/blacklist/', BlacklistTokenView.as_view(), name="blacklist"),
    path('current_user/', CurrentUserView.as_view(), name="current_user")
]

참고로, 제 장고 프로젝트의 root url은 아래와 같이 설정되어 있습니다.

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)
from rest_framework.schemas import get_schema_view
from rest_framework.documentation import include_docs_urls

urlpatterns = [
    path("admin/", admin.site.urls),
    path('', include('myblog.urls')),
    path('api/', include('blog_api.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/user/', include("user.urls")),
    path('schema/', get_schema_view(
        title="트래블원큐 커뮤니티",
        description="트래블원큐 커뮤니티의 게시판을 위한 스키마입니다.",
        version="1.0.0"
    ), name='openapi-schema'),
    path('docs/', include_docs_urls(title="blogAPI"))
]

이제, 프론트엔드에서 로컬호스트/api/user/current_user/ 엔드포인트로 GET요청을 보내면, 장고는 데이터베이스에서 해당 유저의 정보를 프론트엔드로 보내줍니다.

axiosInstance를 사용하면, user/current_user/ 경로로 요청을 보내주면 되겠네요.

3-3. 프론트엔드에서 서버로 요청 보내기

axios라이브러리를 사용하여 서버로 유저 정보 데이터를 보내달라고 요청을 보내보겠습니다.

엔드포인트의 중복 작성을 막기 위해 만들어둔 axiosInstance를 사용하겠습니다.

postgreSQL데이터베이스에서 사용자들이 올린 게시글들을 로드하고,
맨 위에는 현재 로그인한 사용자의 이름을 포함한 인사 메세지를 전달하는 페이지의 전체 소스코드는 아래와 같습니다.

import Logout from "../components/Logout";
import Post from "../components/Post";
import SearchForm from "../components/SearchForm";
import axiosInstance from "../axios";
import { useState, useEffect } from "react";

export default function PostingFeedUI() {
  const [author, setAuthor] = useState(null);

  useEffect(() => {
    const fetchAuthor = async () => {
      try {
        const token = localStorage.getItem("access_token");
        if (!token) {
          throw new Error("Token not found");
        }

        // JWT를 사용하여 현재 사용자 정보 요청
        const response = await axiosInstance.get("user/current_user/", {
          headers: {
            Authorization: `JWT ${token}`,
          },
        });

        setAuthor(response.data); // 현재 사용자 정보 설정
        console.log(response.data);
      } catch (error) {
        console.error("Error fetching current user:", error);
      }
    };

    fetchAuthor();
  }, []);

  if (!author) {
    return <div>Loading...</div>; // 사용자 정보를 불러오는 동안 표시할 내용
  }

  return (
    <>
      <h1>
        {author.user_name}님 안녕하세요! <br />
        떠나기 전, 여행자들의 생생한 후기를 확인해 보세요!
      </h1>
      <SearchForm />
      <Post />
      <Logout />
    </>
  );
}

아무런 디자인도 입히지 않았습니다. 로직에만 집중해주세요.
디자이너님들은 분노를 잠시만 가라앉혀 주세요.

완성된 모습은 아래와 같습니다.

이제, 전체의 코드 중 사용자 정보를 가져오는 부분만 조금 더 자세히 살펴보겠습니다.

포스팅의 시작부에서, 로직의 절차를 다음과 같이 정리하였습니다.

  1. 로컬 스토리지에 저장된 엑세스 토큰을 취득
  2. 사용자 정보를 DB에서 꺼내오는 백엔드 View에 GET요청 - 요청 헤더의 Authorization key에 취득한 엑세스 토큰 전달

각 절차에 맞는 코드를 살펴볼까요?

3-3-1. 엑세스 토큰 취득

서버에 유저 정보를 달라는 요청을 보내려면,
권한이 있어야 합니다.

만약 이 세상 누구에게나 서버에 데이터를 보내달라는 요청을 보낼 수 있게 한다면,
여러 정보가 유출될 수 있겠죠.

그래서 저희는, 본격적으로 서버로 요청을 보내기 전에,
요청의 헤더에 함께 보낼 Authorization value를 구해야 합니다.

요청을 보낼 때, "자 여기, 내 요청은 받아도 안전하다는 증표." 하고 함께 보낼 무언가가 필요합니다.

저의 경우 JWT를 사용하고 있으니, 엑세스 토큰을 보내주면 좋을 것 같습니다.

const token = localStorage.getItem("access_token");

토큰은 브라우저의 로컬 스토리지에 저장되어 있습니다.

참고로 "access_token" 은 제가 지정한 key입니다.

3-3-2. 서버로 요청 보내기

이제, 아래와 같이 헤더에 토큰을 넣어 get요청을 보내주면 됩니다.

axiosInstance.get("user/current_user/", {
          headers: {
            Authorization: `JWT ${token}`,
          },
        });

JWT접두사와 토큰 값 사이엔 한 칸의 공백이 존재해야 한다는 점에 주의해주세요.

3-3-3. 정보를 state에 저장하기

이제, 받아온 정보를 react의 state에 저장하기만 하면 어디서든 사용할 수 있습니다.

기왕 하는거 예외처리 구문도 포함시켜주면 좋을 것 같습니다.

수정이 완료된 코드의 모습은 아래와 같습니다.

const [author, setAuthor] = useState(null);

  useEffect(() => {
    const fetchAuthor = async () => {
      try {
        const token = localStorage.getItem("access_token");
        if (!token) {
          throw new Error("Token not found");
        }

        // JWT를 사용하여 현재 사용자 정보 요청
        const response = await axiosInstance.get("user/current_user/", {
          headers: {
            Authorization: `JWT ${token}`,
          },
        });

        setAuthor(response.data); // 현재 사용자 정보 설정
        console.log(response.data);
      } catch (error) {
        console.error("Error fetching current user:", error);
      }
    };

    fetchAuthor();
  }, []);

  if (!author) {
    return <div>Loading...</div>; // 사용자 정보를 불러오는 동안 표시할 내용
  }

4. 마무리

오늘은 프론트엔드 개발을 하며 마주할 수 있는 상황 중 하나인
사용자 정보 가져오기를 주제로 이야기를 나눠보았습니다.

사실 오늘날의 거의 모든 서비스에서는,
사용자의 이름이 포함된 UI를 다수 구현해야 합니다.

${사용자이름} 님이 좋아하실만한 상품을 추천해드려요! 와 같이, 로그인 된 유저의 이름을 서버에서 받아와야 하는 상황이 정말 많습니다.

여러 사용자 인증 방식 중, 저와 같이 토큰 기반 인증 방식을 채택한 서비스라면,
axios를 통해 간단하게 해결할 수 있었습니다.

DRF-React-JWT 기반의 프로젝트를 진행하시는 분들께 이번 포스팅이 많은 도움이 되었으면 좋겠습니다!

감사합니다.

profile
웹 풀스택 개발자를 준비하고 있습니다. MERN스택을 수상하리만큼 사랑합니다.

0개의 댓글