[React] 리엑트 인증 Authenticate 1

SuamKang·2023년 8월 18일
0

React

목록 보기
32/34
post-thumbnail

인증이 정확히 무엇을 의미하는지 짚어보자.


✔️ 인증 원리


어떠한 리소스들은 보호가 필요하며 모든사람이 접근할 수 있게 해선 안된다.

즉, 어떤 프론트 애플리케이션이 특정 백엔드 리소스에 접근하려 할 때, 접근권한이 주어지기전에 반드시 인증을 받아야 한다는 것으로 설명 할 수 있겠다.


❗️ 그렇다면, 질문은 클라이언트가 서버에서 돌아가는 백엔드앱에서 어떻게 허가를 받느냐 일테다!


모든건 사용자 자격증명을 가지고 요청을 보내는 것에서부터 시작한다.(이메일과 비밀번호)
그리고 나서 서버에선 해당 자격증명의 유효성을 검증하거나 필요하다면 새 사용자를 생성해준다.
만약 자격 증명이 유효한것으로 검증되면, 서버는 클라이언트에게 보호된 리소스에 접근을 허가해준다는 응답을 보내준다!

허가 해주는 응답은 그저 yes or no로 돌려주지 않는다.
만약 이렇게 단순하다면, 과거에 접근 승인 받을때 사용한걸 다시 재사용 할 수도 있어 조작의 위험성이 있고 안전하지 못하기 때문이다.

그리하여 서버에서는 그 이상의 무언가 검증해서 허가를 받았다는걸 증명할 어떤 것을 회신해 주는데

바로 세션(Sesstion) 혹은 토큰(Token) 방법을 통해 응답해준다.


✔️ Server-side Sesstions / Authentication Tokens(JWT)


Server-side Sesstions:

  • 보통 프론트앤드와 백엔드가 분리되지 않는 풀스택 애플리케이션에서 자주 사용된다.

  • 사용자의 로그인 정보를 로그인 되면 인증후 서버 데이터베이스에 고유 식별자를 저장한다.

  • 올바른 정보의 식별자임의 인증을 서버에 저장하고 id를 통해 해당 인증을 특정 클라이언트와 연결한 다음 클라이언트에게 보내는 것이다.

  • 클라이언트는 요청에서 해당 id를 전송하며 보호된 리소스에 접근하려 할것이다.

  • 요청온 id는 올바른 정보라는 인증과 연동되어 서버에 저장되어 있으므로 서버는 클라이언트가 보호된 리소스에 접근할 권한이 있는지 확인할 수 있다.

    정리하자면 세션은 인증을 해결하거나 인증을 구현하는 훌륭한 방식이지만 백엔드와 프론트앤드 사이에 긴밀한 결합이 필요하다.(백엔드쪽에서 사용자 정보를 반드시 저장해야해서 서버에 부담이 생김)


Authentication Tokens(JWT):

  • 리액트는 현재 CRA기반으로 인터렉티브한 애플리케이션을 위해 일정부분 서버와 분리되어 있다. (nextJs등 서버리스 프레임워크를 사용하지 않는다는 가정)

  • 리액트 애플리케이션의 경우에 서버는 클라이언트와 긴밀한 결합 혹은 정보도 저장하고 있지 않다. 따라서 인증 토큰이라는 방법이 등장했다.

  • 인증 토큰 메커니즘의 원리는 사용자의 정보가 서버에서 인증(유효한 자격증명이다라는)을 받은 뒤, 저장하는게 아니라 토큰을 생성하게 된다는 것!

  • 토큰은 기본적으로 알고리즘에 따라 생성된 문자열로 몇가지 정보를 담고있다.

  • 이렇게 서버에서 토큰을 생성하면 다시 클라이언트로 보내준다.

  • 토큰의 특별한 점은 해당 토큰의 유효성을 확인하고 검증할 수 있는건 토큰을 생성한 서버만 가능하다는것이다!
    -> 서버만이 알 수 있는 개인키를 활용해서 만들었기 때문

  • 그리고 이후에 다시 클라이언트가 서버에 요청을 보낼 때, 해당 토큰을 요청에 첨부해 보내면 서버는 토큰을 살펴보고 검증한 후 그 토큰이 바로 자신들이 만든 토큰인지 확인할 것이다.

  • 최종적으로 그게 유효하다고 판명되면 보호된 리소스에대한 접근이 승인된다.


이 토큰은 현재 서버에서 JSON으로 생성한다. 생성할 때 서버에서 개인키를 사용해야하는데 이건 백엔드만이 알 수 있는키이다. 이를 통해 프론트앤드에서 서버로 요청을 보내면 추가 미들웨어를 통해 이 요청들을 실행 할 수 있다.




✔️ 인증 작업 실행하기


import { Form, Link, useSearchParams } from "react-router-dom";

import classes from "./AuthForm.module.css";

function AuthForm() {
  // 첫번째 요소: 접근가능한 객체, 두번째요소: 업데이트 함수(생략)
  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}>
          <Link to={`?mode=${isLogin ? "signup" : "login"}`}>
            {isLogin ? "Create new user" : "Login"}
          </Link>
          <button>Save</button>
        </div>
      </Form>
    </>
  );
}

export default AuthForm;

현재 내가 양식을 관리하고 있는 컴포넌트은 AuthForm컴포넌트에선 이미 리엑트 라우터에서 제공하는 Form컴포넌트를 통해 데이터를 전송하게 구성했다.

결론적으로 받은 양식에 대해 트리거 되는 작업을 추가해주어야 한다.


AuthenticationPage.js

import AuthForm from "../components/AuthForm";

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

export default AuthenticationPage;

위 인증 페이지에서 렌더링되고 있는 양식폼에서 받은 데이터로 백엔드로 전송하기전 action함수를 트리거를 시켜주도록 하자.
action함수에서 데이터의 일부인 request객체를 매개변수로 전달해 활용해야한다.

부가설명

파라미터는 리소스의 경로와 관련된 정보를 전달하고, 쿼리는 웹 서버에게 부가적인 데이터나 동작을 지시하는 데 사용된다.
현재 이 앱에서 쿼리는 key로 'mode'가 설정되어있고 value로는 각각 'login'과 'signup'이 주어져있다.

그리고 login이던 signup이던 현재 양식 mode에 따라 다른 요청을 해야하기 때문에 action함수 안에서 쿼리 매개변수를 확인하고 로그인인지 회원가입요청인지 파악해야한다.

// login 혹은 signup 생성 action
export async function action ({request}) {
  // 1 브라우저가 제공하는 내장 함수인 URL생성자 함수를 이용해서 요청 객체의 url을 만들어준다
  // 받아온 url은 요청이 온 그 url이므로 searchParams에 접근 가능하다.
  const searchParams = new URL(request.url).searchParams; // 그럼 ?뒤에 쿼리를 받는다.
  const mode = searchParams.get('mode') || 'login'; // 기본 모드는 login으로

  // 예외처리
  if(mode !== "login" && mode !== "signup"){
    throw json({ message : "지원하지 않는 모드입니다."}, { status: 422 })
  }

  // 2 모드가 결정되면 해당 모드에서 받은 데이터를 최종 백엔드에 전달해야한다
  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})
  }

    // 리디렉션
  if (mode === "login") {
    return redirect("/");
  } else {
    return redirect("/auth?mode=login");

}

아직은 토큰을 관리하는 로직이 없지만,
이렇게 양식으로부터 받은 데이터를 먼저 백엔드에 보내주었다.
그리고 해당 유효성 검증이 정상적으로 통과되면 회원가입이였을땐 로그인으로 가고 로그인이였을때 보냈다면 홈페이지로 가게 만들었다.


✔️ 사용자에게 인풋 오류 확인시키기


현재 상태로 백엔드로 요청을 보내게 되면 같은 메일과 비밀번호 입력시 '422'상태코드 오류를 확인할 수 있다.

이는 사용자 검증 오류라고 피드백을 준건데 이때 사용자가 바로 파악할 수 있게 화면에 표시해 보자.

다시 사용자 양식을 받는 AuthForm컴포넌트로 돌아와서 action함수가 리턴해준 응답객체를 활용해 오류 메세지를 출력해보자.

복습: useActionData훅은 사용하는 양식 컴포넌트에서 보낸 데이터를 받아서 작업을 처리한 action함수가 리턴한 데이터객체(response)를 받을 수 있는 훅이다.


import {
  Form,
  Link,
  useActionData,
  useNavigation,
  useSearchParams,
} from "react-router-dom";

import classes from "./AuthForm.module.css";

function AuthForm() {
  const data = useActionData();
  const navigation = useNavigation();

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

  // 첫번째 요소: 접근가능한 객체, 두번째요소: 업데이트 함수(생략)
  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>
        {data && data.errors && (
          <ul>
            {Object.values(data.errors).map((error) => (
              <li key={error}>{error}</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>
          <button disabled={isSubmitting}>
            {isSubmitting ? "Submitting..." : "Save"}
          </button>
        </div>
      </Form>
    </>
  );
}

export default AuthForm;

따라서 현재는 내가 인증페이지에서 설정한 action에서 리턴하는건 "422"오류나 "401"오류일때 response를 보내도록 설정해 놓았으니 이 경우엔 인증할때 발생한 문제와 관련 정보가 있는 응답객체일것이다.

중요❗️

그리고 사용되는 data에는 action에서 보내준 response객체안의 전체 오류에관한 내용이 담긴 "message"와 검증되지 못한 인풋에대한 오류메세지가 담겨있는"errors"프로퍼티가 있는 구조이다.
좀 복잡하긴해도 이걸 잘 파악할줄 알아야 추출하기 쉬울것같다.



이로써 우선 토큰을 통한 사용자 검증은 진행하지 않은채 회원가입과 로그인의 mode환경에 따라 각각 양식을 보내고, 유효하지 못한 값에 대해서 오류처리를 한 부분만 정리해 보았다.

이제 잘 가입된 정보는 토큰을 전달 받았을테니 그걸가지고 로그인 로직을 수정해보자.


✔️ 로그인 추가하기


로그인은 사실 이미 잘 작동하고 있다.

단지 mode에 따라서 다른 요청을 보내고 있을 뿐이다. 이제 문제는 백엔드에서 주는 토큰에 집중해봐야한다.

왜냐면 클라이언트에서 다시 서버로 요청할때, 보호되고 있는 리소스에 접근하기 위해서 토큰을 반드시 첨부해야하기 때문이다.

현재 그냥 로그인 없이 만약 메인페이지에 추가로 게시글을 보낼때 앱이 충돌하고 만다. -> 그 이유는 아직 해당 글을 생성할 권한이 주어지지 않았기 때문이다.

즉, 생성요청을 할때, 새로운 게시물을 생성 할 권한에 대한 사용자 정보가 담긴 토큰이 없다는 것!


토큰 받아서 저장후 사용하기


회원가입시 서버로부터 전달받은 토큰을 저장해야한다.

서버로 부터 전달 받은 토큰은 josn 안에 그대로 실려 있을것이다. 그럼 프론트쪽에서 토큰을 추출해 저장해야한다 -> http트리거 작업다음 리디렉션 되기 전에 토큰을 추출해본다.

export async function action({ request }) {
  const searchParams = new URL(request.url).searchParams;
  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"),
  };

  // 양식 저장 후 트리거 하는 http
  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 });
  }

  const resData = await response.json();
  console.log(resData); // 모드에 따른 응답 객체 확인

  return redirect("/");
}

현재 서버로직상, json으로 전달 해주는 데이터에는
회원가입시 응답으로는 성공문자(message)와 방금 가입한유저 정보(user), 그리코 토큰(token)이 나오고,

로그인시 응답으로는 해당 계정에 대한 토큰(token)만 준다.


그럼 받아온 토큰을 저장은 어디서 해야할까? 여러 옵션이 있는데 메모리에 저장할 수도 있고 쿠키에 저장할 수도 있지만 가장 단순한건 브라우저가 제공하는 API인 로컬스토리지에 저장하는것이다.
(현재 이 action함수는 브라우저에서 구동되기 때문에 가능하다.)

loclStorage의 키에 이름을 부여하고 토큰을 저장한다.

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

  localStorage.setItem('token',token);

그리고 다음 로그인시 저장한 토큰을 꺼내 쓰기위한 util함수를 따로 생성해서 아웃소싱 해보자.

util/auth.js

export function getAuthToken() {
	return localstorage.getItem("token");
}

이제 토큰이 필요한 곳에서 이 함수를 써주면 된다.

상세페이지 내에서 삭제를 핸들링하는 컴포넌트가 렌더링 되고 있으니 삭제를 보내는요청엔 반드시 해당 권한을 가지고 있는 토큰을 함께 전달해주어야겠다.


http요청 헤더에 특별한 인증헤더를 추가해 주면 되는데
바로 headers에 'Authorization'키에 'Bearer'와 함께 token값을 붙여 보내주어야한다.

📍 Bearer를 사용하는 이유

  1. 보안: Bearer 토큰 방식은 토큰 값을 평문으로 노출시키지 않고, 헤더 내의 값을 이용해 토큰을 보호한다. 만약 토큰 값을 URL 파라미터나 쿠키에 직접 포함시키는 방식을 사용한다면, 브라우저 히스토리나 서버 로그 등에서 토큰 값이 노출될 수 있다.

  2. 표준화: Bearer 토큰은 보안 토큰을 전달하는 표준 방식 중 하나다. 이러한 표준화는 다양한 개발자나 팀 간의 협업을 용이하게 만들어준다.

  3. 서버 인증: 서버 측에서는 'Authorization' 헤더를 통해 클라이언트가 보낸 토큰을 쉽게 식별하고 인증할 수 있다.

  4. 토큰 타입 구분: Bearer 토큰 방식은 다양한 인증 방식을 지원하는 표준 방식 중 하나입니다. 다른 토큰 타입을 구분할 수 있는 메커니즘이다.

  5. 확장성: 향후에 인증 방식이 변경되더라도 클라이언트와 서버 간의 통신 코드를 크게 수정하지 않고도 토큰을 교체하거나 업그레이드할 수 있다.

따라서 'Authorization' 헤더에 'Bearer' 토큰 값을 포함시켜 보내는 것은 토큰을 보안적으로 안전하게 전달하고, 클라이언트와 서버 간의 통신을 표준화하며, 유지보수 가능한 시스템을 구축하기 위한 중요한 단계인 것이다.

EventDetailPage.js(상세페이지)

// 삭제 요청 action
export async function action({ params, request }) {
  const eventId = params.eventId;
  const token = getAuthToken();

  const response = await fetch("http://localhost:8080/events/" + eventId, {
    method: request.method,
    headers: {
      Authorizatioin: `Bearer ${token}`,
    },
  });

  if (!response.ok) {
    throw json(
      { message: "Could not delete event." },
      {
        status: 500,
      }
    );
  }
  return redirect("/events");
}

이렇게 해서 로그인하면 토큰을 저장하게되고 저장한 토큰을 권한이 있어야 가능한 이벤트요청의 헤더에 첨부하여 보내면 된다.

그럼 이제 다른 페이지 혹은 컴포넌트에서 권한이 있어야만(토큰이 있어야만)가능한 기능들에 다 요청헤더에 토큰을 넣어 주자.

-> 생성페이지, 수정페이지인데 이 두페이지에선 공유하는 양식 컴포넌트가 있으며 거기에서 action을 관리하는 중이기에 해당 action에서 수정한다!

EventForm.js

// 생성 혹은 수정 action
export async function action({ request, params }) {
  const method = request.method;
  const data = await request.formData();
  const token = getAuthToken();

  const eventData = {
    title: data.get("title"),
    image: data.get("image"),
    date: data.get("date"),
    description: data.get("description"),
  };

  let url = "http://localhost:8080/events"; // 기본값은 생성(post)

  if (method === "PATCH") {
    const eventId = params.eventId;
    url = "http://localhost:8080/events/" + eventId;
  }

  const response = await fetch(url, {
    method: method,
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(eventData),
  });

  if (response.status === 422) {
    return response;
  }

  if (!response.ok) {
    throw json({ message: "Could not save event." }, { status: 500 });
  }

  return redirect("/events");
}

이렇게 하고 우선 로그인을 하면 정상적으로 로컬스토리지에 token이라는 이름에 적당한 토큰값이 저장되어있는걸 확인할 수 있고, 해당 토큰을 가지고 처리되는 생성, 수정 , 삭제 로직은 정상적으로 기능하는걸 확인 할 수 있었다.




✔️ 로그아웃 추가하기


이제 토큰을 사용해 사용자에게 권한을 주어 앱을 조작할 수 있도록 했다.

다만,
정상적인 로그인이 되었다면, 네비게이션에 여전히 하드코딩 되어있는 Authentication 경로를 로그아웃 할 수 있는 링크버튼으로 전환해주면 사용자 경험상 매우 좋을것 같고 추가로 로그인이 되지 않았을경우 사용자가 할 수없는 기능들은 하지 못하게 설정해주는것도 UX상 필요하지 않을까 싶다.

두가지 다 진행해보자.


✔️ 토큰의 유무에 따른 UI업데이트

1. 네비게이션 상태


토큰의 유무에 따라서 UI변경을 수정해야한다. 그렇다면 저장했던 토큰을 상황에 따라 제거도 해야한다.
-> 이걸 로그아웃로직에 이벤트로 처리할 계획이다.
-> 단순히 클릭 이벤트 리스너를 추가해서 토큰을 삭제하는 방법도 있지만 리엑트 라우팅적으로 접근하여 새 라우터를 추가해서 처리해 보자.


로그아웃 라우터 하나 만들어서 로직 구현한다.

Logout.js

import { redirect } from "react-router-dom";

// 로컬스토리지에 있는 토큰을 지우고 홈페이지로 리디렉션하는 구조이다.

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

형식은 로그아웃 페이지이지만 실제적으로 렌더링 되는 컴포넌트는 없고 실행하는 작업만 등록해주도록 하자.

정의했으니, 그 다음 해당 라우터를 다시 App에서 등록해주고,

import { action as logoutAction } from "./pages/Logout";

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    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,
              },
            ],
          },
          {
            path: "new",
            element: <NewEventPage />,
            action: manipulateEventAction,
          },
        ],
      },
      {
        path: "auth",
        element: <AuthenticationPage />,
        action: authAction,
      },
      {
        path: "newsletter",
        element: <NewsletterPage />,
        action: newsletterAction,
      },
      {
        path: "logout",
        action: logoutAction,
      },
    ],
  },
]);

그 다음 네비게이션에 react-router-dom에서 제공하는 Form컴포넌트를 통해 양식을 제출해서 전환 될 수 있게 설정할 것이다.

MainNavigation.js

import { Form, NavLink } from "react-router-dom";

import classes from "./MainNavigation.module.css";
import NewsletterSignup from "./NewsletterSignup";

function MainNavigation() {
  return (
    <header className={classes.header}>
      <nav>
        <ul className={classes.list}>
          <li>
            <NavLink
              to="/"
              className={({ isActive }) =>
                isActive ? classes.active : undefined
              }
              end
            >
              Home
            </NavLink>
          </li>
          <li>
            <NavLink
              to="/events"
              className={({ isActive }) =>
                isActive ? classes.active : undefined
              }
            >
              Events
            </NavLink>
          </li>
          <li>
            <NavLink
              to="/newsletter"
              className={({ isActive }) =>
                isActive ? classes.active : undefined
              }
            >
              Newsletter
            </NavLink>
          </li>
          <li>
            <NavLink
              to="/auth?mode=login"
              className={({ isActive }) =>
                isActive ? classes.active : undefined
              }
            >
              Authentication
            </NavLink>
          </li>
          <li>
            <Form action="/logout" method="post">
              <button>Logout</button>
            </Form>
          </li>
        </ul>
      </nav>
      <NewsletterSignup />
    </header>
  );
}

export default MainNavigation;

그리고 Form에 해당 속성으로 action을 내가 방금 전 만든 로그아웃 로직이 있는 해당 경로를 설정해주고, method는 post로 정해준다.


이제 로그인 상태에 따라서 UI를 본격적으로 업데이트 해보자.

이 앱 전체 모든 경로에서 토큰을 쉽게 사용할 수 있도록(자동으로 토큰의 유무를 판단하여 업데이트 되도록)해야 한다.
getAuthToken함수를 사용해 불러오는 방식을 적용한다면 컴포넌트에선 코드가 재평가 될때만 호출되기때문에 토큰이 삭제되면 재평가가 안될 수도 있다.

따라서 리엑트 컨텍스트 API를 사용해 토큰상태만 전역에 걸쳐 관리해주도록 할것이다.


auth.js

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

// 토큰 불러오는 함수
export function tokenLoader() {
  getAuthToken();
}

현재 리엑트 라우터패키지를 사용중이기에 루트 라우터쪽에 가서 loader를 추가해준다.

App.js

import { tokenLoader } from "./util/auth";

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    id: 'root',
    loader: tokenLoader,
    children: [ ...

이 loader는 로컬 저장소를 살펴본 후 있다면 거기서 토큰을 추출할것이며, 루트쪽에 등록하면 다른 모든 라우터에서 이 loader데이터를 사용 할 수 있게된다.

장점: 로그아웃 버튼 클릭해서 로그아웃 양식을 전송하면 react-router-dom이 알아서 재평가를 해준다.

⭐️⭐️⭐️
그렇기 때문에 전역에서 사용자가 양식을 제출하거나 어떤 버튼 이벤트를 발생시켰다면, 현재상태를 확인하고 최신의 상태를 확보해줄것이다.


모든 라우터(자식)에서 이 loader를 사용할 수 있도록 id를 하나 할당해주고 다시 네비게이션 페이지에서 로더가 반환한 데이터를 사용하여 후처리를 진행하도록 하자.

MainNavigation.js

import { Form, NavLink, useRouteLoaderData } from "react-router-dom";

import classes from "./MainNavigation.module.css";
import NewsletterSignup from "./NewsletterSignup";

function MainNavigation() {
  // 토큰이 있으면 로그인 상태 없으면 로그아웃상태!
  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
                }
              >
                Authentication
              </NavLink>
            </li>
          )}
          {token && (
            <li>
              <Form action="/logout" method="post">
                <button>Logout</button>
              </Form>
            </li>
          )}
        </ul>
      </nav>
      <NewsletterSignup />
    </header>
  );
}

export default MainNavigation;

2. 회원일때 사용할 기능 상태


마찬가지로 로그인이 되어있어야만 가능한 기능들인 새로운 글 생성과 수정 그리고 삭제하는 페이지도 loader에서 토큰을 받아와 처리 해주자.

게시글 네비게이션에서 애초에 생성하는 버튼을 비활성화 시켜보려한다.

import { NavLink, useRouteLoaderData } from "react-router-dom";

import classes from "./EventsNavigation.module.css";

function EventsNavigation() {
  const token = useRouteLoaderData("root");
  return (
    <header className={classes.header}>
      <nav>
        <ul className={classes.list}>
          <li>
            <NavLink
              to="/events"
              className={({ isActive }) =>
                isActive ? classes.active : undefined
              }
              end
            >
              All Events
            </NavLink>
          </li>
          {token && (
            <li>
              <NavLink
                to="/events/new"
                className={({ isActive }) =>
                  isActive ? classes.active : undefined
                }
              >
                New Event
              </NavLink>
            </li>
          )}
        </ul>
      </nav>
    </header>
  );
}

export default EventsNavigation;

상세페이지도 마찬가지로 지정해준다.

import { Link, useRouteLoaderData, useSubmit } from "react-router-dom";

import classes from "./EventItem.module.css";

function EventItem({ event }) {
  const token = useRouteLoaderData("root");
  const submit = useSubmit();

  function startDeleteHandler() {
    const proceed = window.confirm("Are you sure?");

    if (proceed) {
      submit(null, { method: "delete" });
    }
  }

  return (
    <article className={classes.event}>
      <img src={event.image} alt={event.title} />
      <h1>{event.title}</h1>
      <time>{event.date}</time>
      <p>{event.description}</p>
      {token && (
        <menu className={classes.actions}>
          <Link to="edit">Edit</Link>
          <button onClick={startDeleteHandler}>Delete</button>
        </menu>
      )}
    </article>
  );
}

export default EventItem;

이렇게 설정하면 로그아웃 상태에선 보이지 않게 된다.

i) 로그인일때

ii) 로그아웃일때,

📍 라우팅 오류잡기


하지만, 만약 url로 수동으로 새로운 생성페이지에 접근하게되면 양식 컴포넌트가 렌더링 되는걸 볼 수 있다.(물론 제출은 안됨) 분명 로그인 안되어있으면 버튼이 활성화되지 못하게 막았는데 왜그럴까?

토큰이 없다면 아예 접근 자체를 못하도록 수정해야할것같다.

라우터 보호 : 특정 라우터를 접근하지 못하도록 설정하는 것이다.

그래서 로더 하나를 더 추가해 주었다.

auth.js

import { redirect } from "react-router-dom";

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

// 토큰 불러오는 함수
export function tokenLoader() {
  return getAuthToken();
}

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

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

  return null; // loader는  반드시 null 또는 기타 다른 값을 리턴해야 한다.
}

토큰이 없는경우 페이지에 접속하면 인증페이지로 리디렉션 되게했다.
이렇게해서 접근권한이 있을때만 페이지에 접근가능하게 리소스를 보호했다.

profile
마라토너같은 개발자가 되어보자

0개의 댓글