본문 바로가기

React

로그인&회원가입&로그아웃 기능 구현(lv.4)

reactlv4-login 프로젝트 브라우저 화면 & 프로젝트 폴더 구성

features : 구현해야 할 기능

  • 로그인, 회원가입 페이지를 각각 구현합니다.
  • 아이디와 비밀번호가 모두 입력되지 않으면, API 요청을 보내지 않도록 합니다.
  • 서버의 에러를 alert 또는 직접 만든 모달 등을 통해 유저에게 표시합니다.
    • id가 중복된 경우
    • 존재하지 않는 아이디를 입력한 경우
    • 비밀번호가 잘못된 경우
  • 로그인을 하지 않은 경우에는 로그인/회원가입 페이지만 접근 가능합니다.
  • 로그인을 이미 한 경우 로그인/회원가입 페이지는 접근 할 수 없습니다.
  • 로그아웃 기능을 구현합니다.
  • [구현하지 못한 부분]
  • JWT의 유효시간이 만료된 경우, 유저에게 재로그인을 할 것을 표시합니다.
  • 로그아웃 기능구현(새로고침X, 쿠키 토큰 활용)
  • 새벽 5시 마다, 모든 데이터 초기화

참고사항

  • mock 서버에서 발급된 JWT의 유효시간은 60분입니다.
  • mock 서버 URL : http://3.38.191.164/
  • mock 서버 API 명세 ( 유저 인증확인 부분 제대로 구현한 건지 확인 필요 )
     

mock 서버 API 명세

 

// src/axios/api.js

import axios from "axios";

// mock 서버 URL : http://3.38.191.164/
// axios.create의 입력값으로 들어가는 객체는 configuration 객체에요.
const instance = axios.create({
	baseURL: "http://3.38.191.164/",
});

export default instance;
// src/pages/HomePage.jsx

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { StDiv, StBtn } from './styled';
import { Cookies } from 'react-cookie';
import api from '../axios/api';

export default function HomePage() {
    const navigate = useNavigate();
    const cookies = new Cookies();
    // const accessToken = cookies.get('token');

// 토큰 만료 추가 코딩
// JWT의 유효시간이 만료된 경우, 유저에게 재로그인을 할 것을 표시

    // 로그아웃 기능을 구현합니다.
    const logoutHandler = async () => {
        cookies.remove('token');
        navigate('/');
        window.location.reload(); // 새로고침 코드
        // 새로고침을 해야 쿠키에서 토큰이 사라짐
        // 새로고침을 안해도 사라지게 하는 방법
        try {
            // mock 서버 API 명세 //
            // 기능: 유저 인증확인 // method: get // url: user // request: header authorization: string
            //  response: 200 {message: “인증에 성공한 요청입니다.”} // error: 401
            const accessToken = cookies.get('token'); // 토큰을 가져옵니다.
            // 로그인을 하지 않은 경우에는 로그인/회원가입 페이지만 접근 가능합니다.
            if (!accessToken) {
                // 토큰이 없으면 로그아웃 처리
                navigate('/');
                return;
            }
            const response = await api.get('/user', { headers: { Authorization: accessToken } }); // 실제 토큰으로 요청
            console.log(response);
            navigate('/');
        } catch (error) {
            if (error.response.status === 401 && error.response.data.message.includes('토큰은 60분간 유지')) {
                // alert(error.response.data.message); // 토큰이 만료되었습니다. 토큰은 60분간 유지됩니다.
                alert('아이디가 만료되었습니다.'); // 어디서 확인하지?
            } else if (error.response.status === 401 && error.response.data.message.includes('존재하지 않습니다.')) {
                alert('존재하지 않는 아이디입니다.'); 
            } // header에 authorization 정보가 존재하지 않습니다. // token value가 존재하지 않습니다.
        } // 5가지 error 모두 나타내야 하는지??
    };

    return (
        <StDiv>
            <h1>Home</h1>
            <h1>무엇을 할까요?</h1>
            <div>
                <StBtn onClick={logoutHandler}>로그아웃</StBtn>
            </div>
        </StDiv>
    );
}
src/pages/LoginPage.jsx

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { StDiv, StDiv2, StInput, TextDiv, StBtn } from './styled';
import api from '../axios/api';
import { Cookies } from 'react-cookie';

// 로그인 페이지
export default function LoginPage() {
    const navigate = useNavigate();
    const cookies = new Cookies();
    const accessToken = cookies.get('token');
    const [inputValue, setInputValue] = useState({
        id: '',
        password: '',
    });
    
    // 로그인을 이미 한 경우 로그인/회원가입 페이지는 접근 할 수 없습니다.
    useEffect(() => {
        if (accessToken) {
            navigate('/home');
        } // 토큰 만료 추가 코딩
        // JWT의 유효시간 60분이 만료된 경우, 유저에게 재로그인을 할 것을 표시합니다
    }, []);

    const loginHandler = async () => {
        // 아이디와 비밀번호가 모두 입력되지 않으면, API 요청을 보내지 않도록 합니다.
        if (inputValue.id === '' && inputValue.password === '') {
            alert('아이디와 비밀번호를 입력해주세요!');
            return;
        }
        try {
            // mock 서버 API 명세 // 
            // 유저 로그인 // method: post // url: login // request: inputValue(id, password) 
            //  response: 201 token // error: 401 (4가지 경우)
            const response = await api.post('/login', inputValue);
            const accessToken = response.data.token; // 리액트 쿠키에 저장
            cookies.set('token', accessToken, { maxAge: 60 * 60, path: '/' });
            navigate('/home');
        } catch (error) {
            // 서버 에러1. 존재하지 않는 아이디를 입력한 경우
            if (error.response.status === 401 && error.response.data.message.includes('존재하지 않는 유저')) {
                // alert(error.response.data.message);
                alert('존재하지 않는 아이디입니다.'); // 서버 에러2. 비밀번호가 잘못된 경우
            } else if (
                error.response.status === 401 &&
                error.response.data.message === '비밀번호가 일치하지 않습니다.'
            ) {
                alert(error.response.data.message); // 비밀번호가 일치하지 않습니다.
                // 서버 에러3. 아이디와 비밀번호가 모두 입력되지 않은 경우
            } else if (error.response.status === 401 && error.response.data.message.includes('존재하지 않습니다.')) {
                alert(error.response.data.message); // id 또는 password가 존재하지 않습니다.
            } // "401 id 또는 password가 string이 아닙니다. 구현하지 않음" status가 201로 뜸
        }
    };
    // 리액트 login, 회원가입 검색
    // 리액트 토큰 쿠키 저장 검색

    return (
        <StDiv>
            <h1>로그인 하기</h1>
            <StDiv2>
                <TextDiv>아이디</TextDiv>
                <StInput
                    type="text"
                    value={inputValue.id}
                    onChange={(e) => {
                        setInputValue({ ...inputValue, id: e.target.value });
                    }}
                    placeholder="아이디를 입력하세요"
                ></StInput>
                <TextDiv>비밀번호</TextDiv>
                <StInput
                    type="password"
                    value={inputValue.password}
                    onChange={(e) => {
                        setInputValue({ ...inputValue, password: e.target.value });
                    }}
                    placeholder="비밀번호를 입력하세요"
                ></StInput>
                <StBtn onClick={() => loginHandler()}>로그인</StBtn>
                <StBtn onClick={() => navigate('/signup')}>회원가입</StBtn>
            </StDiv2>
        </StDiv>
    );
}
src/pages/SignUpPage.jsx

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { StDiv, StDiv2, StInput, TextDiv, StBtn } from './styled';
import api from '../axios/api';
import { Cookies } from 'react-cookie';

// 회원가입 페이지
export default function SignUpPage() {
    const navigate = useNavigate();
    const cookies = new Cookies();
    const accessToken = cookies.get('token');
    const [inputValue, setInputValue] = useState({
        id: '',
        password: '',
    });

    useEffect(() => {
        // console.log('+++', accessToken);
        if (accessToken) {
            navigate('/home');
        } // 토큰 만료 추가 코딩
        // JWT의 유효시간이 만료된 경우, 유저에게 재로그인을 할 것을 표시합니다
    }, []);

    const signUpHandler = async () => {
        // 아이디와 비밀번호가 모두 입력되지 않으면, API 요청을 보내지 않도록 합니다.
        if (inputValue.id === '' && inputValue.password === '') {
            alert('아이디와 비밀번호를 입력해주세요!');
            return;
        }
        try {
            // mock 서버 API 명세 // 
            // 기능: 유저 회원가입 // method: post // url: register // request: inputValue(id, password) 
            //  response: 201, token(없음) // error: 401 (3가지 경우)
            const response = await api.post('/register', inputValue);
            console.log(response);
            navigate('/');
        } catch (error) {
            if (error.response.status === 401 && error.response.data.message.includes('이미 존재하는 유저')) {
                // alert(error.response.data.message);
                alert("이미 존재하는 아이디입니다.");
            } else if (error.response.status === 401 && error.response.data.message.includes('존재하지 않습니다.')) {
                alert(error.response.data.message); // id 또는 password가 존재하지 않습니다.
            } // "401 id 또는 password가 string이 아닙니다. 구현하지 않음" status가 201로 뜸
        }
    };

    return (
        <StDiv>
            <h1>회원가입</h1>
            <StDiv2>
                <TextDiv>아이디</TextDiv>
                <StInput
                    type="text"
                    value={inputValue.id}
                    onChange={(e) => {
                        setInputValue({ ...inputValue, id: e.target.value });
                    }}
                    placeholder="아이디를 입력하세요"
                ></StInput>
                <TextDiv>비밀번호</TextDiv>
                <StInput
                    type="password"
                    value={inputValue.password}
                    onChange={(e) => {
                        setInputValue({ ...inputValue, password: e.target.value });
                    }}
                    placeholder="비밀번호를 입력하세요"
                ></StInput>
                <StBtn onClick={signUpHandler}>회원가입</StBtn>
                <StBtn onClick={() => navigate('/')}>로그인하기</StBtn>
            </StDiv2>
        </StDiv>
    );
}
src/pages/styled.js

import styled from 'styled-components';

export const StDiv = styled.div`
    height: 79vh;
    width: 90%;
    display: flex;
    flex-direction: column;
    gap: 50px;
    -webkit-box-pack: center;
    justify-content: center;
    padding: 0px 12px;
`;
export const StDiv2 = styled.div`
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 20px;
`;
export const StInput = styled.input`
    box-sizing: border-box;
    height: 46px;
    width: 100%;
    outline: none;
    border-radius: 3px;
    padding: 0px 12px;
    font-size: 14px;
    border: 1px solid rgb(238, 238, 238);
`;
export const TextDiv = styled.div`
    box-sizing: border-box;
    padding: 0px;
    margin: 0px;
    text-decoration: none;
    outline: none;
    font-family: "Noto Sans KR", sans-serif;
`;
export const StBtn = styled.button`
    display: flex;
    -webkit-box-align: center;
    align-items: center;
    -webkit-box-pack: center;
    justify-content: center;
    flex-direction: row;
    flex-shrink: 0;
    border: 1px solid rgb(238, 238, 238);
    color: rgb(255, 255, 255);
    height: 46px;
    border-radius: 8px;
    background-color: gray;
    cursor: pointer;
    width: 100%;
`
//src/App.jsx

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from "react-router-dom";
import LoginPage from './pages/LoginPage';
import SignUpPage from './pages/SignUpPage';
import HomePage from './pages/HomePage';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<LoginPage />} />
				<Route path="/signup" element={<SignUpPage />} />
				<Route path="/home" element={<HomePage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
// src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

✅전체코드(githup 주소) : https://github.com/webcreastory/React-4-Login-Project.git

 

GitHub - webcreastory/React-4-Login-Project

Contribute to webcreastory/React-4-Login-Project development by creating an account on GitHub.

github.com

 

특정 유저 (예: 비로그인 유저)의 페이지 접근을 제한하기 위한 전략이나 방식은 무엇이었나?

홈페이지(HomePage) 컴포넌트에서 사용자가 로그인 상태인지 확인하고, 로그인하지 않은 경우 다시 로그인 페이지로 보내는 방식을 구현하고 있습니다.

토큰 확인: 컴포넌트가 처음 렌더링될 때 쿠키에서 접근 토큰을 가져와서 (const accessToken = cookies.get('token');). 만약 토큰이 없다면, 사용자는 로그인되지 않은 상태로 간주합니다.

useEffect: useEffect 훅은 빈 의존성 배열([ ])과 함께 사용하였고 이는 초기 렌더링 후 한 번만 실행된다는 뜻으로 토큰의 존재 여부를 확인할 수 있습니다. 토큰이 없으면(!accessToken), 사용자를 다시 로그인 페이지로 보냅니다.

로그아웃: logoutHandler 함수는 쿠키에서 토큰을 제거하고 사용자를 로그인 페이지로 이동시키며 창을 새로고침합니다. 이렇게 함으로써 토큰을 제거하고 사용자를 로그인 페이지로 다시 보내 로그아웃하는 방식입니다.

 

API 요청과 같은 비동기 작업 중 발생할 수 있는 에러에 대비해 에러 핸들링을 구현했나? 구현했다면, 어떠한 방법을 사용하셨나?

try-catch 블록을 사용하여 비동기 작업 중 발생할 수 있는 에러에 대비하였습니다. loginHandler와 signUpHandler 함수 내부에서 API 요청을 수행할 때 try-catch를 활용하고 있으며 loginHandler 함수에서는 try 블록으로 API 요청을 시도하고, 만약 요청이 실패하면 catch 블록으로 넘어가 에러를 처리합니다. 예를들어, 서버에서 401 Unauthorized 에러가 발생하고 특정 메시지를 포함하고 있다면 해당 메시지를 사용자에게 알림(alert)으로 보여주는 방식으로 처리하고 있습니다.

이와 비슷한 방식으로 signUpHandler 함수도 에러 핸들링을 구현하고 있습니다. 여기서도 비동기 요청을 시도하고, 요청이 실패할 경우에 대비하여 에러에 따른 메시지를 사용자에게 보여주거나 적절히 처리하고 있습니다.

 

JWT 토큰은 무엇인가요?

JWT(JSON Web Token)는 웹에서 정보를 안전하게 전송하기 위한 표준 방법 중 하나입니다. 이는 JSON 형태로 정보를 안전하게 전송하기 위한 경량화된 자가 수용적인 토큰입니다. 서버가 한 번 사용자를 인증하고 JWT를 발급하면, 클라이언트는 이 토큰을 저장해두고 각 요청 시마다 헤더에 포함하여 서버에 보내어 인증을 수행할 수 있습니다.