JiSoo's Devlog

[Udemy React 완벽 가이드] 리액트앱 인증 추가하기 본문

Frontend/React

[Udemy React 완벽 가이드] 리액트앱 인증 추가하기

지숭숭숭 2024. 6. 18. 12:55

프론트엔드 앱이 특정 백엔드 리소스에 접근하려 할 때 접근 권한이 주어지기 전에 반드시 인증을 받아야 한다

어떻게 클라이언트 쪽 리액트 앱이 서버에서 돌아가는 백엔드 앱에서 허가를 받을까?

사용자 자격 증명을 가지고 요청을 보내는 것에서 시작!

자격 증명이 유효한 것으로 검증되면 서버는 우리에게 보호된 리소스에 접근을 허가한다는 응답을 보내준다

서버에서는 단순한 가부가 아니라 검증한 증거를 보내줘야 한다

↓ ↓ ↓

서버 측 세션 혹은 인증 토큰 사용하기

 

 

서버 측 세션

서버 측 세션은 대중적인 솔루션으로 특히 프론트와 백엔드가 분리되지 않은 풀스택 앱에서 자주 사용된다

리액트는 분리되어 있기 때문에 이상적이지 않다

서버측 세션의 원리는 사용자가 로그인하고 인증된 다음 서버에 고유 식별자를 저장하는 것

기본적으로 'yes'를 서버에 저장하고 ID를 이용해 그 대답을 특정 클라이언트와 연결한 다음 그 ID를 클라이언트에게 다시 전송하고 클라이언트는 이후 요청에서 해당 ID를 전송하며 보호된 리소스에 접근하려 한다

ID는 'yes'라는 대답과 연동되어 서버에 저장돼 있으므로 서버는 이 클라이언트가 보호된 리소스에 접근할 권한이 있는지 확인 가능

서버 측 세션은 인증을 해결하거나 인증을 구현하는 훌륭한 방식이지만 프론트와 백 사이에 긴밀한 결합이 필요하다

백엔드가 클라이언트 관련 정보를 반드시 저장해야 한다

 

인증 토큰

사용자가 인증받은 다음 서버에는 사용자가 유효한 자격 증명을 전송한 뒤 허가 토큰을 생성하고 저장하지는 않는다

토큰은 기본적으로 알고리즘에 따라 생성된 스트링으로 몇 가지 정보를 포함한다

백엔드에서 토큰을 생성하고 그걸 다시 클라이언트에게 전송

토큰이 특별한 점은 토큰을 생성한 백엔드만이 해당 토큰의 유효성을 확인하고 검증할 수 있다는 것!

백엔드만 알 수 있는 개인키로 토큰을 생성했기 때문에 이후 다시 클라이언트가 백엔드에 요청을 보낼 때 해당 토큰을 요청에 첨부하면 백엔드는 토큰을 살펴보고 검증하고 그 토큰이 백엔드에서 만들어진 건지 확인한다

유효한 토큰이라면 보호된 리소스에 대한 접근이 승인된다

백엔드 API에서 사용자가 가입하거나 로그인을 하면 JSON 토큰을 만들게 되고 이걸 JSON 웹 토큰이라고 한다

const token = createJSONToken(email);

 

백엔드에서 토큰을 생성하고 유효성 검증을 하고 일부 라우트는 추가 미들웨어에 의해 보호된다

일부 추가 미들웨어는 수신하는 요청에 유효한 토큰이 첨부되어 있는지 확인한다

모든 백엔드 라우트가 보호되는 건 아니다

 

결국 서버는 우리가 로그인을 하면 네 혹은 아니오 응답을 되돌려주는 게 아니라

토큰을 포함한 응답을 회신한다

클라이언트 측 리액트 앱에 토큰을 저장하고 이후에 보낼 요청에 첨부해 해당 토큰을 사용자의 로그인 여부를 판단하는 수단으로 사용해야 한다

토큰의 존재를 활용해 사용자가 현재 로그인 상태인지 확인도 가능하다

 

 

쿼리 매개변수

쿼리 매개변수는 URL에서 물음표 뒤에 붙는 매개변수

같은 페이지에 다른 UI가 렌더링되어도 사용자를 그 페이지로 직접 연결할 수 있다

useSearchParams는 쿼리 매개변수에 쉽게 접근할 수 있는 훅으로 호출 시 배열을 리턴해주고 배열 구조 분해 할당을 활용해 해당 배열의 요소에 접근할 수 있다

첫 번째 요소는 현재 설정된 쿼리 매개변수에 접근권을 주는 객체, 두번째는 현재 설정된 쿼리 매개변수를 업데이트하게 해주는 함수

get 메소드로 쿼리 매개변수를 가져와 로그인 상태인지 확인도 가능

const [searchParams] = useSearchParams()
const isLogin = searchParams.get('mode') ==='login';
<Link to={`?mode=${isLogin ? "signup" : "login"}`}>
  {isLogin ? "Create new user" : "Login"}
</Link>

 

 

인증 작업

function AuthenticationPage() {
  return <AuthForm />;
}

export default AuthenticationPage;

export async function action(){
  
}

이렇게 하면 AuthForm이 전송될 때마다 작업이 트리거 될 것이다

action 함수 안에서 브라우저가 제공하는 내장 URL 생성자 함수는 사용할 수 있다

기본 브라우저 기능으로 searchParams 구하기

const searchParams = new URL(request.url).searchParams;
const mode = searchParams.get("mode") || "login";

 

 

useActionData로 양식이 전송한 작업 함수가 리턴한 데이터를 얻을 수 있다

const data = useActionData();

{data && data.erros && (
    <ul>
      {Object.values(data.errors).map((err) => (
        <li key={err}>{err}</li>
      ))}
    </ul>
)}

 

 

useNavigation 훅을 사용하면 navigation 객체를 주는데 이 객체는 상태 프로퍼티가 있어 현재 전송 상태를 유지하거나 전송하고 있는 상태라는 걸 알려준다

const navigation = useNavigation();

const isSubmitting = navigation.state === "submitting";

.
.
.
<button disabled={isSubmitting}>
  {isSubmitting ? "Submitting..." : "Save"}
</button>

 

 

내보내는 요청에 토큰을 첨부하고 싶다면 가입이나 로그인을 한 뒤 백엔드에서 받은 토큰을 저장해야 한다

토큰은 메모리에 저장할 수도 있고 쿠키에 저장할 수도 있다

가장 단순한 방법은 브라우저 API인 로컬 저장소에 저장하는 것!

const resData = await response.json();
const token = resData.token;

localStorage.setItem("token", token);

이렇게 로컬 저장소에 접근해 새 항목을 설정해 해당 토큰을 브라우저 저장소에 저장한다

 

토큰 추출 함수 ↓

export function getAuthToken() {
  const token = localStorage.getItem("token");
  return token;
}

 

 

토큰 삭제 함수 ↓

export function action() {
  localStorage.removeItem("token");
  return redirect("/");
}

 

 

앱 전반에 걸쳐 토큰을 관리하기 위해 리액트 컨텍스트 활용

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    loader: tokenLoader,
export function tokenLoader() {
  return getAuthToken();
}

loader토큰 상태에 관한 최신 정보를 확보할 수 있다

저렇게 하면 모든 라우트에서 루트 라우트의 로더 데이터를 사용할 수 있다

 

const token = useRouteLoaderData("root");

useRouteLoaderData 훅으로 root 라우트를 타겟팅해 토큰을 가져올 수 있다

토큰이 있으면 로그인한 상태

토큰이 없으면 로그인 안 한 상태

{token && (
        <menu className={classes.actions}>
          <Link to="edit">Edit</Link>
          <button onClick={startDeleteHandler}>Delete</button>
        </menu>
      )}

 

 

라우트 보호

특정 라우트들을 언제나 접근 가능하게 하지 못하도록!

토큰 유무를 확인하는 단순한 loader로 보호할 수 있다

토큰이 없으면 리다이렉션 하도록 해서 로그인 상태가 아닌 경우 접근해서는 안 되는 페이지에 적절한 보호 조치 적용

export function checkAuthLoader() {
  const token = getAuthToken();

  if (!token) {
    return redirect("/auth");
  }

  return null;
}

 

 

자동 로그아웃

useEffect 훅 사용하기

애플리케이션이 시작되고 RootLayout이 렌더링 되면 타이머 설정 가능

useSubmit 훅을 사용하면 submit 함수를 활용해 계획적으로 양식을 전송할 수 있다

const token = useLoaderData();
const submit = useSubmit();
useEffect(() => {
  if (!token) {
    return;
  }
  setTimeout(() => {
    submit(null, { action: "/logout", method: "post" });
  }, 1 * 60 * 60 * 1000);
}, [token, submit]);

토큰이 있는 경우 타이머가 시작되고 한 시간 뒤 토큰 삭제

 

토큰 만료 관리

useEffect(() => {
    if (!token) {
      return;
    }

    if (token === "EXPIRED") {
      submit(null, { action: "/logout", method: "post" });
      return;
    }
    const tokenDuration = getTokenDuration();
    console.log(tokenDuration);
    
    setTimeout(() => {
      submit(null, { action: "/logout", method: "post" });
    }, tokenDuration);
  }, [token, submit]);
export function getAuthToken() {
  const token = localStorage.getItem("token");

  if (!token) {
    return;
  }

  const tokenDuration = getTokenDuration();
  if (tokenDuration < 0) {
    return "EXPIRED";
  }
  return token;
}

 

728x90