본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.
또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.
SpringBoot
에서 구현한JWT
인증시스템을,React
에서 사용하는 방법입니다.
회원가입 및 로그인 처리를 위해React Custom API
를 구현하고 사용하는 방법입니다.
AuthAPI.js
import axios from "axios";
const TOKEN_TYPE = localStorage.getItem("tokenType");
let ACCESS_TOKEN = localStorage.getItem("accessToken");
/** CREATE CUSTOM AXIOS INSTANCE */
export const AuthApi = axios.create({
baseURL: 'http://localhost:8080',
headers: {
'Content-Type': 'application/json',
'Authorization': `${TOKEN_TYPE} ${ACCESS_TOKEN}`,
},
});
/** LOGIN API */
export const login = async ({ username, password }) => {
const data = { username, password };
const response = await AuthApi.post(`/api/v1/auth/login`, data);
return response.data;
}
/** SIGNUP API */
export const signUp = async ({ username, password }) => {
const data = { username, password };
const response = await AuthApi.post(`/api/v1/auth/signup`, data);
return response.data;
}
Axios
인스턴스header
에Authorization
을 추가하는 것이 관건입니다.
일반적으로'Authorization': 'Bearer {token}'
의 형태로 포함시킵니다.
TOKEN_TYPE
,ACCESS_TOKEN
은 로그인 이후 값이 할당되어 사용할 수 있습니다.
사실, 회원가입 및 로그인 단계에서Authorization
,AccessToken
은 필요없습니다.
로그인 이후header
추가에 거부감을 줄이고 익숙해지고자 편의상 사용하였습니다.
간단한 예시를 위해
username
,password
두 개의 파라미터만 사용하였습니다.
요구사항에 따라contact
,address
등 필요한 파라미터를 추가하기 바랍니다.
간단하게
username
,password
를 입력으로 받는form
을 작성하였습니다.
handleChange
,handleSubmit
이 주요 비동기 함수로 사용되고,
AuthAPI
의login
비동기 함수를 가져와 로그인을 수행합니다.
SignInPage.js
import { useState } from "react";
import { login } from "../api/AuthAPI";
export default function SignInPage() {
const [values, setValues] = useState({
username: "",
password: "",
});
const handleChange = async (e) => {
setValues({...values,
[e.target.id]: e.target.value,
});
}
const handleSubmit = async (e) => {
login(values)
.then((response) => {
localStorage.clear();
localStorage.setItem('tokenType', response.tokenType);
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('refreshToken', response.refreshToken);
window.location.href = `/home`;
}).catch((error) => {
console.log(error);
});
}
return (
<div className="d-flex justify-content-center" style={{ minHeight: "100vh" }}>
<div className="align-self-center">
<form onSubmit={handleSubmit}>
<div className="form-group" style={{ minWidth: "25vw" }}>
<label htmlFor="username">아이디</label>
<input type="text" className="form-control" id="username" onChange={handleChange} value={values.username} />
</div>
<div className="form-group" style={{ minWidth: "25vw" }}>
<label htmlFor="password">비밀번호</label>
<input type="password" className="form-control" id="password" onChange={handleChange} value={values.password} />
</div>
<div className="form-group" style={{ minWidth: "25vw" }}>
<button type="submit" style={{ width: "100%"}}>로그인</button>
</div>
</form>
</div>
</div>
);
}
tokenType
,accessToken
,refreshToken
을 응답으로 돌려받습니다.
localStorage
에 각각key-value
의 형태로 저장합니다.
저장한 이후에는/home
화면으로 이동합니다.
간단하게
username
,password
를 입력으로 받는form
을 작성하였습니다.
handleChange
,handleSubmit
이 주요 비동기 함수로 사용되고,
AuthAPI
의signUp
비동기 함수를 가져와 회원가입을 수행합니다.
SignUpPage.js
import { useState } from "react";
import { signUp } from "../api/AuthAPI";
export default function SignUpPage() {
const [values, setValues] = useState({
username: "",
password: "",
});
const handleChange = async (e) => {
setValues({...values,
[e.target.id]: e.target.value,
});
}
const handleSubmit = async (e) => {
signUp(values)
.then((response) => {
window.location.href = `/login`;
}).catch((error) => {
console.log(error);
});
}
return (
<div className="d-flex justify-content-center" style={{ minHeight: "100vh" }}>
<div className="align-self-center">
<form onSubmit={handleSubmit}>
<div className="form-group" style={{ minWidth: "25vw" }}>
<label htmlFor="username">아이디</label>
<input type="text" className="form-control" id="username" onChange={handleChange} value={values.username} />
</div>
<div className="form-group" style={{ minWidth: "25vw" }}>
<label htmlFor="password">비밀번호</label>
<input type="password" className="form-control" id="password" onChange={handleChange} value={values.password} />
</div>
<div className="form-group" style={{ minWidth: "25vw" }}>
<button type="submit" style={{ width: "100%"}}>회원가입</button>
</div>
</form>
</div>
</div>
);
}
회원가입은 응답으로 돌려받는 값이 없습니다.
따라서 정상작동 확인 이후/login
화면으로 이동합니다.
SpringBoot
에서 구현한JWT
인증시스템을,React
에서 사용하는 방법입니다.
유저 정보를 얻기 위해React Custom API
를 구현하고 사용하는 방법입니다.
UserAPI.js
import axios from "axios";
const TOKEN_TYPE = localStorage.getItem("tokenType");
let ACCESS_TOKEN = localStorage.getItem("accessToken");
let REFRESH_TOKEN = localStorage.getItem("refreshToken");
/** CREATE CUSTOM AXIOS INSTANCE */
export const UserApi = axios.create({
baseURL: 'http://localhost:8080',
headers: {
'Content-Type': 'application/json',
'Authorization': `${TOKEN_TYPE} ${ACCESS_TOKEN}`,
'REFRESH_TOKEN': REFRESH_TOKEN,
},
});
// 토큰 갱신
const refreshAccessToken = async () => {
const response = await UserApi.get(`/api/v1/auth/refresh`);
ACCESS_TOKEN = response.data;
localStorage.setItem('accessToken', ACCESS_TOKEN);
UserApi.defaults.headers.common['Authorization'] = `${TOKEN_TYPE} ${ACCESS_TOKEN}`;
}
// 토큰 유효성 검사
UserApi.interceptors.response.use((response) => {
return response;
}, async (error) => {
const originalRequest = error.config;
if (error.response.status === 403 && !originalRequest._retry) {
await refreshAccessToken();
return UserApi(originalRequest);
}
return Promise.reject(error);
});
Response Status
를 통해ACCESS_TOKEN
의 유효성을 검사하고 재발급하는 과정입니다.
Axios Instance
UserApi
요청의 응답을 가로채(intercept
) 에러코드를 확인합니다.
에러코드가403
인 경우REFRESH_TOKEN
을 통해ACCESS_TOKEN
의 재발급을 수행합니다.
401
: 로그인하지 않은 상태
403
: 로그인하였으나AccessToken
이 유효하지 않은 상태
->header
에REFRESH_TOKEN
이 추가되었음에 유의합니다.
->REFRESH_TOKEN
에는TOKEN_TYPE
이 없음에 유의합니다.
/** 회원조회 API */
export const fetchUser = async () => {
const response = await UserApi.get(`/api/v1/user`);
return response.data;
}
/** 회원수정 API */
export const updateUser = async (data) => {
const response = await UserApi.put(`/api/v1/user`, data);
return response.data;
}
/** 회원탈퇴 API */
export const deleteUser = async () => {
await UserApi.delete(`/api/v1/user`);
}
화면 상단의 네비게이션바에 유저정보를 불러와 로그인 상태인지 확인합니다.
로그인을 하지 않은 경우"로그인"
, 로그인한 경우"username님 환영합니다."
라는 메세지가 뜨도록 구현합니다.
Navigation.js
import logo from '../logo.svg';
import { Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
import { useEffect, useState } from 'react';
import { fetchUser } from '../api/UserAPI';
export default function TestPage() {
const [user, setUser] = useState({});
const ACCESS_TOKEN = localStorage.getItem('accessToken');
useEffect(() => {
if (ACCESS_TOKEN) {
fetchUser()
.then((response) => {
setUser(response);
}).catch((error) => {
console.log(error);
});
}
}, [ACCESS_TOKEN]);
const handleLogout = async () => {
localStorage.clear();
}
return (
<Navbar collapseOnSelect expand="lg" bg="dark" variant="dark">
<Container>
<Navbar.Brand href="/home">
<img src={logo} width="40" height="35" alt="" />
Home
</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="me-auto" alt="Nav Empty Space">
</Nav>
<Nav>
<Nav.Link href="/home">Home</Nav.Link>
<NavDropdown title="DropDown1" id="collasible-nav-dropdown">
<NavDropdown.Item href="/dropdown1/menu1">Menu1</NavDropdown.Item>
<NavDropdown.Item href="/dropdown1/menu2">Menu2</NavDropdown.Item>
<NavDropdown.Item href="/dropdown1/menu3">Menu3</NavDropdown.Item>
</NavDropdown>
<NavDropdown title="DropDown2" id="collasible-nav-dropdown">
<NavDropdown.Item href="/dropdown2/menu1">Menu1</NavDropdown.Item>
<NavDropdown.Item href="/dropdown2/menu2">Menu2</NavDropdown.Item>
<NavDropdown.Item href="/dropdown2/menu3">Menu3</NavDropdown.Item>
</NavDropdown>
{ACCESS_TOKEN
?
<NavDropdown title={user.username + "님 환영합니다"} id="collasible-nav-dropdown">
<NavDropdown.Item href="/my-page">MyPage</NavDropdown.Item>
<NavDropdown.Item href="/" onClick={handleLogout}>로그아웃</NavDropdown.Item>
</NavDropdown>
:
<NavDropdown title="Login/SignUp" id="collasible-nav-dropdown">
<NavDropdown.Item href="/login">Login</NavDropdown.Item>
<NavDropdown.Item href="/signup">SignUp</NavDropdown.Item>
</NavDropdown>
}
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
);
}
KEY POINT1
import { useEffect, useState } from 'react';
import { fetchUser } from '../api/UserAPI';
export default function TestPage() {
const [user, setUser] = useState({});
const ACCESS_TOKEN = localStorage.getItem('accessToken');
useEffect(() => {
if (ACCESS_TOKEN) {
fetchUser()
.then((response) => {
setUser(response);
}).catch((error) => {
console.log(error);
});
}
}, [ACCESS_TOKEN]);
const handleLogout = async () => {
localStorage.clear();
}
...
ACCESS_TOKEN
의 여부가 곧 로그인 여부이므로localStorage
에서 읽어옵니다.
존재한다면 로그인상태이므로, 유저정보를GET
요청하여setUser
로 저장합니다.로그아웃의 경우
localStorage
에서ACCESS_TOKEN
을 삭제하는 것으로 처리합니다.
KEY POINT2
import logo from '../logo.svg';
import { Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
export default function TestPage() {
return (
...
{ACCESS_TOKEN
?
<NavDropdown title={user.username + "님 환영합니다"} id="collasible-nav-dropdown">
<NavDropdown.Item href="/my-page">MyPage</NavDropdown.Item>
<NavDropdown.Item href="/" onClick={handleLogout}>로그아웃</NavDropdown.Item>
</NavDropdown>
:
<NavDropdown title="Login/SignUp" id="collasible-nav-dropdown">
<NavDropdown.Item href="/login">Login</NavDropdown.Item>
<NavDropdown.Item href="/signup">SignUp</NavDropdown.Item>
</NavDropdown>
}
...
);
}
Front
화면의 로그인 상태에 따른 표시 여부도ACCESS_TOKEN
을 이용합니다.
삼항연산자를 사용해ACCESS_TOKEN
이 있다면 "환영합니다"를 표시하고,
ACCESS_TOKEN
이 없다면 "Login/SignUp"을 표시합니다.
UserAPI.js
의updateUser()
,deleteUser()
의 경우도
KEY POINT1
과 같이회원정보수정
페이지나,회원탈퇴
버튼 등을 만들어
handleSubmit()
등의 비동기 함수안에 사용하는 방식으로 구현하면 됩니다.
이로서
[React + SpringBoot] JWT 인증 구현
시리즈가 모두 끝났습니다.
본 시리즈는 가능한 필요한 최소한의 라이브러리만 사용하여
필요한 최소한의 기능들을 간결하게 구현하는 것을 목적으로
ChatGPT
를 활용하여 작성되었습니다.
본 시리즈보다 더 다양한 기능을 구현하거나, 여러가지 옵션을 더 추가하거나,
다양한 라이브러리를 사용하여 더 간결하게 구현하는 방법도 있을지 모르나,
본 시리즈는 기본적으로 실행이되는 것을 목표로 본 시리즈를 작성하였습니다.
실무에서 요구하는 좋은 개발자는, 기능을 구현할 줄 아는 개발자입니다.
아무리 코드가 간결하고 다양한 라이브러리를 썼을지언정,
실행이 되지 않는다면 의미가 없습니다.
실행 가능한 코드를 개발하는 개발자.
리팩토링으로 코드를 간결하게 만드는 개발자.
그런 좋은 개발자가 될 수 있도록 발전하기를 기원합니다.