본문 바로가기

JavaScript/NextJS

NextJS - 비인증일 때 로그인 페이지로 리디렉션

이 글은 비인증 사용자가 보안 요소가 포함된 페이지를 방문했을 때, client-side에서 로그인 페이지 등의 다른 페이지로 리디렉션 해주는 방법을 소개합니다.

 

RouteGuard 컴포넌트

RouteGuard 컴포넌트에는 NextJS의 client-side 인증 로직이 있으며, App 컴포넌트에서 사용됩니다.

 

client-side 인증은 authCheck() 함수에 포함되어 있으며, 앱 로드 및 경로가 변경될 때마다 실행됩니다. 만약 로그인 없이 보안 요소가 포함된 페이지에 접근하려고 하면 그 페이지의 내용이 표시되지 않고 로그인 페이지로 자동 이동됩니다. returnUrl은 리디렉션 쿼리 파라미터 중 하나이며, 로그인이 성공했을 때 요청했던 페이지로 이동시켜줄 때 사용됩니다.

 

authorized 상태(state)는 리디렉션이 되기 전에 페이지가 잠깐 보이는 것을 방지하는 데 사용됩니다. 이를 사용한 이유는 NextJS의 routeChangeStart 이벤트를 사용해 경로 변경을 취소하는 명확한 방법을 아직 찾지 못했기 때문입니다.

 

/components/RouteGuard.js

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';

function RouteGuard({ children }) {
    const router = useRouter();
    const [authorized, setAuthorized] = useState(false);

    useEffect(() => {
        // 컴포넌트가 로드되었을 때 - authCheck 호출
        authCheck(router.asPath);

        // 라우트 변경이 시작되었을 때 - authorized 상태를 false로 설정해 페이지의 콘텐츠를 숨김
        const hideContent = () => setAuthorized(false);
        router.events.on('routeChangeStart', hideContent);

        // 라우트 변경이 완료되었을 때 - authCheck 호출 
        router.events.on('routeChangeComplete', authCheck)

        // useEffet 반환 함수에서 이벤트를 해제
        return () => {
            router.events.off('routeChangeStart', hideContent);
            router.events.off('routeChangeComplete', authCheck);
        }
    }, []);

    function authCheck(url) {
        // 로그인 없이 보안 요소가 포함된 페이지에 방문하려고 하면, 로그인 페이지로 리디렉션
        const publicPaths = ['/login'];
        const path = url.split('?')[0];
        
        if (유저인증체크 && !publicPaths.includes(path)) {
            setAuthorized(false);
            router.push({
                pathname: '/login',
                query: { returnUrl: router.asPath }
            });
        } else {
            setAuthorized(true);
        }
    }

    return (authorized && children);
}

export default RouteGuard;

 

 

App 컴포넌트

App 컴포넌트는 NextJS 앱의 최상위 컴포넌트이며, 외부 html, main nav, 현재 페이지 컴포넌트를 가집니다. 현재 페이지 컴포넌트인 <Component {...pageProps} />는 위에서 만든 <RouteGuard> 컴포넌트가 감싸고 있습니다.

 

/pages/_app.js

import Head from 'next/head';
import RouteGuard from '@/components/RouteGuard';

export default App;

function App({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>Example</title>
      </Head>

      <div>
        <RouteGuard>
          <Component {...pageProps} />
        </RouteGuard>
      </div>
    </>
  );
}

 

로그인 페이지

로그인 페이지에는 NextJS 앱에서 로그인할 때 사용되는 아이디와 비밀번호를 저장할 react-hook-form 라이브러리가 포함되어 있습니다.

 

입력 폼의 유효성 검사 규칙은 yup 스키마 유효성 검사 라이브러리에 정의되어 있고, 이는 react-hook-form의 useForm() 함수 formOption에 전달됩니다. yup에 대해 자세히 알고 싶으시다면 여기를 참고해 주세요.

https://github.com/jquense/yup

 

useForm() 함수는 register, handleSubmit, formState 등을 반환하는데, 자세히 알고 싶으시다면 여기를 참고해 주세요.

https://react-hook-form.com/api/useform

 

onSubmit 함수는 입력 폼이 유효하고 submit이 클릭됐을 때 호출되며, 유저 기밀 정보를 api에 submit합니다. 로그인에 성공한 유저는 이전에 요청했던 페이지(returnUrl) 또는 홈('/')으로 리디렉션 됩니다.

 

반환된 JSX 템플릿은 로그인에 필요한 input 등의 html을 포함하고 있습니다. 입력 폼의 필드들은 react-hook-form의 register 함수로 관리됩니다. 더 자세히 알고 싶으시다면 여기를 참고해 주세요.

https://jasonwatmore.com/post/2021/04/21/react-hook-form-7-form-validation-example

 

아래 코드가 동작하려면 라이브러리를 설치해야 합니다.

$ npm i react-hook-form yup @hookform/resolvers/yup

 

/pages/login.jsx

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

export default Login;

function Login() {
  const router = useRouter();

  useEffect(() => {
    // 이미 로그인 되어 있으면 홈('/')으로 리디렉션
    if (유저인증체크) {
        router.push('/');
    }
  }, []);

  // 입력 폼 유효성 검사 규칙
  const validationSchema = Yup.object().shape({
    username: Yup.string().required('아이디를 입력해주세요.'),
    password: Yup.string().required('비밀번호를 입력해주세요.')
  });
  const formOptions = { resolver: yupResolver(validationSchema) };

  // useForm() 훅을 통해 함수들 얻기
  const { register, handleSubmit, setError, formState } = useForm(formOptions);
  const { errors } = formState;

  function onSubmit({ username, password }) {
    return 로그인처리함수(username, password)
    .then(() => {
        // 쿼리 파라미터에서 이전에 요청했던 경로 얻기
        const returnUrl = router.query.returnUrl || '/';
        router.push(returnUrl);
    })
    .catch(error => {
        setError('apiError', { message: error });
    });
  }

  return (
    <>
      <h4>Next.js Basic Authentication Example</h4>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>Username</label>
          <input name="username" type="text" {...register('username')} className={`${errors.username ? 'is-invalid' : ''}`} />
          <div>{errors.username?.message}</div>
        </div>
        <div>
          <label>Password</label>
          <input name="password" type="password" {...register('password')} className={`${errors.password ? 'is-invalid' : ''}`} />
          <div>{errors.password?.message}</div>
        </div>
        <button disabled={formState.isSubmitting}>
          {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
            Login
        </button>
        {errors.apiError &&
          <div>{errors.apiError?.message}</div>
        }
      </form>
    </>
  );
}