Front-end/React

React로 로그인 기능 구현하기 (1): 프론트엔드 처리 과정 🚀

xeunnie 2024. 6. 30. 22:31
728x90
반응형

로그인 기능 구현 (1): 프론트엔드 처리 과정 🚀

로그인 기능은 거의 모든 웹 애플리케이션에서 필수적으로 사용됩니다. 이번 글에서는 React를 사용하여 로그인 기능을 구현하는 과정을 친절히 설명합니다. 토큰 기반 인증 방식상태 관리를 이해하며, 로그인 로직을 어떻게 처리해야 하는지 단계별로 살펴보겠습니다.


0️⃣ 로그인 기능의 기본 원리

로그인 기능은 사용자가 입력한 아이디와 비밀번호를 인증 서버에 전달하여 유효성을 검증한 후, 성공적으로 인증되었을 때 토큰을 반환받는 방식으로 작동합니다.

로그인 흐름 🔄

  1. 사용자가 로그인 폼에 아이디와 비밀번호를 입력합니다.
  2. 입력 데이터를 서버로 전송하여 인증을 요청합니다.
  3. 서버는 데이터를 검증하고, 유효한 경우 Access TokenRefresh Token을 반환합니다.
  4. 클라이언트는 Access Token을 로컬 스토리지 또는 쿠키에 저장하여 인증된 상태를 유지합니다.
  5. 토큰을 사용해 서버와 통신하며 인증이 필요한 요청을 처리합니다.

1️⃣ 프로젝트 준비: 개발 환경 설정

React 프로젝트에서 로그인 기능을 구현하려면 기본적인 설정이 필요합니다.

 

필요한 패키지 설치

React 프로젝트를 시작하고 필요한 라이브러리(axios)를 설치합니다.

npm install axios
  • axios: HTTP 요청을 보내기 위한 라이브러리
    사실 axios 이외에 ajax나 fetch도 있는데, 로그인 기능이 메인 스레드인 만큼 axios를 쓸 예정이다.

폴더 구조 제안

로그인 관련 코드를 깔끔히 관리하려면 폴더 구조를 정리하는 것이 중요합니다.

src/
├── components/
 │         ├── LoginForm.jsx # 로그인 폼 UI
 │         └── ProtectedRoute.jsx # 인증이 필요한 라우팅 처리
├── services/
 │        └── authService.js # 로그인 API 요청 처리
├── utils/
 │        └── tokenStorage.js # 토큰 저장 및 관리
└── App.js # 앱 엔트리포인트

2️⃣ LoginForm 컴포넌트 구현

(제일 중요한 부분) 로그인 폼을 구현하는 LoginForm 컴포넌트를 생성한다.

LoginForm.jsx

import React, { useState } from 'react';

function LoginForm({ onLogin }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onLogin({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">이메일</label>
        <input
          type="email"
          id="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label htmlFor="password">비밀번호</label>
        <input
          type="password"
          id="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      <button type="submit">로그인</button>
    </form>
  );
}

export default LoginForm;

3️⃣ API와 통신: 로그인 요청 처리

사용자가 로그인 폼에 데이터를 입력하면 서버로 요청을 보내야 합니다. Axios를 사용하여 API 통신을 구현합니다.

authService.js

import axios from 'axios';

const API_URL = 'https://example.com/api'; // 서버의 API 주소

export const login = async ({ email, password }) => {
  try {
    const response = await axios.post(`${API_URL}/login`, { email, password });
    return response.data; // 서버에서 반환된 데이터 (토큰 등)
  } catch (error) {
    console.error('로그인 요청 실패:', error);
    throw error;
  }
};

4️⃣ 로그인 상태 관리 및 토큰 저장

토큰 저장하기

React에서는 로컬 스토리지 또는 쿠키를 사용해 토큰을 저장할 수 있습니다.

tokenStorage.js

export const saveToken = (token) => {
  localStorage.setItem('accessToken', token);
};

export const getToken = () => {
  return localStorage.getItem('accessToken');
};

export const removeToken = () => {
  localStorage.removeItem('accessToken');
};

 

onLogin 함수 구현

onLogin 함수는 LoginForm 컴포넌트에서 호출되어 로그인 요청과 토큰 저장을 처리합니다.

 

App.js

import React from 'react';
import LoginForm from './components/LoginForm';
import { login } from './services/authService';
import { saveToken } from './utils/tokenStorage';

function App() {
  const handleLogin = async (credentials) => {
    try {
      const { token } = await login(credentials);
      saveToken(token); // 토큰 저장
      alert('로그인 성공!');
    } catch (error) {
      alert('로그인 실패. 다시 시도하세요.');
    }
  };

  return <LoginForm onLogin={handleLogin} />;
}

export default App;

5️⃣ Protected Route 구현

Protected Route는 인증되지 않은 사용자가 특정 페이지에 접근하지 못하도록 보호하는 역할을 합니다.

작동 원리

  • 사용자가 특정 페이지에 접근하려 할 때 로그인 여부를 확인합니다.
  • 로그인이 되어 있지 않으면 로그인 페이지로 리다이렉트합니다.

ProtectedRoute.jsx

import React from 'react';
import { Navigate } from 'react-router-dom';
import { getToken } from '../utils/tokenStorage';

function ProtectedRoute({ children }) {
  const token = getToken(); // 저장된 토큰 확인

  if (!token) {
    // 로그인 상태가 아니면 로그인 페이지로 리다이렉트
    return <Navigate to="/login" replace />;
  }

  return children; // 로그인 상태면 원래의 컴포넌트 렌더링
}

export default ProtectedRoute;

사용 방법

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ProtectedRoute from './components/ProtectedRoute';
import Dashboard from './pages/Dashboard';
import Login from './pages/Login';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route
          path="/dashboard"
          element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          }
        />
      </Routes>
    </Router>
  );
}

export default App;

6️⃣ 로그아웃 기능

로그아웃은 사용자의 인증 상태를 해제하고 애플리케이션을 초기화하는 중요한 기능입니다.

구현 방법

  1. 저장된 토큰을 삭제합니다.
  2. 로그아웃 시 리다이렉트하거나 상태를 초기화합니다.

LogoutButton.jsx

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { removeToken } from '../utils/tokenStorage';

function LogoutButton() {
  const navigate = useNavigate();

  const handleLogout = () => {
    removeToken(); // 저장된 토큰 삭제
    alert('로그아웃되었습니다.');
    navigate('/login'); // 로그인 페이지로 이동
  };

  return <button onClick={handleLogout}>로그아웃</button>;
}

export default LogoutButton;

7️⃣ Refresh Token 활용

Access Token은 짧은 유효기간을 가지며, 만료 시 Refresh Token을 사용해 새로 발급받아야 합니다.

작동 원리

  1. API 요청 중 Access Token이 만료되면 서버에서 401(Unauthorized)을 반환합니다.
  2. Refresh Token을 서버로 보내 새 Access Token을 요청합니다.
  3. 새로 발급받은 Access Token으로 다시 요청을 시도합니다.

authService.js

import axios from 'axios';
import { getToken, saveToken, removeToken } from '../utils/tokenStorage';

const API_URL = 'https://example.com/api';

const axiosInstance = axios.create({
  baseURL: API_URL,
});

axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response.status === 401) {
      // Access Token 만료 시
      try {
        const refreshResponse = await axios.post(`${API_URL}/refresh`, {
          refreshToken: localStorage.getItem('refreshToken'),
        });

        const newAccessToken = refreshResponse.data.accessToken;
        saveToken(newAccessToken);

        // 기존 요청 재시도
        error.config.headers.Authorization = `Bearer ${newAccessToken}`;
        return axios(error.config);
      } catch (refreshError) {
        console.error('토큰 갱신 실패:', refreshError);
        removeToken();
        window.location.href = '/login'; // 로그아웃 처리
      }
    }

    return Promise.reject(error);
  }
);

export const login = async (credentials) => {
  const response = await axiosInstance.post('/login', credentials);
  saveToken(response.data.accessToken);
  localStorage.setItem('refreshToken', response.data.refreshToken); // Refresh Token 저장
  return response.data;
};

8️⃣ 전역 상태 관리

로그인 상태를 Context API, Redux, Recoil 등으로 전역 관리하면 인증 상태를 애플리케이션 전반에서 쉽게 접근할 수 있습니다.

AuthContext.jsx

import React, { createContext, useState, useContext } from 'react';
import { getToken, saveToken, removeToken } from '../utils/tokenStorage';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(!!getToken());

  const login = (token) => {
    saveToken(token);
    setIsAuthenticated(true);
  };

  const logout = () => {
    removeToken();
    setIsAuthenticated(false);
  };

  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

사용 방법

import React from 'react';
import { useAuth } from './context/AuthContext';

function LogoutButton() {
  const { logout } = useAuth();

  return <button onClick={logout}>로그아웃</button>;
}

9️⃣ Remember Me 기능

Remember Me 기능은 사용자가 브라우저를 닫아도 인증 상태를 유지할 수 있도록 합니다.

Remember Me 체크박스 구현

import React, { useState } from 'react';

function LoginForm({ onLogin }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    onLogin({ email, password, rememberMe });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">이메일</label>
        <input
          type="email"
          id="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label htmlFor="password">비밀번호</label>
        <input
          type="password"
          id="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      <div>
        <label>
          <input
            type="checkbox"
            checked={rememberMe}
            onChange={(e) => setRememberMe(e.target.checked)}
          />
          Remember Me
        </label>
      </div>
      <button type="submit">로그인</button>
    </form>
  );
}

Remember Me 동작 로직

  • rememberMetrue이면 토큰을 로컬 스토리지에 저장하고, 그렇지 않으면 세션 스토리지에 저장합니다.
export const saveToken = (token, rememberMe) => {
  const storage = rememberMe ? localStorage : sessionStorage;
  storage.setItem('accessToken', token);
};
728x90
반응형