2024. 12. 9. 13:22ㆍJavaScript/React
Back-end 설정은 아래 내용을 참조하면 된다.
https://kamsi76.tistory.com/entry/Spring-Security-Back-End-%EC%84%A4%EC%A0%95With-JWT
Frontend는 React + typescript + nextjs 와 axios를 사용하였다.
사전 준비
프로젝트 생성
# nextjs 설치
D:\project\src\main> npm install -g create-next-app
# 버전확인
D:\project\src\main> create-next-app --version
15.1.0
# 프로젝트 생성
D:\project\src\main> npx create-next-app frontend
√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like your code inside a `src/` directory? ... No
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to use Turbopack for `next dev`? ... No
√ Would you like to customize the import alias (`@/*` by default)? ... No
# 생성된 프로젝트로 이동하여 axios 설치
D:\project\src\main> cd frontend
D:\project\src\main> npm install axios
package.json
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.7.8",
"http-proxy-middleware": "^3.0.3",
"jwt-decode": "^4.0.0",
"next": "15.0.3",
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
next.config.ts
서버 Redirect 설정
Backend 경로 설정 처리
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*', // 프론트엔드 요청 경로
destination: 'http://localhost:8080/api/:path*', // 백엔드 실제 경로
},
];
},
};
이 설정에서 처음에는 api를 붙이지 않아서 404 페이지를 올바르게 찾지 못하는 오류가 발생했다.
최초 설정은
source: '/:path*', // 프론트엔드 요청 경로
destination: 'http://localhost:8080/:path*', // 백엔드 실제 경로
위와 같이 api 경로를 제거하고 설정하였는데 문제는 api가 설정하지 경우
존재하지 않는 URL 호출할 경우 frontend에서 404를 캐치하지 못하고 계속 backend로 없는 URL이 전송되는 현상이 발생했다. 때문에 올바른 오류를 확인할 수 없었다.
그래서 forntend에서 무조건 api로 시작하는 호출의 경우에만 적용되도록 api를 추가하였다.
그리고 source에만 api를 추가하고 destination에서 api를 추가 하지 않으면 backend 단에 경로 호출이 api는 제외하고 날아가게 된다.
예를 들면
frontend : /api/v1/auth/signin
backend : /v1/auth/signin
이런식으로 backend에서는 api가 제거 된 상태에서 받게 된다.
AxiosJwtInstance
Axios 인스턴스 생성
모든 Axios는 해당 인스턴스를 타도록 우선 설정하였다.
테스트를 위해 timeout는 설정하지 않았다.
baseURL은 nextjs 설정 파일에서 설정하도록 하였다.
역할 :
요청(request) 시 :
localStorage에 access token과 refresh token 이 존재하면 요청 header에 담아서 전송한다.
응답(response) 시 :
back-end에서 Token 정보를 체크하고 401(access token은 문제가 있으나 refresh token이 살아 있는 경우)의 경우
back-end에서 전달된 access token으로 다시 한번 Token 정보 체크를 하여 로그인이 유지 되도록 한다.
401이 아닌 400(refresh token이 문제가 발생하는 경우 나는 400으로 반환한다.) 오류의 경우 로그인 페이지로 이동 한다.
import axios, { AxiosError } from "axios";
import CommonError from "../error/CommonError";
const AxiosJwtInstance = axios.create({
//baseURL: 'http://localhost:8080',
withCredentials: true, // 쿠키를 포함하거나 인증 정보 필요 시
// timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
AxiosJwtInstance.interceptors.request.use(
(config) => {
if (typeof window !== 'undefined') {
/**
* access token과 refresh token이 있는 경우
* 정보를 header에 담아서 back-end로 전송한다.
*/
const accessToken = localStorage.getItem("accessToken") || '';
const refreshToken = localStorage.getItem('refreshToken') || '';
if( accessToken && refreshToken ) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
config.headers['x-refresh-token'] = `Bearer ${refreshToken}`;
}
}
return config;
},
(error) => {
console.error('[+] 요청중 오류가 발생하였습니다.', error);
return Promise.reject(error);
}
);
AxiosJwtInstance.interceptors.response.use(
(response) => {
console.log( response );
const status = response.status;
const responseData = response?.data;
const responseStatus = responseData?.status;
if( status === 200 ) {
/**
* 정상적인 데이터가 아닌 경우 모두 response.data에 status 정보를 담고 있다.
* (이부분은 조금 더 고민이 필요한 부분)
* status 가 200이 아니면 모두 문제가 있는 오류로 판단한다.
* 401 경우
* access token에 문제가 있는 경우에 발생
* back-end에서 refresh token을 검증하고 access token을 반환한다.
* 반환된 access token을 가지고 다시한번 서버에 로그인 여부를 호출한다.
* 반환된 access token이 없으면 로그인 페이지로 이동한다.
* 403 경우
* 서버 접근 권한이 없는 오류로 경고창을 띄우고 history back 처리 한다.
* 400 경우
* refresh token에 문제가 발생한 것으로 무조건 로그인 화면으로 이동한다.
* 나머지는 모두 오류 처리 한다.
*/
if( responseStatus && responseStatus !== 200 ) {
/**
* 401이면서 access token 있으면 access token 재생성한 데이터가 넘어왔기 때문에 다시 서버에 전송 처리
* access token이 없으면 로그인 실패로 처리 해서 다시 로그인 처리 화면으로 이동
*/
if( responseStatus === 401 ) {
if( responseData?.data?.accessToken ) {
const responseConfig = response.config;
localStorage.setItem('accessToken', response.data.data.accessToken);
responseConfig.headers['Authorization'] = response.data.data.accessToken;
return AxiosJwtInstance(responseConfig);
} else {
window.location.href = '/auth/signin';
}
} else if( responseStatus === 403 ) {
// 권한이 없음 메시지 발생시키고 뒤로 이동한다.
// 권한이 없을 때 뒤로 이동하는 방법 적용 필요....
window.location.href = '/error?code=403'
} else {
throw new CommonError(responseData);
}
}
return response;
}
throw new CommonError(responseData);
},
(error) => {
console.log('[-] 응답이 실패한 경우 수행이 됩니다. ', error);
window.location.href = '/error?code=' + error.status;
//return Promise.reject(error);
}
)
export default AxiosJwtInstance;
기본 설정은 끝났고 디자인 없이 테스트를 진행하였다.
프로젝트 실행
# 실행
D:\project\src\main\frontend> npm run dev
> frontend@0.1.0 dev
> next dev
▲ Next.js 15.1.0
- Local: http://localhost:3000
- Network: http://xxx.xxx.xxx.xxx:3000
✓ Starting...
✓ Ready in 1687ms
브라우저에서 확인
http://localhost:3000
완료!!!