로그인


로그인은 크게 인증인가로 나누어 볼 수 있음


[인증] = API 요청을 통해 사용자임을 인증 받는 단계, 이 단계를 통해 백엔드에서 고유한 id값을 응답으로 받게 됨

[인가] = 매번 API 요청 할 때마다, 받아놓은 id값을 함께 보내어 백엔드에서 지금 요청을 하는 자가 누구인지를 식별 가능하게 함 (id값이 일치하다면 인가 진행 됨)





로그인의 역사 1

  • 브라우저에서 특정 email, password로 로그인을 하게 되면 백엔드로 login API 요청이 날라가고, 백엔드에서 해당 유저가 있는지 DB에서 확인 후 있다면 유저의 정보를 session에 따로 저장하게 됨

  • 이후 해당 유저에 특정 id값을 부여한 다음 이를 응답과 함께 보내줌
    *이후 프론트에서 유저가 API 요청을 할 때마다 이 부여된 id를 함께 보냄으로써 백엔드에서 누가 요청하는 것인지를 식별할 수 있게 해 줌



[BUT] => 유저 정보를 백엔드 서버로 받다 보니 한 번에 여러명의 정보를 받는 데에는 한계가 있음
*이 점을 해결하기 위해 백엔드 컴퓨터의 성능을 scale-up 하게 됨






로그인의 역사 2

  • scale-up 으로 백엔드 컴퓨터의 성능을 올려주었지만 훨씬 많은 유저의 접속이 동시다발적으로 일어날 경우 서버의 부하가 발생했음

  • 그래서 이를 해결하기 위한 방안으로 백엔드 컴퓨터를 복사하는 방법이 생겨났고, 이를 통해 서버의 부하를 분산 가능하게 함


[BUT]

-1. 이 경우 로그인 이후 API 요청하는 컴퓨터와 생성된 session이 저장된 컴퓨터가 같아야만 로그인 정보를 가져올 수 있음
ex. A컴퓨터로 처음 로그인 하면 A컴퓨터에 로그인 정보 session이 저장됨, 이후 특정 API를 요청 시 만약 A컴퓨터가 아닌 B컴퓨터로 요청이 들어가지면 B컴퓨터에는 로그인 정보가 담긴 session이 없기에 정보를 가져오지 못함


-2. 백엔드 컴퓨터를 복사해도 결국 DB는 하나이기에, 오히려 DB에 부하가 몰리는 병목현상 발생






로그인의 역사 3


  • 위 문제점들을 해결하기 위해 로그인 정보를 session이 아닌 DB에 저장하기 시작

  • 다만 DB의 부하가 발생하는 것은 똑같았고, 이를 해결하기 위해 데이터를 쪼개기 시작하면서 서버 부하문제를 해결

    DB 데이터 쪼개기 (파티셔닝)

    -수직 쪼개기 (수직 파티셔닝)
    -수평 쪼개기 (수평 파티셔닝)


[BUT]

-DB는 데이터들이 disk에 저장되기 때문에 안전하지만 속도가 느림

-즉, 로그인 정보를 DB 데이터가 저장된 disk 에서 추출(scrapping)해 오기 때문에 속도가 느림




Redis 사용

  • 앞의 disk의 속도 문제를 해결하기 위해 로그인 정보를 DB가 아닌 Redis 라는 임시 저장소에 저장하는 방식으로 바꾸게 됨

  • 이렇게 백엔드에 로그인 정보(현재 상태정보)를 두지 않고(stateless), 임시저장소에 저장해 두었다가 필요할 때마다 여기서 데이터를 꺼내오면서 속도 문제도 해결하게 됨







로그인의 역사 4 (JWT 토큰)


로그인 정보를 굳이 서버나 DB에 저장해야 되는가 라는 의문과 함께 새로운 저장방식으로 탄생한 것이 JWT 토큰을 이용한 로그인 방식


  • JWT 토큰은 로그인 API 요청 시 받은 유저 정보를 객체형태로 담고, 이를 문자열로 만들어 암호화한 후에 이 암호화한 키(accessToken)를 브라우저에 응답과 함께 보냄

  • 응답으로 받은 암호화 키 (accessToken)는 브라우저 저장소에 저장되고, 이후 API 요청 시 해당 accessToken을 백엔드로 보내고, 백엔드는 이를 복호화 해서 객체 안에서 유저 정보를 식별한 후 접근을 허용하게 됨
    *즉, 암호화 된 키 (accessToken)이 고유한 id 값이 되는 거라 볼 수 있음!

    *이를 통해 로그인 정보를 굳이 DB에서 찾아보지 않고도, 바로바로 식별 가능하게 됨 (JWT 토큰을 복호화 하면 객체 데이터 안에 그대로 담겨 있는 유저 정보를 활용)









단방향 암호화(Hashing), 양방향 암호화



  • 로그인 이후, 로그인 정보를 fetch 할 때 브라우저에서 비밀번호 정보는 fetch 할 수 없어야 함 (비밀번호는 알아내는 것이 불가능해야 함)

  • 해커가 DB를 해킹했을 때, 비밀번호 같은 민감한 정보에 그대로 접근이 가능하면 안되기 때문에 백엔드에서는 이런 민감한 정보를 그대로 저장하지 않고, 암호화시켜 저장함




양방향 암호화


  • 암호화, 복호화 모두 가능한 암호화 방식
    (해킹 시 암호 복호화로 민감한 정보 접근 용이)




단방향 암호화


  • 암호화는 가능하나 복호화가 안되는 암호화 방식

  • 만약 하나의 알고리즘으로 복호화가 가능하다면 해당 알고리즘으로 DB의 모든 사용자의 민감한 정보에 해커가 접근할 수 있기 때문에, 복호화 방식의 경우의 수를 늘려서 원래 정보에 접근이 어렵도록 함

    *하지만 이 또한 레인보우 테이블 같은 방식으로 24시간 컴퓨터를 돌려서 복호화를 할 수도 있기에 아주 안전한 방식은 아님





해싱(Hashing) 사용

Hashing = 어떤 수학적 연산에 원본 데이터를 매핑시켜 완전히 다른 데이터값으로 변환시키는 것

  • salt(임의의 문자열)를 추가해서 반복적으로 hashing

    ex. [비밀번호+salt]를 hash -> 한 것을 다시 [hash한 것 + salt]를 hash -> 한 것을 또 다시 [또 한것 + salt]를 hash...반복

    *이 경우 현재의 컴퓨터 성능으로는 복호화가 어렵기 때문에 안전함







API 요청에 토큰 같이 보내기


  • 로그인 이후 인가(요청 API 보내기)할 때마다 사용자임을 식별할 수 있는 고유 id (토큰)을 보내주어야 함

  • 이 토큰 정보는 header에 Authorization key에 담아서 보내줄 수 있음

  • 토큰 앞에 사용되는 Bearer 는 관례상 사용하는 것이며 이 부분은 백엔드 개발자와 상의해서 사용해야 함







JWT 토큰의 조작불가능성


  • API 응답에서 accessToken으로 받아온 JWT 토큰은 JWT 사이트에서 Decode(복호화) 해 볼 수 있음



  • 문제는 JWT 토큰을 decode하면 토큰의 모든 정보가 조회가능하다는 것

  • 이는 JWT 토큰의 특징으로 누구든지 토큰만 탈취한다면 사이트에서 토큰 정보 조회가 가능

    따라서 민감하거나 중요한 정보는 JWT 토큰에 저장해서는 안 됨!




+a) JWT 토큰의 조작 방지

  • JWT 토큰은 조회는 가능하지만 내용 조작을 위해서는 비밀번호를 알아야 함
    *해당 비밀번호는 백엔드에서 생성하기에 알 수 없음

  • 또한 보안문제로 토큰의 만료시간을 짧게 지정
    *짧은 토큰 만료기간을 주어 제3자가 해당 토큰 데이터를 이용해 악용할 수 없도록 함









로그인 기능 실습


// login 폴더의 index.tsx _ accessToken global state에 저장해주기
import {useMutation,gql} from "@apollo/client"
import {ChangeEvent} from "react"
import { useRecoilState } from "recoil";
import {useRouter} from "next/router"

cosnt LOGIN_USER = gql`
	mutation loginUser($email:String){
		loginUser(email: $email, password: $password){
			accessToken
		}
	}
`

export default function LoginPage(){
	const [email,setEmail]=useState("")
	const [password,setPassword]=useState("")
	const [loginUser] = useMutation<Pick<IMutation,'loginUser'>,IMutationLoginUserArgus>(LOGIN_USER)
	const router = useRouter()
	cosnt [accessToken,setaccessToken] = useRecoilState(accessTokenState)

	const onChangeEmail = (event:ChangeEvent<HTMLInputElement>)=>{
		setEmail(event.target.value)
		}
	const onChangePassword = (event:ChangeEvent<HTMLInputElement>)=>{
    setPassword(event.target.value)
		}

	const onClickLogin = async()=>{
	try{
		cosnt result = await loginUser({
			variables:{
					email : email,
					password : password
				}
			})
			const accessToken = result.data?.loginUser.accessToken
			setAccessToken(accessToken)
			router.push('/loginsuccess')
		}catch(error){
			// alert(error.message)을 사용하셔도 무방합니다.
			Modal.error({content : error.message})
		}
	} 

	return(
		<div>
			이메일 : <input type="text" onchange={onChangeEmail}/> <br/>
			비밀번호 : <input type="password" onchange={onChangePassword}/> 
			<button onClick={onClickLogin}>로그인하기!!</button>
		</div>
	)
}
  • loginUser API 에 입력한 email, password가 변수값으로 들어가서 요청이 이루어지고, 이후 accessToken을 반환함

  • 반환받는 accessToken을 useRecoil을 이용해 전역으로 저장





//app.tsx파일
import { RecoilRoot } from "recoil";

function MyApp({ component,pageProps }:AppProps){

	return (
		<RecoilRoot>
        <ApolloSetting>
          <Global styles={globalStyles} />
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </ApolloSetting>
      </RecoilRoot>
	)
}
  • app 파일에서 recoil 기능을 사용할 컴포넌트들을 RecoilRoot 태그로 감싸줌







//app.tsx파일
function MyApp({ component,pageProps }:AppProps){
const [accessToken,setAccessToken] = useState("")
const uploadLink = createUploadLink({
		uri : "백엔드 주소",
		headers : { Authorization : `Bearer ${accessToken}` }
	})

	return (
			<ApolloProvider client={client}>
				<Component {..pageProps}/>
			</ApolloProvider>
	)
}
  • Apollo- setting 부분에서 headers: {Authorization: `Bearer ${accessToken}`} 으로 지정하여 모든 컴포넌트에서 요청을 보낼 때 header 부분에 accessToken을 담아서 보내도록 함

  • 위 설정으로 인해, 로그인을 하지 않았다면 header에 accessToken 없이 요청이 들어가게 될 것이고, 이에 따라 로그인 유무를 식별할 수 있음







// loginsuccess 폴더의 index.tsx
const FETCH_USER_LOGGED_IN = gql`
query fetchUserLoggedIn{
	fetchUserLoggedIn{
		email
		name
		}
	}
`
export default function LoginSuccessPage(){
	const {data} = useQuery<Pick<IQuery,"fetchUserLoggedIn">>(FETCH_USER_LOGGED_IN)

	return(
		<div>
			{data?.fetchUserLoggedIn.name}님 환영합니다.
		</div>
	)
}
  • 이후 fetchUserLoggedIn으로 로그인 된 유저의 정보를 조회하려고 하면 해당 API 요청 시 header도 보내지기 때문에 로그인이 되었다면 header의 변수에 accessToken의 값이 담기게 되고, 이에 따라 fetch 가 가능하게 됨
    *accessToken 값이 없다면 fetch 불가능!









새로고침과 로그인 정보 불러오기


  • 위의 경우처럼 setState 로 변수에 accessToken을 넣어서 사용하면 새로고침시 값이 초기화 되면서 저장해둔 토큰이 날라가서 로그인 정보 데이터가 사라져버림



  • 이를 해결하기 위한 방안으로 영구적인 저장을 위해 두 가지 방법 사용 가능


    1. 브라우저의 local Storage 에 저장
    *보안에 취약한 편이라 실무적인 방법은 아님 (임시방편)

    2. refreshToken 사용
    *실무적인 방법








브라우저의 local Storage 에 저장

+a) 브라우저 저장소의 종류와 특징

  • localstorage = 브라우저 껐다 켜도 데이터 유지

    -localStorage 값 저장 = localstorage.setItem("key", "value")
    -localStorage 값 꺼내기 = localstorage.getItem("key")



  • sessionStorage = 브라우저 껐다 키면 데이터 초기화

  • cookies = 브라우저 껐다 켜도 데이터 유지 (만료시간 설정 가능),/ 보안기능 강화(httpOnly, Secure..)
    *다른 storage들과 cookies의 차이점
    => [cookies는 만료시간을 설정할 수 있으며 자동으로 백엔드 서버와 연동이 가능!]






// login-localstorage 폴더의 index.tsx

import {useMutation,gql} from "@apollo/client"
import {ChangeEvent} from "react"
import { useRecoilState } from "recoil";
import {useRouter} from "next/router"

cosnt LOGIN_USER = gql`
	mutation loginUser($email:String){
		loginUser(email: $email, password: $password){
			accessToken
		}
	}
`

export default function LoginPage(){
	cosnt [accessToken,setaccessToken] = useRecoilState(accessTokenState)
 
	const [email,setEmail]=useState("")
	const [password,setPassword]=useState("")
	const [loginUser] = useMutation<Pick<IMutation,'loginUser'>,IMutationLoginUserArgus>(LOGIN_USER)
	const router = useRouter()

	const onChangeEmail = (event:ChangeEvent<HTMLInputElement>)=>{
		setEmail(event.target.value)
		}
	const onChangePassword = (event:ChangeEvent<HTMLInputElement>)=>{
    setPassword(event.target.value)
		}

	const onClickLogin = async()=>{
	try{
		// 1. 로그인해서 accessToken 받오기
		cosnt result = await loginUser({
			variables:{
					email : email,
					password : password
				}
			})
			const accessToken = result.data?.loginUser.accessToken

			// 2. accessToken이 있다면 global state에 저장 후 localStorage에 저장하기
			if(accessToken){setAccessToken(accessToken || "" )
			
			// 3. 로그인 성공페이지로 이동하기
				void router.push('/loginsuccess')
				localStorage.setItem("accessToken",accessToken) // 임시로 사용 나중에 지울예정
			}
		}catch(error){
			// alert(error.message)을 사용하셔도 무방합니다.
			Modal.error({content : error.message})
		}
	} 

	return(
		<div>
			이메일 : <input type="text" onchange={onChangeEmail}/> <br/>
			비밀번호 : <input type="password" onchange={onChangePassword}/> 
			<button onClick={onClickLogin}>로그인하기!!</button>
		</div>
	)
}
  • 받아 온 accessToken의 값을 localStorage.setItem으로 로컬 스토리지에 저장

  • 위 코드에서 새로고침 하면 setState로 저장한 accessToken 의 값은 초기화되기 때문에 setAccessToken(localStorage.getitem("accessToken")) 페이지가 렌더링 될 때마다 로컬스토리지에 저장된 값을 불러와서 accessToken 변수의 값으로 지정해 줘야 함


    [BUT] => 위의경우 localstorage is not defined 라는 에러 발생!








not defined 에러와 Next.js의 렌더링 방식 이해

  • 브라우저에 주소를 입력하고 들어가면 우선 프론트 서버 (yarn dev로 실행시킨 서버) 에서 html, css, js 를 다운 받아서 화면에 그리는 과정을 거치게 됨

  • 이 때, html이 우선 다운되고, html 상에 있는 css, js는 이후 화면에서 그려지는 과정에서 다운받고 그려지게 됨

  • 미리 그려본 것을 html 형식으로 만들고 이를 브라우저로 보냄(prerendering)
    *프론트 서버에서 미리 그려보는 과정에서 localstorage 같은 브라우저에서 실행되는 기능은 읽어들이지 못하기 때문에 not defined 에러가 발생!!

  • 이후 브라우저에서 그린 부분과 프론트 서버에서 그린 내용의 차이점을 비교 (diffing)

  • diffing 으로 비교 후 최종적으로 반영해서 렌더링(hydration)





프론트 서버에서 프리렌더링 시 생기는 not defined 에러 해결법 3가지


1. 현재 브라우저인지 여부를 체크해서 실행하기

1. // 프리렌더링 예제() - process.browser 방법
  if (process.browser) {
    console.log("나는 지금 브라우저다");
    alert("반갑다!");
  } else {
    console.log(
      "지금은 아직 프론트엔드 서버다(yarn dev 로 실행시킨 프로그램 내부다!)"
    );
  }
  • 조건문에 porcess.browser 를 추가하여 브라우저에서 실행되고 있는지 아닌지 분기를 나눔







2. window 타입여부로 체크해서 실행하기

// 2. 프리렌더링 예제 - typeof window 방법
  if (typeof window !== "undefined") {
    console.log("나는 지금 브라우저다");
    alert("반갑다!");
  } else {
    console.log(
      "지금은 아직 프론트엔드 서버다((yarn dev 로 실행시킨 프로그램 내부다!))"
    );
  }
  • typeof window 가 undefined가 아니라면 현재 브라우저에서 실행중인 것
    *브라우저에서 실행중인 겨우 window의 type은 전역 객체로 존재함







3. useEffect로 실행하기 (자주 쓰이는 방식)

// 3. 프리렌더링 예제 - 프리렌더링 무시 방법
  useEffect(() => {
    const result = localStorage.getItem("accessToken") ?? "";
    setAccessToken(result);
  }, []);
  • useEffect는 기본적으로 브라우저에 화면이 렌더링 된 이후 실행되기 때문에 위와 같이 작성하면 렌더링 된 이후 useEffect 내의 코드가 한 번 실행됨









// src/components/commons/apollo/index.tsx

// Apollo Setting 빼주기
import { useRecoilState } from "recoil";
import { accessTokenState } from "../../../commons/store";

export default function ApolloSetting(props) {
	const [accessToken,setAccessToken] =useRecilState(accessTokenState)

	useEffect(()=>{
		if(localStorage.getItem("accessToken")){
		setAccessToken(localStorage.getItem("accessToken")||"")
	}
},[])

	const uploadLink = createUploadLink({
			uri : "백엔드 주소",
			headers : { Authorization : "Bearer 받아온 토큰" }
		})

	return (
		<ApolloProvider client={client}>
			{props.children}
		</ApolloProvider>
	)
}
  • 브라우저인지 아닌지 분기를 체크하여 실행될 수 있도록 해주면 이후 새로고침을 해도 데이터가 초기화되어 날아가지 않고, 렌더링 될 때마다 로컬스토리지에 저장된 값을 불러와서 accessToken 값으로 할당하여 header를 보내기 때문에 로그인 정보 data가 날라가지 않음









권한분기


  • 로그인 인증 이후에는 이에 따른 권한분기가 이루어 짐

  • 로그인을 한 사람, 안 한사람, 운영자로 로그인 한 사람, 판매자로 로그인 한 사람 등 다양하게 권한을 분리가 가능






권한분기 사전지식 - 스택과 큐, 스코프체이닝, 클로저



스택

  • 하나의 출입구로, 가장 먼저 입력된 함수가 가장 나중에 스택을 빠져나가는 형태

  • FILO (First In Last Out)






  • 양쪽으로 출입구가 있어 가장 먼저 입력된 함수가 가장 먼저 빠져 나감

  • FIFO (First In First Out)







스코프 체이닝 (Scope Chaining)


  • 해당 스코프(범위)에 없으면 그 상위 스코프(범위)로, 상위 스코프에도 없으면 더 상위의 스코프로 계속해서 찾아 올라가는 과정






클로저 (closure)


// closure.html 파일
<!DOCTYPE html>
<html lang="ko">
	<head>
		<title>클로저 실습</title>
		<script>
			function aaa(){
				const apple = 10

				function bbb(){
					console.log(apple)
				}
				bbb()
			}
			aaa();
		</script>
	</head>
	<body>
		클로저 실습
	</body>
</html>
  • 위 코드에서 bbb 함수가 실행되면 apple의 값이 없어서 이를 찾기 위해 상위 함수 aaa의 스코프로 올라가 찾아보게 됨 (스코프 체이닝 발생)

  • 이 때, 해당 상위 함수 aaa 와 상위함수를 둘러싼 환경 (범위)를 클로저라고 함

  • 즉, aaa함수는 bbb함수의 클로저라고 할 수 있음










HOF, HOC


HOF => function 을 return

HOC => JSX Element 를 return





함수를 리턴하는 함수 HOF


function aaa(){
	console.log("저는 aaa예요")

	return function bbb(){
		console.log("저는 bbb예요")
	}
}
  • 위 코드에서 aaa( ) 로 함수를 실행하면 bbb 함수가 그대로 return 됨

  • 따라서 aaa( )이 곧 bbb 함수 자체가 되기에 aaa( )( )는 bbb 함수를 실행한 것과 같은 결과를 도출






// 함수 선언식
function aaa(apple){

	return function bbb(banana){
		console.log(banana)
		console.log(apple)
	}
}

aaa(2)(3)

// 실행 결과
// 2 => aaa에 넣은 인자값
// 3 => bbb에 넣은 인자값
  • 위와 같이 매개변수 자리에 각각의 값을 넣어 결과 도출 가능






// 중괄호 생략
const aaa = (apple)=>(banana)=>{
				console.log(apple)
				console.log(banana)
}

aaa(2)(3)
  • 중괄호 생략(return 생략)된 화살표 함수로 보다 더 간단하게 표현할 수도 있음






HOF 사용 예시

 const onClickPage = (page: number) => (): void => {
    void refetch({ page: page });
  };

  return (
    <div>
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          <span style={{ margin: "10px" }}>{el.title}</span>
          <span style={{ margin: "10px" }}>{el.writer}</span>
        </div>
      ))}

      {new Array(10).fill("철수").map((_, idx) => (
        <span key={idx + 1} onClick={onClickPage(idx + 1)}>
  • 기존의 event 값을 가져오는 것이 아닌, 인자로 index 값을 넣어서 바로 매개변수로 전달하여 사용 가능
    만약 event 값을 사용한다면 onClickpage 함수의 두번째 매개변수 자리에 event를 넣어서 받아와서 사용 가능

    기본적으로 태그 내에 함수옆에 소괄호를 붙이면 (ex. function()) 렌더링 과정에서 함수지정이 아닌 함수호출이 되어버리기 때문에 바로 실행되는 것을 막기 위해 HOF를 사용







HOC (Higher Order Component)


  • 상위에 있는 컴포넌트로, 다른 컴포넌트보다 먼저 실행되는 컴포넌트



  • Aaa 컴포넌트를 기준으로 보면 hoc 컴포넌트가 먼저 실행되는데 이 때 hoc 함수의 인자로 Bbb 컴포넌트가 들어가게 됨

  • Hoc 컴포넌트에 두 번째 인자로 들어가는 {qqq: "철수"} 는 hoc함수가 return 하는 함수의 매개변수 props의 인자로 들어감

  • 이 받아오는 props를 그대로 다시 Component를 return 하는 과정에서 해당 Component에 {...props} 로 그대로 props로 다시 전달해 줌






HOC 사용예시 = withAuth 만들기

// src/components/commons/hoc/withAuth.tsx 파일

export const withAuth = (Component:any)=>(props:any)=>{
	const router = useRouter()

	//loginCheckSuccess 파일에 있는 useEffect를 가지고 오시면 됩니다. 
	useEffect(()=>{
		if(!localStorage.getItem("accessToken")){
			alert("로그인을 먼저 해주세요")
			void router.push("/로그인 페이지")
		}
	},[])

	return <Component {...props} />
}
  • 권한분기를 체크하는 withAuth 함수를 만듬

  • 첫 번째 매개변수로 Component를 받아오고, 해당 Component에 넣을 props를 두 번째 매개변수로 받아옴

  • 화살표 함수로 return이 생략되어 있지만, 사실 두 번째 인자로 받는 props는 withAuth 함수가 return 하는 함수의 매개변수로 들어가는 것이고 해당 함수에서 props를 Component 내의 props로 전달해서 다시 return 하는 것!
    *이 때 props를 매개변수로 하는 함수의 클로저(상위함수) 범위에서 Component 인자를 그대로 가져올 수 있기에 return<Component> 가 가능한 것!!





// loginSuccessPage -> withAuth 적용하기 

const LoginSuccessPage = ()=>{
	const {data} = useQuery(FETCH_USER_LOGGED_IN)

	return <div>{data?.fetchUserLoggedIn.name}님 환영합니다.</div>
}

export default withAuth(LoginSuccessPage)
  • 이후 권한분기를 적용하고 싶은 컴포넌트들은 위와 같이 withAuth의 인자로 넣어서 로그인 체크가 가능

  • 위의 경우 LoginSuccessPage 컴포넌트가 실행되기 이전 withAuth 로그인 체크 컴포넌트가 먼저 실행되고, 로그인 여부를 검사하게 됨

profile
막 발걸음을 뗀 신입

0개의 댓글