[2024.08.02 TIL] 내일배움캠프 77일차 (최종 팀프로젝트, 회원가입/로그아웃/소셜로그인 프론트엔드 구현)

My_Code·2024년 8월 2일
0

TIL

목록 보기
92/112
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 회원가입 ejs 프론트엔드 구현

  • 로그인과 회원 가입은 같은 페이지에서 진행함

  • 회원 가입에는 입력할 양식이 더 있기 때문에 평소에는 display: none으로 감춰둠

  • 회원 가입으로 전환하면 그때 보이도록 구성

// sign.js

...

  //----------- sign up ---------------------
  signUp.addEventListener('click', function (e) {
    e.preventDefault();

    const h1 = signUp.closest('li').parentNode.previousElementSibling; // h1 요소 찾기
    h1.textContent = 'SIGN UP';
    signUp.parentElement.style.opacity = '1';
    Array.from(signUp.parentElement.parentElement.children).forEach(function (sibling) {
      if (sibling !== signUp.parentElement) sibling.style.opacity = '.6';
    });
    firstInput.classList.remove('first-input__block');
    firstInput.classList.add('signup-input__block');
    // 회원가입에서는 닉네임과 비밀번호 확인 input이 보이도록 변경
    hiddenInputNickname.style.opacity = '1';
    hiddenInputNickname.style.display = 'block';
    hiddenInputPw.style.opacity = '1';
    hiddenInputPw.style.display = 'block';
    // 로그인 버튼은 감추고 회원가입 버튼 보이도록 변경
    signInBtn.style.opacity = '0';
    signInBtn.style.display = 'none';
    signUpBtn.style.opacity = '1';
    signUpBtn.style.display = 'block';

    // 회원 가입 이벤트 연결
    signUpBtn.addEventListener('click', async (e) => {
      e.preventDefault();

      // 회원가입 DTO 객체
      const signUpDto = {
        nickname: document.getElementById('nickname').value,
        email: document.getElementById('email').value,
        password: document.getElementById('password').value,
        passwordCheck: document.getElementById('repeat__password').value,
      };

      try {
        // 백엔드 회원가입 API 호출
        await axios.post('/auth/sign-up', signUpDto);
        alert('회원가입에 성공했습니다. 로그인을 진행해 주세요.');
        // 로그인 페이지로 리다이렉트
        window.location.href = '/views/auth/sign';
      } catch (err) {
        console.log(err);
        const errorMessage = err.response.data.message;
        alert(errorMessage);
      }
    });
  });

...

✏️ 로그아웃 ejs 프론트엔드 구현

  • 로그아웃 ejs는 header ejs에서 구현함

  • 로그인이나 회원 가입처럼 페이지 이동을 하지 않고 header에 계속 위치하기 때문

  • header ejs가 DOM에 로드되면 로그아웃 버튼에 이벤트를 추가함

document.addEventListener('DOMContentLoaded', async () => {
  const signBefore = document.querySelector('#sign-in-before');
  const signAfter = document.querySelector('#sign-in-after');
  const signOutBtn = document.querySelector('#sign-out-btn');

  try {
    // localStorage에서 access token 가져오기
    const token = window.localStorage.getItem('accessToken');

    // 포인트 조회를 통해서 토큰이 유효한지 확인
    const response = await axios.get('/users/me/point', {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    // 로그인 한 상태라면
    signBefore.style.display = 'none';
    signAfter.style.display = 'flex';

    // 로그아웃 이벤트 연결
    signOutBtn.addEventListener('click', async (e) => {
      e.preventDefault();

      try {
        const token = window.localStorage.getItem('refreshToken');

        // axios에서 post이면서 보낼 값은 없고 config 옵션(params, headers 등)만 있다면
        // 보내는 인자값으로 null 같은 아무 값이 필요함
        const response = await axios.post('/auth/sign-out', null, {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        });

        // localStorage에 있는 토큰들 삭제
        window.localStorage.clear();
        // 성공 메세지 출력
        alert(response.data.message);
        // 로그아웃 완료 후 메인 페이지로 이동
        window.location.href = '/views';
      } catch (err) {
        console.log(err);
        const errorMessage = err.response.data.message;
        alert(errorMessage);
      }
    });
  } catch (err) {
    console.log(err);
    // 토큰이 유효하지 않은, 즉 로그인하지 않은 상태라면
    signBefore.style.display = 'flex';
    signAfter.style.display = 'none';
  }
});

// 브라우저 닫을 때 토큰 초기화
document.addEventListener('unload', () => {
  window.localStorage.clear();
});
  • axios에서는 post 메서드 사용 시, 아무 값(null 이라도)을 백엔드 측으로 보내야 함

  • axios에서 사용하는 http 메서드의 기본적인 종류

axios.get(url[, config])            // GET
axios.post(url[, data[, config]])   // POST
axios.put(url[, data[, config]])    // PUT
axios.patch(url[, data[, config]])  // PATCH
axios.delete(url[, config])         // DELETE
  • 모든 메서드들은 url은 필수로 작성해야 하고 data나 config는 옵션임

  • 하지만 post, put, patch 사용 시에는 data가 반드시 필요함

  • data인자는 해당 백엔드 측에서 필요한 데이터를 작성

  • config인자는 params나 headers와 같은 값들을 담아서 백엔드 측에 보냄



✏️ 카카오 로그인 ejs 프론트엔드 구현

  • 카카오 로그인은 기존에 있던 로그인과 동작 방식이 다름

  • 일반적인 로그인은 그냥 axios를 통해 백엔드의 API를 호출하고 토큰값을 반환 받으면 되었음

  • 하지만 카카오 로그인은 카카오 API에서 인증 과정을 대신해주고 사용자에 대한 정보를 넘겨주는 방식임

  • 그래서 프론트엔드에서 axios를 통해서 토큰을 가져올 수 없음

  • 즉, 전체적인 진행 과정은 다음과 같음

    1. 카카오 로그인 URL로 이동
    2. 카카오 UI에서 로그인
    3. 카카오에서 사용자 검증
    4. 사용자 데이터를 Redirect URI 주소로 넘겨줌
    5. Redirect URI 주소를 백엔드 API 경로로 설정
    6. 백엔드 API에서 필요한 절차 진행
    7. 토큰을 Query에 담아서 별도의 프론트엔드 페이지로 이동
    8. 프론트엔드 페이지에서 받아온 토큰을 localStorage에 저장
  • 로그인, 회원가입 페이지

  • 카카오 로그인 페이지로 이동
// sign.js

document.addEventListener('DOMContentLoaded', function () {
  const signUp = document.querySelector('#signup');
  const signIn = document.querySelector('#signin');
  const reset = document.querySelector('#reset');
  const firstInput = document.querySelector('.first-input');
  const hiddenInputNickname = document.querySelector('#nickname');
  const hiddenInputPw = document.querySelector('#repeat__password');
  const signInBtn = document.querySelector('.signin__btn');
  const signUpBtn = document.querySelector('.signup__btn');
  const kakaoSignInBtn = document.querySelector('.kakao__btn');

  ...

  //----------- sign in ---------------------
  signIn.addEventListener('click', function (e) {
    e.preventDefault();
    const h1 = signIn.closest('li').parentNode.previousElementSibling; // h1 요소 찾기
    h1.textContent = 'SIGN IN';
    signIn.parentElement.style.opacity = '1';
    Array.from(signIn.parentElement.parentElement.children).forEach(function (sibling) {
      if (sibling !== signIn.parentElement) sibling.style.opacity = '.6';
    });
    firstInput.classList.add('first-input__block');
    firstInput.classList.remove('signup-input__block');
    hiddenInputNickname.style.opacity = '0';
    hiddenInputNickname.style.display = 'none';
    hiddenInputPw.style.opacity = '0';
    hiddenInputPw.style.display = 'none';
    signInBtn.style.opacity = '1';
    signInBtn.style.display = 'block';
    signUpBtn.style.opacity = '0';
    signUpBtn.style.display = 'none';
  });

  // 로그인 이벤트 연결
  signInBtn.addEventListener('click', async (e) => {
    e.preventDefault(); // 기본 이벤트 동작을 막기 위한 부분

    // 로그인 API에게 보낼 사용자 입력 DTO 객체
    const signInDto = {
      email: document.getElementById('email').value,
      password: document.getElementById('password').value,
    };

    try {
      // 백엔드 로그인 API 호출
      const response = await axios.post('/auth/sign-in', signInDto);

      // 반환된 토큰을 localStorage에 저장
      window.localStorage.setItem('accessToken', response.data.accessToken);
      window.localStorage.setItem('refreshToken', response.data.refreshToken);

      // 로그인 완료 후 메인 페이지로 이동
      window.location.href = '/views';
    } catch (err) {
      // 로그인 실패 시 에러 처리 (에러 메세지 출력)
      console.log(err.response.data);
      const errorMessage = err.response.data.message;
      alert(errorMessage);
    }
  });

  //----------- kakao sign in ---------------------
  kakaoSignInBtn.addEventListener('click', async (e) => {
    e.preventDefault();
    window.location.href = '/auth/kakao';
  });
	
	...
  
});
  • 카카오 로그인 성공 후 설정한 Redirect URI로 이동
// auth.controller.ts

...

  /**
   * 카카오 로그인
   * @param req
   * @returns
   */
  @UseGuards(KakaoAuthGuard)
  @Get('/kakao')
  async kakaoSignIn(@Req() req: any, @Res() res: any) {
    const { accessToken, refreshToken } = await this.authService.signIn(req.user);
    res.redirect(
      `http://localhost:3000/views/auth/kakao/process?accessToken=${accessToken}&refreshToken=${refreshToken}`
    );
  }
  
...
  • 백엔드 카카오 로그인 API에서 넘겨준 토큰을 별도의 페이지에서 저장
    • 메인 페이지에서 JS를 실행하지 않는 이유는 header.js에 있는 로그인 여부를 파악하는 코드에서 다시 토큰을 검증하기 때문에
    • 그리고 매번 새로 고침 할 때마다 header.js에서 토큰이 localStorage에 저장하는 코드를 동작 시킬 필요가 없기 때문에
// auth.view.controller.ts

import { Controller, Get, Render } from '@nestjs/common';

@Controller('views/auth')
export class AuthViewsController {
  @Get('/sign')
  @Render('auth/sign.view.ejs')
  signIn() {}

  // 단순하게 카카오 로그인 API의 redirect 처리를 위한 경로
  // 일반 로그인과 진행 방식이 다르기 때문에 별로의 경로를 사용
  // process 페이지에서 토큰을 위한 JS 코드 실행
  @Get('/kakao/process')
  @Render('auth/kakao-sign-in.view.ejs')
  kakaoSignInProcess() {}
}
<!-- kakao-sign-in.view.ejs -->

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nest & EJS ❤</title>
    <script type="module" src="/js/auth/kakao-sign-in.js"></script>
</head>

</html>
// kakao-sign-in.js

window.onload = () => {
  const urlParams = new URLSearchParams(window.location.search);
  const accessToken = urlParams.get('accessToken');
  const refreshToken = urlParams.get('refreshToken');

  if (accessToken && refreshToken) {
    window.localStorage.setItem('accessToken', accessToken);
    window.localStorage.setItem('refreshToken', refreshToken);
    // 로그인 완료 후 메인 페이지로 이동
    window.location.href = '/views';
  }
};


📌 Tomorrow's Goal

✏️ 백엔드 인증, 인가 코드 리팩토링

  • 문자열 및 상수들 모듈화 할 예정

  • 코드 상에서 필요없는 코드들 지우고 테스트 진행할 예정

  • 일찍 끝나면 다른 팀원들 기능 구현이나 프론트엔드 도울 예정



📌 Today's Goal I Done

✔️ 인증 관련 프론트엔드 구현

  • 오늘은 회원가입, 로그아웃, 카카오 로그인 관련 프론트엔드를 구현함

  • 회원가입 및 로그아웃은 사실 CSS 부분이 대부분이고 단순하게 axios로 백엔드 API만 호출하면 되었기에 금방 구현함

  • 하지만 카카오 로그인은 생각보다 어려웠음

  • 기존에 사용하던 로그인 방식과 많이 다르기 때문에 프론트엔드에서도 다른 구조로 구현됨

  • 카카오 로그인 따로 처리되어야 하고, 중복이 발생할 수 있는 로직이 있기 때문에 별도의 빈 페이지를 통해서 그 과정을 처리함


profile
조금씩 정리하자!!!

0개의 댓글