이 글은 비인증 사용자가 보안 요소가 포함된 페이지를 방문했을 때, 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>
</>
);
}
'JavaScript > NextJS' 카테고리의 다른 글
HttpOnly 쿠키를 활용한 JWT 로그인 구현 (0) | 2023.02.04 |
---|---|
NextJS 백그라운드 서버 시작 (0) | 2022.12.28 |
NextJS - 경로 alias 설정하기(typescript) (0) | 2022.10.03 |
WebSocket connection to 'ws://localhost/_next/webpack-hmr' failed; (0) | 2022.06.21 |
NextJS - 사용해보기 (0) | 2022.06.02 |