[Next13 + TypeScript ] API, JWT(accesstoken) 사용하여 로그인&로그아웃 구현하기

ezi·2023년 9월 5일
0

⛴️ Api의 명세는 Swagger 를 사용하였습니다.

백엔드에서 만든 로그인 API 를 사용하여 axios로 post 하기
Swagger 를 사용하면 데이터의 request 형식을 알 수 있기 때문에 백엔드와 api 소통이 편리합니다.


🌊 구현 Flow를 알려드리자면,

  1. 이메일(or 아이디 : 원하시는 걸로) 과 비밀번호를 적어 로그인 버튼을 누르면

  2. 서버로 데이터(이메일, 비밀번호)를 보내주고

  3. 서버에선 회원가입이 된 계정이 로그인 성공을 함을 인증해주는 accesstoken을 발급해줍니다.
    ( accesstoken은 저장하여 console에 찍어보면 확인 가능합니다.)

  4. 이 accesstoken을 localstorage에 저장해주고

  5. localstorage에 accesstoken가 있다면 ? 로그인 상태

  6. localstorage에 accesstoken가 없다면 ? 비로그인 상태

  7. 로그인 상태일 때 로그아웃을 하는 방법

여기서 프론트가 해야 하는 일은 1, 2, 4, 5, 6, 7 입니다.


먼저 목업작업은 끝난 상태에서, axios 를 사용하여 데이터를 post 하는 과정을 알려드릴게요.


아래와 같이 이메일과 비밀번호를 적는 input 과 로그인 버튼이 있습니다.


이메일과 비밀번호에 적은 데이터가 로그인 버튼을 누르면 login APIpost 할 거에요.

전체적인 흐름을 위해 구현 코드를 먼저 첨부하겠습니다.

🦭 구현 코드

//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로 구현 된 점 참고해주세요 !


1. useState를 사용하여 email 관리하기

	const [email, setEmail] = useState('');

    const handleEmailChange = (
      event: React.ChangeEvent<HTMLInputElement>
    ): void => {
      setEmail(event.currentTarget.value);
    };


...

    <LoginTextField
              type="email"
              placeholder="이메일"
              value={email}
              onChange={handleEmailChange}
            />


2. useState를 사용하여 password 관리하기

	const [password, setPassword] = useState('');
	
    const handlePasswordChange = (
      event: React.ChangeEvent<HTMLInputElement>
    ): void => {
      setPassword(event.currentTarget.value);
    };

...

	<LoginTextField
          type="password"
          placeholder="비밀번호"
          value={password}
          onChange={handlePasswordChange}
        />

3.로그인 버튼 클릭 시 axios.post


  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을 저장해줍니다.

catch문을 사용하여 만약 서버와의 통신이 실패했을때,

console.log를 사용하여 해당 오류를 확인하고,
만들어 두었던 모달창을 사용하여 띄워주었습니다.

여기까지 하면 플로우의 4번까지 완료했습니다 !

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}

무튼 여기까지 하면 특정 조건 시에만 활성화 되는 로그인 버튼 구현 완료입니다.


5. localstorage에 accesstoken가 있다면 ? 로그인 상태

6. localstorage에 accesstoken가 없다면 ? 비로그인 상태

전체적인 흐름을 위하여 Header.tsxHeader.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;

statustrue 면? (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 을 사용하여 localStorageaccessToken 을 가져옵니다.

removeItem 를 사용하여 accessToken 를 삭제해주고,

페이지 리로드 ! window.location.reload();

로그아웃 Api 연결

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로만 통신이 되는 줄 알았는데

localstoragejwt 가 저장되고 이를 사용하여 회원과 비회원을 구분한다는 것을

배우게 되었다.

아쉬운 점

보안쪽으로 신경을 쓰지 못한 부분과,

redux 와 Auth를 사용하여 로그인을 구현하는 피드백을 받고

ver2 때 리팩토링을 하며 더 나은 코드를 만들 것이다.

profile
차곡차곡

0개의 댓글