장점
단점
장점
단점
import axios from 'axios';
import { getGlobalModal } from '../context/ModalContext';
import { getGlobalNavigate } from '../service/NavigationService';
// Axios 인스턴스 생성
const axiosInstance = axios.create({
baseURL: '/main-dev', // 프록시 경로
//timeout: 5000, // 요청 타임아웃 설정 (ms)
headers: {
'Content-Type': 'application/json',
},
});
// 토큰 갱신
const refreshAccessToken = async() => {
const response = await axiosInstance.post('/auth/refresh'
, { token : localStorage.getItem('refreshToken')}
);
// refresh token expire 403 Error
if(response.data.status === 403){
return false;
}
const responseHeader = response.headers;
const responseData = response.data.data;
localStorage.clear();
localStorage.setItem('accessToken', responseHeader.authorization);
localStorage.setItem('refreshToken', responseData.token);
axiosInstance.defaults.headers.common['Authorization'] = `${responseHeader.authorization}`;
return true;
}
// 요청 인터셉터
axiosInstance.interceptors.request.use(
(config) => {
// 요청 헤더 설정
const accessToken = localStorage.getItem('accessToken');
//console.log(accessToken);
if (accessToken) {
config.headers.Authorization = `${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 공통 요청 함수
const axiosRequest = async (method, url, data = null, fn_callback, bResponseOnlyData = true) => {
const openModal = getGlobalModal();
try {
const response = await axiosInstance({
method,
url,
data,
});
if(response == null){
return ;
}
var res = response;
console.log(res);
if(bResponseOnlyData){
res = res.data.data;
}
if(response.data.status !== 200){
openModal("E", `[${response.data.status}] ${response.data.message}`);
return;
}
if (fn_callback && typeof fn_callback === "function") {
fn_callback(res);
}
} catch (error) {
fn_axiosError(error, openModal);
}
};
const fn_axiosError = async(error, openModal) => {
const navigate = getGlobalNavigate();
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // 중복 호출 방지
const tokenRefreshed = await refreshAccessToken();
if (tokenRefreshed) {
return axiosInstance(originalRequest); // 새 토큰으로 재시도
}
// 토큰 갱신 실패 시 로그인 페이지로 이동
openModal("A", '로그인 토큰이 만료되었습니다');
localStorage.clear();
navigate('/Login');
return;
}
// 기타 에러 처리
console.log(error);
openModal("E", `Transaction failed: ${error.message}`);
}
export default axiosRequest; // 기본 익스포트 사용
Access Token 재발급
import React, { useEffect } from 'react';
import { BrowserRouter, Route, Routes, Navigate, useNavigate } from 'react-router-dom';
import CombinedProvider from './context/CombinedProvider';
import { setGlobalNavigate } from './service/NavigationService';
import LoginPage from './pages/LoginPage';
import MainPage from './pages/MainPage';
const App = () => {
const NavigateInitializer = () => {
const navigate = useNavigate();
useEffect(() => {
setGlobalNavigate(navigate);
}, [navigate]);
return null; // 이 컴포넌트는 UI를 렌더링하지 않음
};
return (
<CombinedProvider>
<BrowserRouter>
<NavigateInitializer />
<Routes>
<Route path="/" element={<LoginPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/main" element={<MainPage />} />
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
</BrowserRouter>
</CombinedProvider>
);
}
export default App;
let globalNavigate = null;
export const setGlobalNavigate = (navigate) => {
globalNavigate = navigate;
};
export const getGlobalNavigate = () => {
if (!globalNavigate) {
throw new Error("Navigate function is not set. Make sure to initialize it.");
}
return globalNavigate;
};
axios.js에서 navigate를 사용하기 위해 전역으로 관리
import React, { useEffect, useState } from 'react';
import "./LoginPage.css";
import axiosRequest from '../api/axios';
import { getGlobalNavigate } from '../service/NavigationService';
import { useModal } from '../context/ModalContext';
import { useAuth } from '../context/AuthContext';
function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState('');
const navigate = getGlobalNavigate();
const { openModal } = useModal();
const { login, logout } = useAuth();
useEffect(() => {
localStorage.clear();
logout();
}, []);
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
};
const inputValidation = () => {
if(!email){
openModal("A", "이메일을 입력해주세요.");
return false;
}
if(!password){
openModal("A", "비밀번호 입력해주세요.");
return false;
}
return true;
}
const handleLogin = (e) => {
//<form> 태그의 기본 동작은 페이지 새로고침(reload)을 발생시키므로, onSubmit에서 기본 동작을 막아야함
e.preventDefault();
if(!inputValidation()){
return;
}
axiosRequest("post" //method
, "/auth/login" //url
, { email, password } //data
, fn_loginCallback //callback
, false //true : OnlyData, false : Original Response
);
};
const fn_loginCallback = (response) => {
const responseHeader = response.headers;
const responseData = response.data.data;
// localStorage에 토큰정보 저장
localStorage.clear();
localStorage.setItem('accessToken', responseHeader.authorization);
localStorage.setItem('refreshToken', responseData.refreshToken);
responseData.refreshToken = "";
login(responseData);
//메인화면 이동
navigate('/main');
}
return (
<div className="login-container">
<h2>로그인</h2>
<form onSubmit={handleLogin} className="login-form">
<div className="input-group">
<label htmlFor="email">이메일</label>
<input
type="email"
id="email"
value={email}
onChange={handleEmailChange}
className="input-field"
/>
</div>
<div className="input-group">
<label htmlFor="password">비밀번호</label>
<input
type="password"
id="password"
value={password}
onChange={handlePasswordChange}
className="input-field"
/>
</div>
<button type="submit" className="login-btn" id="default-login">
로그인
</button>
</form>
<div>
<button type="submit" className="login-btn" id="kakao-login">
카카오 로그인
</button>
<button type="submit" className="login-btn" id="naver-login">
네이버버 로그인
</button>
</div>
</div>
);
}
export default LoginPage;
.login-container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
}
h2 {
text-align: center;
margin-bottom: 20px;
}
/* 폼 스타일 */
.login-form {
display: flex;
flex-direction: column;
}
.input-group {
margin-bottom: 15px;
margin-right: 15px;
}
label {
font-weight: bold;
margin-bottom: 5px;
display: block;
}
.input-field {
width: 100%;
padding: 8px;
margin-top: 5px;
border-radius: 4px;
border: 1px solid #ccc;
}
.error-message {
color: red;
margin-bottom: 15px;
font-size: 14px;
}
.login-btn {
width: 100%;
padding: 10px;
margin: 4px 4px 4px 4px;
border: none;
border-radius: 4px;
cursor: pointer;
}
#default-login {
background-color: #AAAAAA;
color: white;
}
#default-login:hover {
background-color: black;
color: yellow;
}
#kakao-login {
background-color: yellow;
color: black;
}
#kakao-login:hover {
background-color: black;
color: yellow;
}
#naver-login {
background-color: #4CAF50;
color: white;
}
#naver-login:hover {
background-color: black;
color: yellow;
}
import React, { useEffect, useState, lazy } from 'react';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
import './MainPage.css';
function MainPage() {
const navigate = useNavigate();
const { user, logout } = useAuth();
const [isMenuOpen, setIsMenuOpen] = useState(true); // 메뉴 열림/닫힘 상태
const [menuItems, setMenuItems] = useState([]); // 메뉴 데이터
const [selectedMenu, setSelectedMenu] = useState(null); // 선택된 메뉴
const [selectedComponent, setSelectedComponent] = useState(null); // 동적 컴포넌트
useEffect(() => {
if(user == null){
fn_logout();
}else{
fn_init();
}
}, []);
useEffect(() => {
if (selectedMenu) {
// 선택된 메뉴의 컴포넌트 로드
const menu = menuItems.find(item => item.id === selectedMenu.id);
if (menu) {
const Component = lazy(() => import(`../components/${menu.componentPath}`));
setSelectedComponent(<Component />);
}
}
}, [selectedMenu, menuItems]);
const fn_toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
const fn_init = () => {
fn_searchMenuItems();
};
const fn_searchMenuItems = async () => {
// DB에서 메뉴 데이터 가져오기
var db = [
{ "id": "1", "menuName": "Auth Test", "componentPath": "AuthTest.js" },
{ "id": "2", "menuName": "Props and State Example", "componentPath": "ParentComp.js" }
];
setMenuItems(db);
};
const fn_logout = () => {
localStorage.clear();
logout();
navigate('/Login');
}
const handleLogout = (e) => {
fn_logout();
};
return (
<div className="main-page">
<header className="header">
<div className='userInfo'>
{user && (
<>
<div>{user.userId} / {user.userName}</div>
<button className="logout-btn" onClick={handleLogout}>
Logout
</button>
</>
)}
</div>
</header>
<div className="content">
<button className="toggle-btn" onClick={fn_toggleMenu}>
{isMenuOpen ? '◀' : '▶'}
</button>
<aside className={`left ${isMenuOpen ? 'open' : 'closed'}`}>
<ul className="menu-item">
{menuItems.map((item, index) => (
<li key={index} onClick={() => setSelectedMenu(item)}>
{item.menuName}
</li>
))}
</ul>
</aside>
<main className="main">
{selectedMenu && (<h1>{selectedMenu.menuName}</h1>)}
{selectedComponent}
</main>
</div>
<footer className="footer">Footer</footer>
</div>
);
}
export default MainPage;
동적으로 컴포넌트를 불러올 수 있게 해주는 함수
이 기능을 사용하면 컴포넌트의 코드를 처음 렌더링될 때까지 로딩을 지연시킬 수 있음
.main-page {
display: flex;
flex-direction: column;
height: 100vh;
}
/*Header Area Start*/
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #4CAF50;
border-bottom: 1px solid #ddd;
}
.userInfo {
display: flex;
align-items: center;
justify-content: flex-end; /* 요소를 오른쪽으로 정렬 */
flex-grow: 1; /* 요소가 남는 공간을 차지하게 함 */
}
.logout-btn {
margin-left: 20px;
padding: 5px 10px;
background-color: #ff4b4b;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.logout-btn:hover {
background-color: #e04343;
}
/*Header Area End*/
/*content Area Start*/
.content {
display: flex;
flex: 1;
position: relative;
}
.toggle-btn {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: #f0f0f0;
border: none;
padding: 0;
cursor: pointer;
}
.left {
width: 200px;
background-color: #f4f4f4;
padding: 10px;
transition: all 0.3s ease;
}
.left.closed {
width: 0;
overflow: hidden;
}
.menu-item {
cursor: pointer;
}
/*content Area End*/
.main {
flex: 1;
padding: 10px;
}
.footer {
background-color: #ddd;
color: black;
padding: 10px;
text-align: center;
}
기본 ID/PW를 사용한 로그인은 구현되었다. 이후 OAuth를 사용한 로그인을 구현해 보자.
이전글
다음 글
React Modal 구현하기 (Alter, Loading) (1) | 2025.02.08 |
---|---|
React axios 모듈화 (0) | 2025.02.07 |
React-Router-Dom 정리 (0) | 2025.02.06 |
React Hook 기본 정리 / Custom Hook (1) | 2025.02.05 |
React CORS 설정 - 프록시 (http-proxy-middleware) (7) | 2025.01.31 |
댓글 영역