본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
로그인과 회원 가입은 같은 페이지에서 진행함
회원 가입에는 입력할 양식이 더 있기 때문에 평소에는 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는 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와 같은 값들을 담아서 백엔드 측에 보냄
카카오 로그인은 기존에 있던 로그인과 동작 방식이 다름
일반적인 로그인은 그냥 axios를 통해 백엔드의 API를 호출하고 토큰값을 반환 받으면 되었음
하지만 카카오 로그인은 카카오 API에서 인증 과정을 대신해주고 사용자에 대한 정보를 넘겨주는 방식임
그래서 프론트엔드에서 axios를 통해서 토큰을 가져올 수 없음
즉, 전체적인 진행 과정은 다음과 같음
로그인, 회원가입 페이지
// 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';
});
...
});
// 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}`
);
}
...
// 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';
}
};
문자열 및 상수들 모듈화 할 예정
코드 상에서 필요없는 코드들 지우고 테스트 진행할 예정
일찍 끝나면 다른 팀원들 기능 구현이나 프론트엔드 도울 예정
오늘은 회원가입, 로그아웃, 카카오 로그인 관련 프론트엔드를 구현함
회원가입 및 로그아웃은 사실 CSS 부분이 대부분이고 단순하게 axios로 백엔드 API만 호출하면 되었기에 금방 구현함
하지만 카카오 로그인은 생각보다 어려웠음
기존에 사용하던 로그인 방식과 많이 다르기 때문에 프론트엔드에서도 다른 구조로 구현됨
카카오 로그인 따로 처리되어야 하고, 중복이 발생할 수 있는 로직이 있기 때문에 별도의 빈 페이지를 통해서 그 과정을 처리함