백엔드에서 만든
로그인 API
를 사용하여 axios로 post 하기
Swagger
를 사용하면 데이터의request
형식을 알 수 있기 때문에 백엔드와 api 소통이 편리합니다.
이메일(or 아이디 : 원하시는 걸로) 과 비밀번호를 적어 로그인 버튼을 누르면
서버로 데이터(이메일, 비밀번호)를 보내주고
서버에선 회원가입이 된 계정이 로그인 성공을 함을 인증해주는 accesstoken을 발급해줍니다.
( accesstoken은 저장하여 console에 찍어보면 확인 가능합니다.)
이 accesstoken을 localstorage에 저장해주고
localstorage에 accesstoken가 있다면 ? 로그인 상태
localstorage에 accesstoken가 없다면 ? 비로그인 상태
로그인 상태일 때 로그아웃을 하는 방법
여기서 프론트가 해야 하는 일은 1, 2, 4, 5, 6, 7 입니다.
먼저 목업작업은 끝난 상태에서, axios
를 사용하여 데이터를 post
하는 과정을 알려드릴게요.
이메일과 비밀번호에 적은 데이터가 로그인 버튼을 누르면 login API
를 post
할 거에요.
전체적인 흐름을 위해 구현 코드를 먼저 첨부하겠습니다.
//login.tsx
'use client';
import {
Wrapper,
Section,
LoginText,
Box,
BottomText,
BottomRight,
} from './styled';
import Btn from '../common/Btn';
import { useState } from 'react';
import axios from 'axios';
import LoginTextField from '../common/LoginTextField';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import LoginFailMessage from './LoginFailMessage';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const isDisabled = !email.includes('@') || password.length < 4;
const btnState = isDisabled ? 'white' : 'orange';
const router = useRouter();
const showModal = () => {
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
};
const handleEmailChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
setEmail(event.currentTarget.value);
};
const handlePasswordChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
setPassword(event.currentTarget.value);
};
const handleSignin = async (): Promise<void> => {
if (!email.includes('@') || password.length < 4) {
}
try {
const response = await axios.post(
'/api/v1/user/login',
{
email,
password,
}
);
const accessToken = response.data.data.accessToken;
localStorage.setItem('accessToken', accessToken);
router.push('/');
window.location.replace('/');
console.log('로그인성공');
} catch (error) {
console.log('실패하였습니다', error);
showModal();
}
return;
};
return (
<Section>
<LoginText>로그인</LoginText>
<Wrapper>
<LoginTextField
type="email"
placeholder="이메일"
value={email}
onChange={handleEmailChange}
/>
<LoginTextField
type="password"
placeholder="비밀번호"
value={password}
onChange={handlePasswordChange}
/>
<Btn
size="middle"
text="로그인"
disabled={isDisabled}
onClick={handleSignin}
state={btnState}
/>
</Wrapper>
<Box>
<Link href="/signup">
<BottomText>회원가입</BottomText>
</Link>
<BottomRight>
<Link href="/resetpassword">
<BottomText>비밀번호 찾기</BottomText>
</Link>
</BottomRight>
</Box>
{modalOpen && (
<LoginFailMessage
title={'로그인 실패'}
text={'아이디 혹은 비밀번호를 확인해주세요'}
onClick={closeModal}
/>
)}
</Section>
);
};
export default Login;
태그들은 styled-components
로 구현 된 점 참고해주세요 !
const [email, setEmail] = useState('');
const handleEmailChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
setEmail(event.currentTarget.value);
};
...
<LoginTextField
type="email"
placeholder="이메일"
value={email}
onChange={handleEmailChange}
/>
const [password, setPassword] = useState('');
const handlePasswordChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
setPassword(event.currentTarget.value);
};
...
<LoginTextField
type="password"
placeholder="비밀번호"
value={password}
onChange={handlePasswordChange}
/>
const handleSignin = async (): Promise<void> => {
if (!email.includes('@') || password.length < 4) {
}
try {
const response = await axios.post(
'/api/v1/user/login',
{
email,
password,
}
);
const accessToken = response.data.data.accessToken;
localStorage.setItem('accessToken', accessToken);
router.push('/');
window.location.replace('/');
console.log('로그인성공');
} catch (error) {
console.log('실패하였습니다', error);
showModal();
}
return;
};
if문으로 이메일은 '@'가 포함되고 비밀번호 자릿수 제한을 두어
조건 만족 시 try 문 이 실행되게 하였습니다.
async과 await을 사용하여 동기실행을 해주었습니다.
보내줄 데이터 email, password를 담고 서버에 post
해주고,
reponse의 data 안에 있는 accessToken
을 가져와서 변수 accessToken
에 저장해줍니다.
( .data.data인 이유는 서버에서 설정을 잘못했더군요 ..; )
setItem
을 사용하여 accessToken
을 저장해줍니다.
console.log를 사용하여 해당 오류를 확인하고,
만들어 두었던 모달창을 사용하여 띄워주었습니다.
여기까지 하면 플로우의 4번까지 완료했습니다 !
이메일은 '@'를 포함하고 비밀번호는 4글자를 넘기는 조건을 만족시킬 때 버튼이 회색에서 오렌지색으로 바뀌게 하겠습니다.
const isDisabled = !email.includes('@') || password.length < 4;
원하는 조건을 isDisabled
변수에 저장해주고
const btnState = isDisabled ? 'white' : 'orange';
isDisabled
의 조건이라면 즉 , '@'를 포함하지 않고 비밀번호는 4글자 이하라면 true
,
이메일은 '@'를 포함하고 비밀번호는 4글자를 넘기면 false
가 되겠죠 ?
true
라면, 버튼 색을 white
로
false
라면, 버튼 색을 orange
로 설정해주었습니다.
<Btn
size="middle"
text="로그인"
disabled={isDisabled}
onClick={handleSignin}
state={btnState}
/>
전 여기서 조건 자체도 변수에 true, false로 적용된다는 것이 신기했습니다 !
disabled={isDisabled}
무튼 여기까지 하면 특정 조건 시에만 활성화 되는 로그인 버튼 구현 완료입니다.
전체적인 흐름을 위하여 Header.tsx
와 Header.tsx의 부모 컴포넌트
의 전체 코드를 첨부하겠습니다.
Header.tsx의 부모 컴포넌트
//Header.tsx의 부모 컴포넌트
'use client';
import Footer from '@/components/common/Footer';
import Header from '@/components/common/Header';
import { Providers } from '@/redux/provider';
import { ThemeWrapper } from '@/utils/ThemeWrapper';
import { useEffect, useState } from 'react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [status, setStatus] = useState(false);
useEffect(() => {
if (localStorage.getItem('accessToken') == null) {
setStatus(false);
} else {
setStatus(true);
}
}, []);
return (
<html lang="ko">
<Providers>
<ThemeWrapper>
<body>
<header>
<Header nickname="테스트" status={status} transparent={false} />
</header>
{children}
<footer>
<Footer />
</footer>
</body>
</ThemeWrapper>
</Providers>
</html>
);
}
status
변수와 이를 변경할 setStatus
를 만들어 줍니다.
초기값을 false
로 설정하고 useEffect
으로 setStatus
를 사용할 거에요
useEffect(() => {
if (localStorage.getItem('accessToken') == null) {
setStatus(false);
} else {
setStatus(true);
}
}, []);
accessToken
이 비어있으면 즉, null 값이면 setStatus(false)를 사용하여 status 값을 false 바꿔주고
else문으로 accessToken
이 비어있지 않다면,
setStatus(true)를 사용하여 status 값을 true 바꿔줍니다.
<Header nickname="테스트" status={status} transparent={false} />
status={status}
Header에 props로 넘겨줍니다 !
그럼 여기서 넘겨준 status props를 사용하여 로그인 / 비로그인 상태를 체크해주며
헤더의 모습을 바꿔줄 거에요
Header.tsx
//Header.tsx
'use client';
import Image from 'next/image';
import {
HeaderCenterWrap,
HeaderLeftLi,
HeaderLeftUl,
HeaderLeftWrap,
HeaderRightLi,
HeaderRightUl,
HeaderStyleProps,
HeaderWrap,
} from './styled';
import Link from 'next/link';
interface HeaderProps extends HeaderStyleProps {
nickname: string;
status: boolean;
transparent: boolean;
}
const logout = () => {
let accessToken = localStorage.getItem('accessToken');
localStorage.removeItem('accessToken');
window.location.reload();
};
const Header = ({ nickname, status, transparent = false }: HeaderProps) => {
return (
<HeaderWrap $transparent={transparent}>
<HeaderCenterWrap>
<HeaderLeftWrap>
<Image
src={brand}
width={32}
height={32}
alt="brand"
style={{ cursor: 'pointer' }}
/>
<HeaderLeftUl $transparent={transparent}>
<HeaderLeftLi>omo 소개</HeaderLeftLi>
<HeaderLeftLi>기획전</HeaderLeftLi>
<HeaderLeftLi>아티클</HeaderLeftLi>
</HeaderLeftUl>
</HeaderLeftWrap>
<HeaderRightUl $transparent={transparent}>
<HeaderRightLi>
<Image
src={transparent ? whiteSearch : search}
width={24}
height={24}
alt="search"
/>
</HeaderRightLi>
<HeaderRightLi>
<Image
src={transparent ? whiteBookmark : bookmark}
width={24}
height={24}
alt="bookmark"
/>
</HeaderRightLi>
{status ? (
<>
<HeaderRightLi>{nickname}</HeaderRightLi>
<HeaderRightLi onClick={logout}>로그아웃</HeaderRightLi>
</>
) : (
<Link
href="/login"
style={{ textDecoration: 'none', color: '#2d2d2d' }}
>
<HeaderRightLi>로그인</HeaderRightLi>
</Link>
)}
</HeaderRightUl>
</HeaderCenterWrap>
</HeaderWrap>
);
};
export default Header;
status
가 true
면? (accesstoken이 있다는 말)
로그아웃을 보여주고,
:
= false
면?
로그인을 주여주기
{status ? (
<>
<HeaderRightLi>{nickname}</HeaderRightLi>
<HeaderRightLi onClick={logout}>로그아웃</HeaderRightLi>
</>
) : (
<Link
href="/login"
style={{ textDecoration: 'none', color: '#2d2d2d' }}
>
<HeaderRightLi>로그인</HeaderRightLi>
</Link>
)}
const logout = () => {
let accessToken = localStorage.getItem('accessToken.');
localStorage.removeItem('accessToken');
window.location.reload();
};
로그아웃 함수를 만들어서
getItem
을 사용하여 localStorage
의 accessToken
을 가져옵니다.
removeItem
를 사용하여 accessToken
를 삭제해주고,
페이지 리로드 ! window.location.reload();
const handleLogout = () => {
const accessToken = getAccessTokenFromLocalStorage();
instance
.post(
`/api/v1/user/logout`,
{},
{
// 빈 객체를 요청 데이터로 전달
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
.then((response) => {
console.log('로그아웃성공', response);
localStorage.removeItem('accessToken');
deleteCookie('accessToken');
window.location.reload();
})
.catch((error) => {
console.log(error, '실패하였습니다');
localStorage.removeItem('accessToken');
deleteCookie('accessToken');
});
};
헤더의 로그아웃을 누르면 위의 handleLogout
함수가 작동하도록,
<HeaderRightLi onClick={handleLogout}>로그아웃</HeaderRightLi>
이렇게 aixos를 사용한 API적용과 localstorage의 JWT 를 이용하여 로그인 / 로그아웃을 구현해봤습니다.
평소 로그인을 사용할 때 그저 api로만 통신이 되는 줄 알았는데
localstorage
에 jwt
가 저장되고 이를 사용하여 회원과 비회원을 구분한다는 것을
배우게 되었다.
보안쪽으로 신경을 쓰지 못한 부분과,
redux 와 Auth를 사용하여 로그인을 구현하는 피드백을 받고
ver2 때 리팩토링을 하며 더 나은 코드를 만들 것이다.