[개인 프로젝트] CardLog 제작기(4) - Validation 회원가입과 JWT 로그인

건둔덕 ·2023년 3월 27일
3

CardLog 제작기

목록 보기
4/7
post-thumbnail

이전 포스팅 후 꽤 많은 시간이 흘렀다.. 오래 걸린 이유는 유효성 검증을 프론트와 백엔드에서 처리 후 회원가입을 진행하는 부분을 만들고, JWT(JsonWebToken)을 활용한 로그인 처리를 해주기 위해 자료조사와 로직을 이해하고 프론트단과 백엔드단을 만들다 보니 열심히 해도 오래 걸렸다 ..

Validation(유효성) 회원가입

일단 먼저 유효성 검증을 통한 회원가입 로직을 제작하기 위해 프론트와 백엔드 부분에서 어디에서 유효성을 검증해줘야 할 지 정하기 위해 자료조사를 시작했었다.

결론부터 말하자면 프론트와 백엔드 둘 다 해줬다.

프론트 부분에서만 유효성을 검증하면 유효성 처리를 전부 클라이언트에서 진행해주기 때문에 퍼포먼스가 좋다는 장점이 있다. 하지만 javascript로 조작해서 서버로 데이터를 보낼 수 있다는 문제점이 있다.

그렇다고 백엔드에서만 유효성 검증을 하자니 매번 유효성 검증을 진행할 때 마다 데이터를 서버로 쏴준 후 검증 후 검증된 결과값을 다시 클라이언트로 리턴해주는 과정을 게속 일으켜야 하기 때문에 이 방법도 문제가 많았다.

그래서 프론트 부분에서 먼저 기본적인 유효성 검증을 한 후 데이터를 백엔드로 전달해 주고 백엔드 부분에서는 유효성이 검증된 데이터들을 DB안의 데이터들과 비교하며 2차 검증을 거친 후 문제가 없다고 판단될 시 리턴 해주는 방식으로 작업을 마무리 했다.



회원가입 폼 작성

회원가입 부분은 리렌더링을 최대한 줄이는 방식을 사용하기 위해서 고민을 많이 했던 것 같다.

각 Input의 useState를 사용하는 것은 자제하고 useRef 방식을 사용해서 제작하려다가 각 Input들을 컴포넌트로 만들어두고, 그 Input 컴포넌트를 기반으로 ValidInput 컴포넌트를 새로 제작해서 사용을 했다.

ref를 props로 내려줄 때 이중으로 내려가야 해서 수정을 하자니 기존의 Input 컴포넌트를 사용 중인 부분도 문제가 되기도 하고 여러모로 고려해야할 사항이 많았다.

그래서 렌더링을 줄이면서 최대한 컴팩트하게 제작한 방식은 위와 같이 Object를 만들어서 부모 컴포넌트에서 각 하위 컴포넌트로 내린 후 각 Key 값에 맞는 Input들이 그에 해당하는 정규식으로 유효성을 검증했다.

유효성이 검증된 Input 값은 onBlur 이벤트를 활용해서 ValidObj 안의 검증 여부와 value 값을 넣어주게 제작했다.

참 이 부분 때문에 고민도 많이하고 생각도 많이 했었던 것 같다.



이메일 인증

회원가입 폼을 유효성에 맞게 전부 작성한 후 다음 스텝으로 넘기면 이전 스텝에서 입력했던 이메일로 인증을 위한 이메일을 발신한다.


이메일이 발송되면 실제로 위와 같은 이미지 처럼 이메일을 발송되게 백엔드에서 처리를 해두었다.

위의 이메일 이미지에서 '계정 인증하기' 버튼을 클릭하면 실제 DB안의 유저의 접근 권한을 승인해준다.

위의 과정이 완료되면 이제 로그인이 가능하다.

추후엔 Django crontab을 사용해서 회원가입은 했지만, 이메일 인증을 하지 않은 계정을 24시간에 한번씩 체크해서 삭제하는 로직도 제작해 볼 예정이다.



JWT(JsonWebToken) 로그인

이제 이메일 인증까지 완료된 계정으로는 로그인이 가능하다.

원래는 Session 방식의 로그인을 구현하려고 했었지만.. Session 방식은 기존 회사에 있었을 때 해봤었기 때문에 이번엔 굳이~~ JWT 공부도 할 겸 JWT 방식을 채택했다.

하지만.. 개발을 하면서 게속 후회했었다. JWT 방식은 지금 내가 개발하고 있는 방식에서는 너무 맞지 않는 방식이였다...

왜 맞지 않냐면.. JWT 방식은 로그인할 때 refresh_token과 access_token을 리턴 받고 리턴 받는다.

그 중 access_token은 서버와 통신할 때 항상 request header의 프로퍼티로 access_token 값을 포함해서 보내줘야만 데이터를 리턴해준다.

물론!! access_token이 필요한 부분과 필요하지 않은 부분을 구분해서 프론트와 백엔드에서 각각 처리해주면 상관은 없긴한데 작업해줄 부분이 좀 많아진다...

하지만 그것보다 더 큰 문제가 있었었다.

첫번째로 내가 지금 제작하고 있는 CardLog는 나 혼자만 사용하는 블로그가 아니라 회원가입을 한 유저들이 블로그를 사용하고, 회원가입을 하지 않아도 다른 사용자들이 그 블로그에 접근을 할 수 있어야 하는 서비스 웹이고,

두번째는 내가 JWT 구현에 너무 집중을 하고 있다보니 첫번째 문제들을 생각 안하고 구현을 했다는 부분이 정말 큰 문제 였었다.

하하하 ...

그 이후 열심히 리팩토링을 진행했다.

let loginInterval: NodeJS.Timer | null = null;

interface LoginProps {
  email: string;
  password: string;
}

export const login = async (data: LoginProps) => {
  let statusText: string = "Bad Request..";

  await api
    .post(API_Path.LOGIN, data)
    .then((res) => {
      setCookie("access", res.data.access_token);
      setCookie("refresh", res.data.refresh_token);
      checkAccess();
      statusText = res.statusText;
    })
    .catch((error) => {
      statusText = error.response.statusText;
      Swal.fire({
        icon: "error",
        html: `
          <h4>로그인 실패</h4>
        `,
        confirmButtonColor: palette.black4,
        confirmButtonText: "확인",
        focusConfirm: true,
      });
    });

  return statusText;
};

export const reissueAccess = async () => {
  const refreshToken = getCookie("refresh");

  if (refreshToken) {
    await api
      .post(API_Path.REFRESH_TOKEN, { refresh: refreshToken })
      .then((res) => {
        setCookie("access", res.data.access);
      })
      .catch((error) => {
        console.log(error);
      });
  } else if (!window.location.pathname.includes(RouterInfo.LOGIN.path)) {
    const params = new URLSearchParams(window.location.search);

    if (!params.get("blog_id")) window.location.href = RouterInfo.LOGIN.path;
  }
};

export const checkAccess = () => {
  const accessToken = getCookie("access");

  // access_token 없을 때
  if (!accessToken) {
    reissueAccess();
    return;
  }

  // access_token 만료시간이 1분 남았을 때
  const decode: any = jwt_decode(accessToken);
  const expireTime = decode.exp;
  const currentTime = new Date().getTime() / 1000 - 60000; // 현재 시간 1분 전

  if (expireTime < currentTime) reissueAccess();

  const delay = (expireTime - Date.now() / 1000) * 1000;

  if (loginInterval !== null) clearInterval(loginInterval);
  loginInterval = setInterval(reissueAccess, delay - 30000);
};

export const logout = async () => {
  if (loginInterval !== null) clearInterval(loginInterval);

  await accessApi
    .post(API_Path.LOGOUT)
    .then((res) => {
      removeCookie("access");
      removeCookie("refresh");
    })
    .catch((error) => console.log(error))
    .finally(() => (window.location.href = RouterInfo.LOGIN.path));
};

[2023.04.06] 서버 프론트와 백엔드를 분리해서 올려두니 accessToken과 refreshToken을 Django에서 쿠키에 설정해주는 부분이 작동이 안되서 auth 부분 수정작업


로그인이 되었을 때 백엔드에서 바로 쿠키에 refresh_token과 access_token을 넣어주고

프론트에서는 로그인 직 후 access_token의 값을 받아와서 decode 후 만료기한만 뽑아내고 그 만료기한에 맞춰서 Interval을 작동시켰다.

물론!! 새로고침 할 때와 라우팅 동작할 때 마다 체크하는 함수를 만들어서 만료기한이 1분도 안 남았을 때는 쿠키안에 가지고 있는 refresh_token으로 다시 access_token을 받아와서 로그인 했을 때와 동일하게 처리해줬다.



회원가입과 로그인 부분의 제작을 하는 부분에서 생각보다 많은 시간이 소요됬던 것 같다.

참고
CardLog FE Github
CardLog BE Github

profile
건데브

0개의 댓글