[Dev] 0829 / NextJS 페이지 이동간에 발생하는 문제

Noah Ko·2022년 8월 29일
1

DevNote

목록 보기
30/31
post-thumbnail

1. 문제점

어플리케이션을 처음 실행하면 아래와 같이 JWT의 유무를 따진 후 상황에 맞는 페이지로 이동하는 로직을 만들었다.

Token 有 : Main 페이지로 이동.
Token 無 : Login 페이지로 이동.

토큰 유무에 따라 페이지를 이동하는 것은 성공했으나, 토큰이 없는 상태에서 메인 페이지 url로 접근하려고 하면 메인 페이지가 잠깐 보였다가 다시 로그인 페이지로 Redirecting되는 현상이 나타난 것이다.

2. Token이 true일때와 false일 때로 나누면 되는 것 아닌가?

맨처음에는 간단하게 Token을 가져와서 있는 경우 true, 없을 경우는 false로 나누어서, 있는 경우에만 state를 바꾸어 보여주고, 없을 때는 Login page로 보내면 된다고 생각해서 아래와 같이 코드를 작성했다.

import Router, { useRouter } from 'next/router'
import { ReactNode, useEffect, useState } from 'react'
import LoginPage from '../../pages/login'

interface IsLoginProps {
    children: ReactNode
}

export default function IsLogin({ children }: IsLoginProps) {
    const [token , setToken ] = useState<boolean>(false)
 
    useEffect(() => {
        const Token = localStorage.getItem('Token')? true : false
        if(Token === false ) {
            Router.push('/login')
        } else {
            setToken(Token)
        }
    }, [children])
  
    return (
        <>
        {token && children}
        </>
    )
}

그리고 이 컴포넌트는 모든페이지에서 토큰 여부 검사를 실시하는 Provider 역할을 하기 위해서 _app.tsx에서 모든 component를 감싸게 되었다.

import type { AppProps } from 'next/app'
import Head from 'next/head'
import '../styles/globals.css'
import IsLogin from '../components/Service/IsLogin';



function MyApp({ Component, pageProps }: AppProps) {
  return <>
    <Head>
      <title>OPOS</title>
    </Head>
    <RecoilRoot>
      <IsLogin>
        <Component {...pageProps} />
      </IsLogin>
    </RecoilRoot>
  </>
}

export default MyApp

하지만 위와 같이 했을 때의 치명적인 문제점은 Token 없을 때는 아무것도 그려지지 않는 화면이 출력된다는 점이었다.
즉 Token이 없어서 로그인을 할 수 없는데, 로그인 화면까지도 보여주지 않는 어플리케이션이 된 것이다.

3. return에서 두가지 값을 다 정의한다면?

그래서 Token값이 false 일 때만 Login 화면을 출력하여 주면 어떨까 하는 생각에 return에서 token의 state에 따라 출력되는 화면을 다르게 설정해 줬다.

import Router, { useRouter } from 'next/router'
import { ReactNode, useEffect, useState } from 'react'
import LoginPage from '../../pages/login'

interface IsLoginProps {
    children: ReactNode
}

export default function IsLogin({ children }: IsLoginProps) {
    const [token , setToken ] = useState<boolean>(false)

    useEffect(() => {
        const Token = localStorage.getItem('Token')? true : false
        if(Token === false ) {
            Router.push('/login')
        } else {
            setToken(Token)
        }
    }, [children])
  
    return (
        <>
        {token === false && <LoginPage/>}
        {token && children}
        </>
    )
}

이렇게 되면 어플리케이션이 처음 실행되었을 때, token의 initial 값이 false이기 때문에 LoginPage만 보이게 되고, LoginPage에서 MainPage로 접근했을 때도 MainPage를 보여주지 않을 수 있었다.

이렇게만 보면 성공인 것 같지만, token이 false일때 Page를 강제로 설정해 놓은 것이 프로젝트가 진행됨에 따라 약점이 될 수도 있다.

4. 그니까, Token이 있을 때만 children을 보여주면 되는거 아닌가? 없으면 Login Page만 보여주고?

맞다. 결론은 그렇게 되면 되는 것이다. 말하자면 Token이 없는 경우이거나 접근한 페이지가 Login페이지가 아니면 빈 화면을 보여주면 된다. 바꿔말하면 Token이 있거나 Url path가 '/login'인 경우에만 children을 출력하면 되는다는 말이다. 이에 대한 접근방식에는 2가지가 있다.

4-1. PathName에 따라 페이지를 보여준다.

일단 결론부터 보자.
(* 여기서 부터는 받아온 JWT의 유효성 검사를 하는 함수도 포함되었다.)

import Router, { useRouter } from 'next/router'
import { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';

interface WithLoginCheckerProps {
    children: ReactNode
}

export default function WithLoginChecker({ children }: WithLoginCheckerProps) {
    const [token, setToken] = useState<string>("") 
    const { pathname } = useRouter();

    useEffect(() => {
        if(pathname === "/login") return;
        const token = localStorage.getItem('Token') || "";
        try {
            jwt.verify(token, 'secret')
            setToken(token)
        } catch {
            Router.replace('/login')
        }
    }, [pathname])

    return (
        <>
            {(token || pathname === '/login') && children}
        </>
    )
}

일단 useEffect는 무시하고 보자, 사용자가 localhost:3000으로 오게 되면 가장먼저 WithLoginChecker 함수가 실행된다. 그리고는 맨처음 정의한 초깃값 대로 token의 값은 false이기 때문에 화면에는 아무런 것도 출력되지 않는다.

자 이제 useEffect를 살펴보자, useEffect의 deps 안에 변수를 넣게 되면, 컴포넌트가 맨처음 마운트 될때와 deps의 값이 변할 때 실행된다는 특징을 가지고 있다.

즉 이에 따라 위 코드를 순서대로 살펴보면,

  • token의 초기값을 빈 문자열로 설정하고 useRouter를 통해 현재 pathName을 받아온다.
  • 그리고 나서는 return 코드에 따라 화면을 출력해야 하는데, 아무것도 보이지 않는다.
  • 왜냐하면 맨 처음 들어오면 url은 localhost:3000/이고 token은 빈 문자열인 false상태이기 때문이다.
  • 그리고는 페이지가 렌더링이 다 된후에 useEffect에 있는 함수가 실행된다.
  • useEffect 함수가 실행되고서는 일단 지금은 login page가 아니기 때문에 if문에는 걸리지 않는다.
  • 그리고 나서 localStore에서 토큰을 가져오는데 받아온 token이 없기 때문에 빈 문자열이 된다.
  • 그렇게 가져온 token이라는 변수로 try-catch 문으로 들어가는데, 여기서 JWT의 유효성 검사를 한다.
  • JWT의 유효성은 true/false가 아니라, 유효하지 않을 경우 Error를 출력한다.
  • 빈 문자열은 유효성 검사를 해봐야 어차피 유효하지 않기 때문에 오류를 내게 되고, 여기서 catch 부분의 함수가 실행된다.
  • 여기서 login page로 이동하게 되는 것이다.

그러면 여기서 pathName이 './'에서 '/login'으로 바뀌었기 때문에 이번에는 children이 보이게 된다.
또한 pathName이 바뀌었기 때문에 useEffect의 함수가 다시 실행되는데, 현재 pathName은 '/login'이고 if문에 딱 걸리게 된다.
이렇게 되면 return이 있기 때문에 Login page는 출력되면서 더이상 함수는 실행되지 않고 여기서 부터는 Login page의 로직을 따르게 된다.

그리고 나서 여기서 ID와 PWD를 입력후 로그인을 하게되면, axios로 통신을 한 후에 res를 받고 page는 메인('./')으로 이동하게 된다.
이렇게 메인 페이지로 이동하게 되면, WithLoginChecker 함수가 다시 실행되지만, 이번에는 다른다.

똑같이 처음에는 아무것도 그려지지 않겠지만, useEffect를 거치고 나면 token에는 적합한 JWT가 들어오게 되고 이는 결국 token은 ture가 되어 Main Page가 출력되게 된다.

children이라는 말이 조금 헷갈릴수 있지만 여기서 children은 "접근 가능하다"라는 말과 동일시 하면 어떨까.

children = "자식 요소에 접근 가능하다"

또한 여기서 useEffect의 인자가 children에서 pathName으로 바뀌었다. childern이 되면 Token이 없는 경우 '/login'으로 Redirecting될텐데 그러면 또 pathName이 바뀐 것이기 때문에 다시 또 '/login'으로 redirecting되는 지옥이 발생하게 된다. 이를 방지 하기 위해서 pathName으로 변경하고 uesEffect 함수 첫번째 줄에 login페이지에 왔을 때를 함수로 정의하여 멈추게 만들어 주었다.

4-2. Initialized를 통한 페이지 출력

이번 관점도 위의 내용과 크게 다르지는 않다.

import Router, { useRouter } from 'next/router'
import { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';

interface WithLoginCheckerProps {
    children: ReactNode
}

export default function WithLoginChecker({ children }: WithLoginCheckerProps) {
  
    const [initialized, setInitialized] = useState(false);
    const { pathname } = useRouter();

    useEffect(() => {
        if (pathname === "/login") {
            setInitialized(true)
            return;
        }
        
        const token = localStorage.getItem('Token') || "";
        try {
            jwt.verify(token, 'secret')
            setInitialized(true)
        } catch {
            Router.replace('/login')
        }
    }, [pathname])

    return (
        <>  
            {initialized && children}
        </>
    )
}

여기서도 token의 유효성 검사를 했을 때, 에러가 나게 되면 login page로 redirecting 되는데, 이 경우 맨 처음 선언해놓은 false값을 true로 바꿔는 함수때문에 children이 출력되게 된다.

결론

즉, pathName에 따라 보여줄 것인가, pathName에 따라 state를 선언하여 그에 따라 children이 보여지게 만들어 줄것인가에 따라 달라질 수 있다.

PathName이 변화하는 것에 따라 children을 출력하지 않고 useEffect를 잘 활용하여 화면을 보여주고 가리는 것을 잘 조절한다면 충분히 해결이 가능한 문제 였다.

profile
아코 자네 개발이 하고 싶나

0개의 댓글