
이번 섹션에서는 “인증”에 관해 알아본다.
1. 리액트 앱에서 어떻게 인증을 어떻게 추가하고 구동이 되는것인지 알아본다.
2. “인증 지속성”에 대해서 알아본다. “인증 지속성”이란 사용자가 로그인되어 있는지
안되어있는지를 계속해서 추적한다는 의미.
3. 더하여 일정 시간 지난다면 자동 로그아웃 시키는 방법을 알아본다.
4. 이 과정 속에서 새로운 라우팅 개념을 학습한다.
인증이라는것은 결국 리소스의 보호를 의미하기도 한다. 특정한 백엔드 라우트들은 보호가 필요하며, 모든 사람이 접근할 수 있으면 안되기 때문이다.
그래서 프론트엔드(리액트같은)앱이 특정 백엔드 리소스에 접근하려 할때 접근권한을 위한 인증이 필수적이다. 즉 허가를 받아야한다.
이 과정은 어떻게 이루어지는것일까? 프론트에서는 요청을하고 백엔드단에서는 검증을 통하여 응답을한다는것이 기본 골자다. 그렇다면 아이디와 비밀번호를 백엔드에 요청하면 백엔드에서는 “YES"만 하고 응답을 해주면 되는걸까? 당연하지만 틀리다. 단지 그냥 "YES"라고 하고 정보를 보여준다면 반쪽짜리 일것이다.
이를 보완하기위해서 사용하는것이
1. 서버측 세션
2. 인증 토큰
두가지중 하나로 사용된다.
서버측 세션은 아주 대중적인 솔루션으로 프론트엔드, 백엔드가 분리되지않은 풀스택 앱에서 사용된다. 즉 리액트는 분리되어 있으므로 해당 개념은 리액트에 적절치 않다.
일단 기본 개념 자체로는 사용자가 로그인하고 인증된 다음 서버에 고유 식별자를 저장한다. 해당 고유 식별자는 해당 고유 식별자에 맞는 정보의 접근이 가능토록 한다.
서버는 해당 "고유 식별자“가 “YES"와 연동되어 있으므로 보호된 리소스에 접근할 권한이 있는지 확인이 가능한것이다.
인증 토큰은 사용자가 인증받은 다음 서버에 허가 토큰을 생성하거나 저장하지는 않는다.
토큰은 기본적으로 알고리즘에 따라 생성된 스트링으로 몇 가지 정보를 포함한다.
이렇게 백엔드에서 이 토큰을 생성하고, 그것을 다시 클라이언트에게 전송한다.
특별한점으로 이 토큰을 생성한 백엔드만이 해당 토큰의 유효성을 확인하고 검증이 가능하다.
백엔드만이 알 수 있는 개인키를 활용해 토큰을 생성했기 때문이다.
이후 클라이언트가 백엔드에 요청을 보낼때 해당 토큰을 요청에 첨부하면 백엔드는 토큰을 살펴보고 검증하며 또한 그 토큰이 백엔드에서 만들어진것인지 확인한다. 유효하다면 보호된 리소스에 접근 승인이 허락된다.
이렇게 서버측 세션을 사용하던가 인증 토큰을 사용하여 얻을수 있는 이점과 작동 방식은 각기 다르긴하지만 하고자하는 역할과 목적은 동일하다.
“로그인 창”과 “회원 가입”창을 변환할때 해당 사진처럼 변환을 할것이다.

이와 같은 방식으로 변환할때 기존의 사용했던 방식은 “useState"를 활용한 상태관리로 토글키를 활용하여 다른 상태를 보여준다.
AuthForm.js (일부코드)
const [isLogin, setIsLogin] = useState(true);
<h1>{isLogin ? 'Log in' : 'Create a new user'}</h1>
useState를 사용하여 상태관리로 사용하는 방법도 있지만 쿼리 매개변수를 사용해볼 수 있다.
쿼리 매개변수 : URL에서 물음표 뒤에 붙는 매개변수
해당 쿼리 매개변수를 사용해서 얻는 이점은 설정하는 용도에 따라 “다른 양식 로딩”의 이점이 있다.
라우트의 경로도 같다 경로는 계속 “/auth“ 이지만 여기에서 매개변수를 추가하여 정확히 어떤 컴포넌트가 주어지는것인지 정의하는것이다. 페이지를 가입 혹은 로그인 모드에 직접 연결할 수 있는것이다.
import { Form,Link, useSearchParams } from 'react-router-dom';
import classes from './AuthForm.module.css';
function AuthForm() {
// 현재 설정된 쿼리 매개변수에 쉽게 접근할 수 있는 훅 "useSearchParams"
// 해당 훅을 사용하여 첫번째에 배열에 설정된 객체를 통해 접근이 가능하다.
// 두번째 배열은 쿼리 매개변수를 업데이트 하는 함수인데 여기서는 함수가 필요없다.
const [searchParams] = useSearchParams();
// 쿼리 매개변수인 mode의 값이 login이라면 로그인폼
const isLogin = searchParams.get('mode') === 'login';
return (
<>
<Form method="post" className={classes.form}>
<h1>{isLogin ? 'Log in' : 'Create a new user'}</h1>
<p>
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" required />
</p>
<p>
<label htmlFor="image">Password</label>
<input id="password" type="password" name="password" required />
</p>
<div className={classes.actions}>
{/* 쿼리 매개변수(mode)를 설정한다. */}
{/* isLogin이 참이라면 즉 로그인상태면 새 모드는 가입으로
isLogin이 거짓이라면 새 모드는 로그인으로 */}
<Link to={`?mode=${isLogin ? 'signup' :'login'}`}>
{isLogin ? 'Create new user' : 'Login'}
</Link>
<button>Save</button>
</div>
</Form>
</>
);
}
export default AuthForm;

"auth"의 같은 라우터 내에서 쿼리 매개변수인 "mode"를 통하여 다른 양식을 보여주는 로그인 폼과 가입 폼을 나타낸다.
Authentication.js
import { json, redirect } from 'react-router-dom';
import AuthForm from '../components/AuthForm';
function AuthenticationPage() {
return <AuthForm />;
}
export default AuthenticationPage;
// 해당 action 함수는 AuthForm이 전송될때마다 트리거될것이다.
// 이 AuthForm이 있는 라우트와 동일하기 때문이다.
export async function action({request}) {
// 쿼리 매개변수를 통해 가입 요청을 보내야하는지,
// 로그인 요청을 보내야 하는지 알아야 하는데 "useSearchParams"를 사용했었다.
// 하지만 여기에서는 사용할 수 없는데,
// 브라우저가 제공하는 내장 URL 생성자 함수는 사용할 수 있다.
const searchParams = new URL(request.url).searchParams;
// searchParagms에서 get을 호출해 mode를 얻는다. 뒤의 login은 기본값
// login이든 singup이든 설정한 값에 맞는 모드로 설정된다.
const mode = searchParams.get('mode') || 'login';
if (mode !== 'login' &&mode !== 'signup') {
throw json({message: '지원하지 않는 모드 입니다.'}, {status : 422});
}
// 리액트 라우터에의해 실행되는 요청 객체를 활용하여 데이터를 가져온다.
const data = await request.formData();
const authData = {
email : data.get('email'),
password : data.get('password'),
};
const response = await fetch('http://localhost:8080/'+ mode, {
method : 'POST',
headers : {
'Content-Type' : 'application/json'
},
body : JSON.stringify(authData)
});
if(response.status === 422 || response.status === 401) {
return response;
}
if (!response.ok) {
throw json({ message : "인증된 유저가 아닙니다."}, {status :500});
}
// 시작페이지로 리디렉션한다.
// 즉 로그인성공시 시작 페이지로 리디렉션되는것.
return redirect('/');
}
"Suwon1004@naver.com"로 인증을 추가했다.

"가입되어 있지 않은 Seoul1004@naver.com"로 로그인 하고자하면 401 오류메시지가 나온다.
가입되어있는 Suwon1004@naver.com로 로그인하면 문제없이 로그인이된다.

현재 가입되어 있지 않은 이메일로 로그인하려고 하면 콘솔창에 오류메시지가 나오는것 이외에 사용자가 명시적으로 받을수 있는 오류메시지가 존재하지 않는다.

로그인시 어떠한 문제가 발생했음을 사용자에게 피드백을 줄 수 있도록 코드를 추가해보자.
import {
Form,
Link,
useSearchParams,
useActionData,
useNavigation,
} from "react-router-dom";
import classes from "./AuthForm.module.css";
function AuthForm() {
// 양식이 전송한 함수를 받아오는 useActionData를 사용하여 데이터를 받자
const data = useActionData();
const navigation = useNavigation();
const [searchParams] = useSearchParams();
const isLogin = searchParams.get("mode") === "login";
const isSubmitting = navigation.state === "submitting";
return (
<>
<Form method="post" className={classes.form}>
<h1>{isLogin ? "Log in" : "Create a new user"}</h1>
{/* 양식을 전송할때, 즉 데이터가 있을때 출력하고자함 */}
{data &&data.erros &&(
<ul>
{Object.values(data.errors).map((err) => (
<li key={err}>{err}</li>
))}
</ul>
)}
{data &&data.message &&<p>{data.message}</p>}
<p>
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" required />
</p>
<p>
<label htmlFor="image">Password</label>
<input id="password" type="password" name="password" required />
</p>
<div className={classes.actions}>
<Link to={`?mode=${isLogin ? "signup" : "login"}`}>
{isLogin ? "Create new user" : "Login"}
</Link>
// 로그인시 전송하고있음을 알려주는 추가된 코드
// Save 버튼 클릭시 피드백을 사용자에게 알려준다.
<button disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Save"}
</button>
</div>
</Form>
</>
);
}
가입되어있지 않은 이메일로 로그인시 적절한 오류메시지를 출력한다.

추가로 "Save"버튼을 잘 보자. 양식 전송을 하고있다는것을 사용자에게 피드백을 주기위해서 전송중에 "Submitting"버튼으로 바뀌고있는것을 볼 수 있다.

비단 로그인 뿐만아니라 해당 “토큰”을 가지고 글 편집이라던지, 글 삭제라던지 로그인시에 얻을수 있는 권한이 주어진다고 보면 된다.
// 토큰이 포함되어있는 응답 데이터를 받기
const resData = await response.json();
const token = resData.token;
// 브라우저 API인 로컬 저장소에 토큰 저장하기
localStorage.setItem('token',token);
자세한 뒤의 해당 내용은 정리하기 조금 힘들어서 일단은 생략