본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.
목차 🌳
1️⃣ 리액트 앱에서 유저의 권한을 부여하는 방법
2️⃣ 로그인 후, 백엔드로부터 받은 res에서 토큰 찾아 유저의 권한저장 및 유지 🔍
3️⃣ 유저의 권한 부여 이후, 백엔드에 req 보내기 📤
4️⃣ 유저의 권한 회수 및 로그아웃 방법 📭
인증(권한 부여)를 통해,필요한 데이터를 보호하기 위해 사용
(내가 올린 글만을 수정하고 삭제, .. )
대개, 보호된 리소스에 접근하려는 미래의 요청에 서버 측의 세션이나, 인증 토큰을 사용
1️⃣ server-side session
서버에서 뿌려준 식별자를 저장해, 이를 클라이언트로 보내주는 방법으로, 세션을 이용하기 위해서는 프론트와 백의 긴밀한 연결이 선행되어야 한다, (그치만 react 는 분리된 백엔드 api를 주로 사용하기 때문에 토큰 사용이 권장됨)
2️⃣ Authentiation token
토큰을 이용할 경우, 서버로 부터 생성된 인증 토큰을 (저장하지 않고) 클라이언트로 전송한다
이후, 클라이언트에서 토큰을 req에 첨부해서 (header
에 포함) 보내면 서버가 토큰의 유효성을 검사한다.
이전에 사용했던 이벤트 등록 사이트에 로그인과 회원가입 기능, 유저의 권한에 따른 삭제와 수정, 로그아웃을 구현해보며 권한에 따른 UI 변경과 http 요청을 알아보자!
로그인에서 post
로 req
를 보낸 이후, 받은 res
에서 token
을 추출해 이를 브라우저단에 저장한다! 보통 locastorage storage
나 session storage
등등을 사용하는데 취향에 따라 골라잡으면 될 듯하다. (물론 스토리지에 대한 선택은 브라우저를 뭘 쓰는냐에 따라 특정 웹 스토리지를 지원하지 않을 수도 있고, 데이터 지속성에도 차이가 있어 상황에 따라서도 더 생각해보자!)
영상에서는 localstorage
를 사용했고 토큰을 웹 단에서 저장하는 방법은 다음과 같았다.
Authentication.js
// res 에 있는 토큰을 추출하여 사용 -> 보통 localstorage에 저장
const resData = await res.json();
const token = resData.token;
localStorage.setItem('token', token);
또한, JWT
를 통한 토큰 생성에 있어서, 토큰에는 만료시간이 존재하는데 이에 따라 클라이언트쪽에서도 localstorage
에 저장한 토큰을 제거해줘야 한다! 그래서 생성된 토큰에 대한 만료시간을 같이 저장하면서 권한을 업데이트 해준다.
Authentication.js
// 현재 시간을 추출한다.
const expiration = new Date();
// 현재 시간 + 1시간을 더한 값을 계산한다.
expiration.setHours(expiration.getHours() + 1);
// 날짜 객체를 ISO 8601 형식의 문자열로 변환 -> localstorage 에 저장하기
localStorage.setItem('expiration', expiration.toISOString());
이렇게 클라이언트 측에서 보관한 정보들은 util
폴더안에서 그 값들을 꺼내보며 토큰이 존재하는지, 토큰이 만료되지는 않았는지를 판별한다.
utils -> auth.js
export function getAuthToken() {
const token = localStorage.getItem('token');
//토큰이 없는 경우,
if (!token) {
return;
}
const tokenDuration = getTokenDuration();
// 토큰이 만료된 경우
if (tokenDuration < 0) {
return 'EXPIRED';
}
// 토큰이 존재하는 경우
return token;
}
export function getTokenDuration() {
// localstorage에 저장된 만료 시간을 꺼내어, 만료시간 - 현재시간을 뺀 값을 리턴
const storedExpirationDate = localStorage.getItem('expiration');
const expiration = new Date(storedExpirationDate);
const now = new Date();
const duration = expiration.getTime() - now.getTime();
return duration;
}
또한 여기에loader
로 걸어줄 함수들도 선언해준다!
// loader로 걸어줄 토큰 추출 함수
export function tokenLoader() {
return getAuthToken();
}
// token 이 있어야만 특정 페이지에 도달할 수 있도록 판별
export function checkAuthLoader() {
const token = getAuthToken();
if (!token) {
return redirect('/auth');
}
return null;
}
router
에 권한이 필요한 곳에 checkAuthLoader
을 걸어주어, URL
을 통한 유입을 막고,
getAuthToken
을 통해 token
값의 유무에 따라 Nav
에서 노출되어야 할 정보를 조절하면 된다
router
에 loader
어떻게 걸어주는지 잘보고 익히기 👀
app.js
// 라우트 보호
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
id: 'root',
loader: tokenLoader, ⭐️
children: [
{ index: true, element: <HomePage /> },
{
path: 'events',
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{
path: ':eventId',
id: 'event-detail',
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
action: deleteEventAction,
},
{
path: 'edit',
element: <EditEventPage />,
action: manipulateEventAction,
loader: checkAuthLoader, ⭐️
},
],
},
{
path: 'new',
element: <NewEventPage />,
action: manipulateEventAction,
loader: checkAuthLoader, ⭐️
},
],
},
// ... 생략
],
},
]);
MainNav.js
import { Form, NavLink, useRouteLoaderData } from 'react-router-dom';
// 생략 ...
function MainNavigation() {
// root 에 걸어둔 loader 함수의 반환값 가져오기
const token = useRouteLoaderData('root');
return (
<header className={classes.header}>
<nav>
<ul className={classes.list}>
// 생략 ...
{!token && (
<li>
<NavLink to="/auth?mode=login" className={({ isActive }) => (isActive ? classes.active : undefined)}>
auth
</NavLink>
</li>
)}
{token && (
<li>
<Form action="/logout" method="post">
<button>Logout</button>
</Form>
</li>
)}
</ul>
</nav>
</header>
);
}
export default MainNavigation;
백엔드에서 토큰의 유효성을 검사하고 특정 api를 실행할 수 있도록 설계했다면, 클라이언트쪽에서는
req를 보낼때, header
에 Authentication 값으로 Bearer + token
값을 함께 넣어주어야 한다! (localstorage 에 저장한 값을 빼서, 같이 req로 날려버려 ~)
req 보내는 형식 예시
// 📁 utils -> auth.js 에서 만들어놓은 함수에서 token 값 반환 받기
const token = getAuthToken();
// 생략...
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token, // ⭐️
},
body: JSON.stringify(eventData),
});
유저는 2가지 경우에 권한을 잃게 된다.
1️⃣ 로그아웃 버튼 누르기
2️⃣ 시간 만료로 인한, 토큰 만료
하나씩 처리 방법을 알아보자
1️⃣ 로그아웃 버튼 누르기
1) action
에서 localstorage
에 저장한 토큰정보와 만료시간을 제거하고, root 페이지로 리다이렉트를 건다
//components -> Logout.js
import { redirect } from 'react-router-dom';
export function action() {
localStorage.removeItem('token');
localStorage.removeItem('expiration');
return redirect('/');
}
2) router
에 위에서 설정한 action
걸어두기
// app.js
{
path: '/logout',
action: logoutAction,
},
3) 클릭하면 걸어둔 action 함수가 호출되어 로그아웃이 이루어지도록 설정
// MainNav.jsx
{token && (
<li>
<Form action="/logout" method="post">
<button>Logout</button>
</Form>
</li>
)}
2️⃣ 시간 만료로 인한, 토큰 만료
root page
에서 token
값을 통해, 자동 로그아웃 처리
function RootLayout() {
const token = useLoaderData();
const submit = useSubmit();
useEffect(() => {
if (!token) {
return;
}
// 토큰이 만료된 경우, 로그아웃 처리
if (token === 'EXPIRED') {
submit(null, { action: '/logout', method: 'post' });
}
// 토큰 만료시간에 따라, 타이머 on 설정 후 시간 도달시 자동 로그아웃
const tokenDuration = getTokenDuration();
setTimeout(() => {
submit(null, { action: '/logout', method: 'post' });
}, tokenDuration);
}, [token, submit]);
return (
<>
// ... 생략
</>
);
}