React x JWT x SpringSecurity
Client (React)
Server (Spring Boot)
React
•
Preview
•
라이브러리
◦
react-router-dom
◦
axios
◦
js-cookie
◦
sweetalert2
◦
sweetalert2-react-content
•
package.json
•
client 프로젝트 구조
•
contexts
•
apis
•
components
•
pages
•
Root
◦
App.css
◦
App.js
Preview
•
메인 화면
•
회원가입 화면
◦
회원가입 성공
•
로그인 화면
◦
로그인 성공
◦
로그인 실패
•
로그아웃
◦
로그아웃 성공
•
마이 페이지
◦
회원 정보 수정 성공
◦
회원 가입 성공
메인 화면
회원가입 화면
회원가입 성공
로그인 화면
로그인 성공
로그인 실패
로그아웃
로그아웃 성공
마이 페이지
회원 정보 수정 성공
회원 가입 성공
라이브러리
•
react-router-dom
•
axios
•
js-cookie
•
sweetalert2
•
sweetalert2-react-content
# React X JWT X Spring Security
## 라이브러리 설치
### router
npm install react-router-dom
### axios
npm install axios
### cookie
npm install js-cookie
### sweetalert2
npm install sweetalert2
npm install sweetalert2-react-content
Markdown
복사
package.json
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.2",
"js-cookie": "^3.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
"react-scripts": "5.0.1",
"sweetalert2": "^11.12.0",
"sweetalert2-react-content": "^5.0.7",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8080"
}
JSON
복사
client 프로젝트 구조
contexts
•
LoginContextProvider.jsx
•
LoginContextConsumer.jsx
LoginContextProvider.jsx
import React, { createContext, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import Cookies from 'js-cookie'
import api from '../apis/api'
import * as auth from '../apis/auth'
import * as Swal from '../apis/alert'
// 📦컨텍스트 생성
export const LoginContext = createContext()
const LoginContextProvider = ({ children }) => {
/* -----------------------[State]-------------------------- */
// 로그인 여부
const [isLogin, setLogin] = useState(false);
// 유저 정보
const [userInfo, setUserInfo] = useState(null)
// 권한 정보
const [roles, setRoles] = useState({isUser : false, isAmdin : false})
/* -------------------------------------------------------- */
// 페이지 이동
const navigate = useNavigate()
// 🍪➡💍 로그인 체크
const loginCheck = async () => {
// 🍪 accessToken 쿠키 확인
const accessToken = Cookies.get("accessToken")
console.log(`accessToken : ${accessToken}`);
// 💍in🍪 ❌
if( !accessToken ) {
console.log(`쿠키에 accessToken(jwt) 가 없음`);
// 로그아웃 세팅
logoutSetting()
return
}
// 💍in🍪 ⭕
console.log(`쿠키에 JWT(accessToken) 이 저장되어 있음`);
// axios common header 에 등록
api.defaults.headers.common.Authorization = `Bearer ${accessToken}`
// 👩💼 사용자 정보 요청
let response
let data
try {
response = await auth.info()
} catch (error) {
console.log(`error : ${error}`);
console.log(`status : ${response.status}`);
return
}
data = response.data // data = 👩💼 사용자 정보
console.log(`data : ${data}`);
// 인증 실패 ❌
if( data == 'UNAUTHORIZED' || response.status == 401 ) {
console.log(`accessToek(jwt) 이 만료되었거나 인증에 실패하였습니다.`);
return
}
// 인증 성공 ✅
console.log(`accessToken(jwt) 토큰으로 사용자 정보 요청 성공!`);
// 로그인 세팅
loginSetting( data, accessToken )
}
// 🔐 로그인
const login = async (username, password) => {
console.log(`username: ${username}`);
console.log(`password: ${password}`);
try {
const response = await auth.login(username, password)
const data = response.data
const status = response.status
const headers = response.headers
const authorization = headers.authorization
// 💍 JWT
const accessToken = authorization.replace("Bearer ", "")
console.log(`data : ${data}`);
console.log(`status : ${status}`);
console.log(`headers : ${headers}`);
console.log(`jwt : ${accessToken}`);
// 로그인 성공 ✅
if( status == 200 ) {
Cookies.set("accessToken", accessToken)
// 로그인 체크
loginCheck()
Swal.alert("로그인 성공", "메인 화면으로 이동합니다", "success",
() => { navigate("/") }
)
// 메인 페이지로 이동
// navigate("/")
}
} catch (error) {
Swal.alert("로그인 실패", "아이디 또는 비밀번호가 일치하지 않습니다", "error")
console.log(`로그인 실패`);
}
}
// 🔐 로그인 세팅
// 👩💼 userData, 💍 accessToken(jwt)
const loginSetting = (userData, accessToken) => {
const { no, userId, authList } = userData // 👩💼 Users (DTO) [JSON]
const roleList = authList.map((auth) => auth.auth) // 💳 [ROLE_USER,ROLE_ADMIN]
console.log(`no : ${no}`);
console.log(`userId : ${userId}`);
console.log(`authList : ${authList}`);
console.log(`roleList : ${roleList}`);
// axios common header - Authorizaion 헤더에 jwt 등록
api.defaults.headers.common.Authorization = `Bearer ${accessToken}`
// 📦 Context 에 정보 등록
// 🔐 로그인 여부 세팅
setLogin(true)
// 👩💼 유저 정보 세팅
const updatedUserInfo = {no, userId, roleList}
setUserInfo(updatedUserInfo)
// 👮♀️ 권한 정보 세팅
const updatedRoles = { isUser : false, isAdmin : false }
roleList.forEach( (role) => {
if( role == 'ROLE_USER' ) updatedRoles.isUser = true
if( role == 'ROLE_ADMIN' ) updatedRoles.isAdmin = true
})
setRoles(updatedRoles)
}
// 로그아웃 세팅
const logoutSetting = () => {
// 🚀❌ axios 헤더 초기화
api.defaults.headers.common.Authorization = undefined;
// 🍪❌ 쿠키 초기화
Cookies.remove("accessToken")
// 🔐❌ 로그인 여부 : false
setLogin(false)
// 👩💼❌ 유저 정보 초기화
setUserInfo(null)
// 👮♀️❌ 권한 정보 초기화
setRoles(null)
}
// 🔓 로그아웃
const logout = (force=false) => {
if( force ) {
// 로그아웃 세팅
logoutSetting()
// 페이지 이동 ➡ "/" (메인)
navigate("/")
return
}
Swal.confirm("로그아웃하시겠습니까?", "로그아웃을 진행합니다.", "warning",
(result) => {
// isConfirmed : 확인 버튼 클릭 여부
if( result.isConfirmed ) {
Swal.alert("로그아웃 성공", "", "success")
logoutSetting() // 로그아웃 세팅
navigate("/") // 메인 페이지로 이동
}
}
)
// const check = window.confirm("정말로 로그아웃하시겠습니까?")
// if( check ) {
// // 로그아웃 세팅
// logoutSetting()
// // 메인 페이지로 이동
// navigate("/")
// }
}
// Mount / Update
useEffect( () => {
// 로그인 체크
loginCheck()
// 1️⃣ 🍪 쿠키에서 jwt💍 을 꺼낸다
// 2️⃣ jwt 💍 있으면, 서버한테 👩💼 사용자정보를 받아온다
// 3️⃣ 로그인 세팅을 한다. (📦 로그인여부, 사용자정보, 권한정보 등록)
}, [])
return (
// 컨텍스트 값 지정 ➡ value={ ?, ? }
<LoginContext.Provider value={ {isLogin, userInfo, roles, login, loginCheck, logout} }>
{children}
</LoginContext.Provider>
)
}
export default LoginContextProvider
JavaScript
복사
LoginContextConsumer.jsx
import React, { useContext } from 'react'
import { LoginContext } from './LoginContextProvider'
const LoginContextConsumer = () => {
const { isLogin } = useContext(LoginContext)
return (
<div>
<h3>로그인 여부 : {isLogin ? '로그인' : '로그아웃'} </h3>
</div>
)
}
export default LoginContextConsumer
JavaScript
복사
apis
•
api.js
•
auth.js
•
alert.js
api.js
import axios from 'axios'
// axios 객체 생성
const api = axios.create()
export default api
JavaScript
복사
auth.js
import api from './api';
// 로그인
export const login = (username, password) => api.post(`/login?username=${username}&password=${password}`)
// 사용자 정보
export const info = () => api.get(`/users/info`)
// 회원 가입
export const join = (data) => api.post(`/users`, data)
// 회원 정보 수정
export const update = (data) => api.put(`/users`, data)
// 회원 탈퇴
export const remove = (userId) => api.delete(`/users/${userId}`)
JavaScript
복사
alert.js
import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
/**
* icon : success, error, warning, info, question
*/
const MySwal = withReactContent(Swal)
// 기본 alert ⚠
export const alert = (title, text, icon, callback) => {
MySwal.fire({
title: title,
text: text,
icon: icon
})
.then( callback ) // 경고창 출력 이후 실행할 콜백함수
}
// confirm 👩🏫
export const confirm = (title, text, icon, callback) => {
MySwal.fire({
title: title,
text: text,
icon: icon,
showCancelButton: true,
cancelButtonColor: "#d33",
cancelButtonText: "No",
confirmButtonColor: "#3085d6",
confirmButtonText: "Yes",
})
.then( callback )
}
JavaScript
복사
components
•
Header
◦
Header.jsx
◦
Header.css
•
Join
◦
JoinForm.jsx
◦
JoinForm.css
•
Login
◦
LoginForm.jsx
◦
LoginForm.css
•
User
◦
UserForm.jsx
◦
UserForm.css
Header
Header.jsx
import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import './Header.css'
import { LoginContext } from '../../contexts/LoginContextProvider'
const Header = () => {
// 📦 LoginContext 가져오기
// 🧊 isLogin
// 🌞 logout
const { isLogin, logout } = useContext(LoginContext)
return (
<header>
<div className="logo">
<Link to="/">
<img src="https://i.imgur.com/fzADqJo.png" alt='logo' className='logo' />
</Link>
</div>
<div className="util">
<ul>
{/* 로그인 여부(isLogin)에 따라서 조건부 렌더링 */}
{
isLogin ?
<>
<li><Link to="/user">마이페이지</Link></li>
<li><button className='link' onClick={ () => logout() }>로그아웃</button></li>
</>
:
<>
<li><Link to="/login">로그인</Link></li>
<li><Link to="/join">회원가입</Link></li>
<li><Link to="/about">소개</Link></li>
</>
}
</ul>
</div>
</header>
)
}
export default Header
JavaScript
복사
Header.css
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #000;
}
.logo {
width: 80px;
}
.util ul {
display: flex;
justify-content: space-between;
align-items: center;
column-gap: 12px;
}
CSS
복사
Join
JoinForm.jsx
import React from 'react'
const JoinForm = ({ join }) => {
const onJoin = (e) => {
e.preventDefault() // submit 기본 동작 방지
const form = e.target
const userId = form.username.value
const userPw = form.password.value
const name = form.name.value
const email = form.email.value
console.log(userId, userPw, name, email);
join( {userId, userPw, name, email} )
}
return (
<div className="form">
<h2 className="login-title">Join</h2>
<form className='login-form' onSubmit={ (e) => onJoin(e) }>
<div>
<label htmlFor="name">username</label>
<input type="text"
id='username'
placeholder='username'
name='username'
autoComplete='username'
required
/>
</div>
<div>
<label htmlFor="password">password</label>
<input type="password"
id='passowrd'
placeholder='password'
name='password'
autoComplete='password'
required
/>
</div>
<div>
<label htmlFor="name">Name</label>
<input type="text"
id='name'
placeholder='name'
name='name'
autoComplete='name'
required
/>
</div>
<div>
<label htmlFor="name">Email</label>
<input type="text"
id='email'
placeholder='email'
name='email'
autoComplete='email'
required
/>
</div>
<button type='submit' className='btn btn--form btn-login'>
Join
</button>
</form>
</div>
)
}
export default JoinForm
JavaScript
복사
JoinForm.css
.form {
width: 400px;
margin: auto;
padding: 36px 48px 48px 48px;
background-color: #f2efee;
border-radius: 11px;
box-shadow: 0 2.4rem 4.8rem rgba(0, 0, 0, 0.15);
}
.login-title {
padding: 15px;
font-size: 22px;
font-weight: 600;
text-align: center;
}
.login-form {
display: grid;
grid-template-columns: 1fr;
row-gap: 16px;
}
.login-form label {
display: block;
margin-bottom: 8px;
}
.login-form input {
width: 100%;
padding: 1.2rem;
border-radius: 9px;
border: none;
}
.login-form input:focus {
outline: none;
box-shadow: 0 0 0 4px rgba(253, 242, 233, 0.5);
}
.btn--form {
background-color: #f48982;
color: #fdf2e9;
align-self: end;
padding: 8px;
}
.btn,
.btn:link,
.btn:visited {
display: inline-block;
text-decoration: none;
font-size: 20px;
font-weight: 600;
border-radius: 9px;
border: none;
cursor: pointer;
font-family: inherit;
transition: all 0.3s;
}
.btn-login:hover {
outline: 1px solid #f48982;
}
.btn--form:hover {
background-color: #fdf2e9;
color: #f48982;
}
CSS
복사
Login
LoginForm.jsx
import React, { useContext } from 'react'
import { LoginContext } from '../../contexts/LoginContextProvider'
import './LoginForm.css'
const LoginForm = () => {
const { login } = useContext(LoginContext) // 📦 LoginContext 의 login 함수
const onLogin = (e) => {
e.preventDefault() // 기본 이벤트 방지
const form = e.target // <form> 요소
const username = form.username.value // 아이디 - <form> 아래 input name="username" 의 value
const password = form.password.value // 비밀번호 - <form> 아래 input name="passwword" 의 value
login( username, password ) // 로그인 처리 요청
}
return (
<div className="form">
<h2 className="login-title">Login</h2>
<form className='login-form' onSubmit={ (e) => onLogin(e) }>
<div>
<label htmlFor="name">username</label>
<input type="text"
id='username'
placeholder='username'
name='username'
autoComplete='username'
required
/>
</div>
<div>
<label htmlFor="password">password</label>
<input type="password"
id='passowrd'
placeholder='password'
name='password'
autoComplete='password'
required
/>
</div>
<button type='submit' className='btn btn--form btn-login'>
Login
</button>
</form>
</div>
)
}
export default LoginForm
JavaScript
복사
LoginForm.css
.form {
width: 400px;
margin: auto;
padding: 36px;
background-color: beige;
border-radius: 12px;
box-shadow: 2px 2px 2px rgba(0,0,0,0.15);
}
.login-title {
text-align: center;
padding: 15px;
font-size: 22px;
font-weight: 600;
}
.login-form label {
display: block;
margin-bottom: 8px;
}
.login-form input {
width: 100%;
padding: 1.2rem;
border-radius: 10px;
border: none;
}
.login-form input:focus {
outline: none;
box-shadow: 0 0 0 4px rgba(253, 242, 233, 0.5);
}
.btn--form {
background-color: #f48982;
color: #fdf2e9;
padding: 8px;
}
.btn {
display: inline-block;
text-decoration: none;
font-size: 20px;
font-weight: 600;
border-radius: 10px;
border: none;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin-top: 12px;
}
.btn-login:hover {
outline: 1px solid #f48982;
}
CSS
복사
User
UserForm.jsx
import React from 'react'
const UserForm = ({ userInfo, updateUser, deleteUser }) => {
const onUpdate = (e) => {
e.preventDefault()
const form = e.target
const userId = form.username.value
const userPw = form.password.value
const name = form.name.value
const email = form.email.value
console.log(userId, userPw, name, email);
updateUser( {userId, userPw, name, email } )
}
return (
<div className="form">
<h2 className="login-title">UserInfo</h2>
<form className='login-form' onSubmit={ (e) => onUpdate(e) }>
<div>
<label htmlFor="name">username</label>
<input type="text"
id='username'
placeholder='username'
name='username'
autoComplete='username'
required
readOnly
defaultValue={ userInfo?.userId }
/>
</div>
<div>
<label htmlFor="password">password</label>
<input type="password"
id='passowrd'
placeholder='password'
name='password'
autoComplete='password'
required
/>
</div>
<div>
<label htmlFor="name">Name</label>
<input type="text"
id='name'
placeholder='name'
name='name'
autoComplete='name'
required
defaultValue={ userInfo?.name }
/>
</div>
<div>
<label htmlFor="name">Email</label>
<input type="text"
id='email'
placeholder='email'
name='email'
autoComplete='email'
required
defaultValue={ userInfo?.email }
/>
</div>
<button type='submit' className='btn btn--form btn-login'>
정보 수정
</button>
<button type='button' className='btn btn--form btn-login'
onClick={ () => deleteUser(userInfo.userId)} >
회원 탈퇴
</button>
</form>
</div>
)
}
export default UserForm
JavaScript
복사
UserForm.css
CSS
복사
pages
•
Home.jsx
•
Login.jsx
•
Join.jsx
•
User.jsx
•
Admin.jsx
•
About.jsx
Home.jsx
import React from 'react'
import Header from '../components/Header/Header'
import LoginContextConsumer from '../contexts/LoginContextConsumer'
const Home = () => {
return (
<>
<Header />
<div className="container">
<h1>Home</h1>
<hr />
<h2>메인 페이지</h2>
<LoginContextConsumer />
</div>
</>
)
}
export default Home
JavaScript
복사
Login.jsx
import React from 'react'
import Header from '../components/Header/Header'
import LoginForm from '../components/Login/LoginForm'
const Login = () => {
return (
<>
<Header />
<div className="container">
<LoginForm />
</div>
</>
)
}
export default Login
JavaScript
복사
Join.jsx
import React from 'react'
import Header from '../components/Header/Header'
import JoinForm from '../components/Join/JoinForm'
import * as auth from '../apis/auth'
import { useNavigate } from 'react-router-dom'
import * as Swal from '../apis/alert';
const Join = () => {
const navigate = useNavigate()
// 회원가입 요청
const join = async ( form ) => {
console.log(form);
let response
let data
try {
response = await auth.join(form)
} catch (error) {
console.error(`${error}`);
console.error(`회원가입 요청 중 에러가 발생하였습니다.`);
return
}
data = response.data
const status = response.status
console.log(`data : ${data}`);
console.log(`status : ${status}`);
if( status === 200 ) {
console.log(`회원가입 성공!`);
Swal.alert("회원가입 성공", "메인 화면으로 이동합니다.", "success", () => { navigate("/login") })
// alert(`회원가입 성공!`)
// navigate("/login")
}
else {
console.log(`회원가입 실패!`);
// alert(`회원가입에 실패하였습니다.`)
Swal.alert("회원가입 실패", "회원가입에 실패하였습니다.", "error" )
}
}
return (
<>
<Header />
<div className="container">
<JoinForm join={ join } />
</div>
</>
)
}
export default Join
JavaScript
복사
User.jsx
import React, { useContext, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import * as auth from '../apis/auth'
import Header from '../components/Header/Header'
import UserForm from '../components/User/UserForm'
import { LoginContext } from '../contexts/LoginContextProvider'
import * as Swal from '../apis/alert';
const User = () => {
const { isLogin, roles, logout } = useContext(LoginContext)
const [ userInfo, setUserInfo ] = useState()
const navigate = useNavigate()
// 회원 정보 조회 - /user/info
const getUserInfo = async () => {
// 비로그인 또는 USER 권한이 없으면 ➡ 로그인 페이지로 이동
if( !isLogin || !roles.isUser ) {
console.log(`isLogin : ${isLogin}`);
console.log(`roles.isUser : ${roles.isUser}`);
navigate("/login")
return
}
const response = await auth.info()
const data = response.data
console.log(`getUserInfo`);
console.log(data);
setUserInfo(data)
}
// 회원 정보 수정
const updateUser = async ( form ) => {
console.log(form);
let response
let data
try {
response = await auth.update(form)
} catch (error) {
console.error(`${error}`);
console.error(`회원정보 수정 중 에러가 발생하였습니다.`);
return
}
data = response.data
const status = response.status
console.log(`data : ${data}`);
console.log(`status : ${status}`);
if( status === 200 ) {
console.log(`회원정보 수정 성공!`);
// alert(`회원정보 수정 성공!`)
// logout()
Swal.alert("회원수정 성공", "로그아웃 후, 다시 로그인해주세요.", "success", () => { logout(true) })
}
else {
console.log(`회원정보 수정 실패!`);
// alert(`회원정보 수정 실패!`)
Swal.alert("회원수정 실패", "회원수정에 실패하였습니다.", "error" )
}
}
// 회원 탈퇴
const deleteUser = async (userId) => {
console.log(userId);
let response
let data
try {
response = await auth.remove(userId)
} catch (error) {
console.error(`${error}`);
console.error(`회원삭제 중 에러가 발생하였습니다.`);
return
}
data = response.data
const status = response.status
console.log(`data : ${data}`);
console.log(`status : ${status}`);
if( status === 200 ) {
console.log(`회원삭제 성공!`);
// alert(`회원삭제 성공!`)
// logout()
Swal.alert("회원탈퇴 성공", "그동안 감사했습니다:)", "success", () => { logout(true) })
}
else {
console.log(`회원삭제 실패!`);
// alert(`회원삭제 실패!`)
Swal.alert("회원탈퇴 실패", "회원탈퇴에 실패하였습니다.", "error" )
}
}
useEffect( () => {
if( !isLogin ) {
return
}
getUserInfo()
}, [isLogin])
return (
<>
<Header />
<div className="container">
<UserForm userInfo={userInfo} updateUser={updateUser} deleteUser={deleteUser} />
</div>
</>
)
}
export default User
JavaScript
복사
Admin.jsx
import React, { useContext, useEffect } from 'react'
import Header from '../components/Header/Header'
import { LoginContext } from '../contexts/LoginContextProvider'
import { useNavigate } from 'react-router-dom';
import * as Swal from '../apis/alert';
const Admin = () => {
const { isLogin, userInfo, roles } = useContext(LoginContext);
const navigate = useNavigate()
useEffect( () => {
if( !isLogin || !userInfo ) {
// alert(`로그인이 필요합니다.`)
// navigate("/login")
Swal.alert("로그인이 필요합니다.", "로그인 화면으로 이동합니다.", "warning", () => { navigate("/login") })
return
}
if( !roles.isAdmin ) {
// alert(`권한이 없습니다`)
// navigate(-1)
Swal.alert("권한이 없습니다.", "이전 화면으로 이동합니다.", "warning", () => { navigate(-1) })
return
}
}, [userInfo])
return (
<>
{
isLogin && roles.isAdmin &&
<>
<Header />
<div className="container">
<h1>Admin</h1>
<hr />
<h2>관리자 페이지</h2>
<center>
<img src="/img/loading.webp" alt="loading" />
</center>
</div>
</>
}
</>
)
}
export default Admin
JavaScript
복사
About.jsx
import React from 'react'
import Header from '../components/Header/Header'
import LoginContextConsumer from '../contexts/LoginContextConsumer'
const About = () => {
return (
<>
<Header />
<div className="container">
<h1>About</h1>
<hr />
<h2>소개 페이지</h2>
<LoginContextConsumer />
</div>
</>
)
}
export default About
JavaScript
복사
Root
•
App.css
•
App.js
App.css
.container {
width: 960px;
margin: 50px auto;
}
hr { margin-bottom: 50px; }
CSS
복사
App.js
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Home from './pages/Home';
import Login from './pages/Login';
import Join from './pages/Join';
import User from './pages/User';
import About from './pages/About';
import LoginContextProvider from './contexts/LoginContextProvider';
import Admin from './pages/Admin';
function App() {
return (
<BrowserRouter>
<LoginContextProvider>
<Routes>
<Route path="/" element={ <Home /> }></Route>
<Route path="/login" element={ <Login /> }></Route>
<Route path="/join" element={ <Join /> }></Route>
<Route path="/user" element={ <User /> }></Route>
<Route path="/about" element={ <About /> }></Route>
<Route path="/admin" element={ <Admin /> }></Route>
</Routes>
</LoginContextProvider>
</BrowserRouter>
);
}
export default App;
JavaScript
복사