권한분기와 관리자

로그인한사람과 안한사람, 운영자, 판매자, 구매자로 로그인한 사람들 또한 다르게 만들어줄 수 있다.
물론 페이지에서 if문으로 토큰여부를 확인해서 페이지를 다르게 보여줄 수 있지만
권한인증 페이지를 만들어서 import를 쓰는 것이 좋다. 이걸 실현하려면 클로저 , high-order component를 알아야한다. 일단 권한분기를 알아보자

회원 / 비회원 / 관리자 이런식으로 권한에 따라 다른페이지가 열려야한다.
로그인 권한도 판매자 / 구매자 / 중개자에 따라서 다르게 열려야한다.

이중에 기본은 회원/비회원 으로 어제 로그인으로 보여줬다.
그런데 로그인이 되지 않은 사람은 어떻게 로그인을 할 수 있도록 할까?

먼저 if문으로 접속되지 않은 사람은 로그인 페이지로 넘기도록 해보자

import { gql, useQuery } from "@apollo/client";
import { useContext } from "react";
import { IQuery } from "../../src/commons/types/generated/types";
import { GlobalContext } from "../_app";
import {useRouter} from 'next/router'

const FETCH_USER_LOGGED_IN = gql`
  query fetchUserLoggedIn {
    fetchUserLoggedIn {
      email
      name
    }
  }
`;

export default function LoginSuccessPage() {
  const router = useRouter()
  const{myAccessToken} = useContext(GlobalContext)
  const { data } =
    useQuery<Pick<IQuery, "fetchUserLoggedIn">>(FETCH_USER_LOGGED_IN);


  if(!myAccessToken){
    alert("로그인 한 사람만 입장 가능합니다!! 로그인을 먼저 해주세요")
    router.push('22/01-login')
  }

  return (
    <>
      <div>로그인에 성공하였습니다 !</div>
      <div>{data?.fetchUserLoggedIn.name}님 환영합니다!!!</div>
    </>
  );
}

이렇게 하면 작동은 하지만, 규모가 커졌을 때 수정을 해야할경우, 로그인이 필요한 모든페이지에서 바꿔야한다.
그럼 이걸 컴포넌트에 넣어서 사용한다면 쉽게 쓸 수 있을 것 같다.
한번 분리해보자
src/components/hocs/withAuth.tsx (high order components) 에 넣는다

import { gql, useQuery } from "@apollo/client";
import { IQuery } from "../../src/commons/types/generated/types";
import { withAuth } from "../../src/components/commons/hocs/withaAuth";

const FETCH_USER_LOGGED_IN = gql`
  query fetchUserLoggedIn {
    fetchUserLoggedIn {
      email
      name
      picture
    }
  }
`;

function LoginSuccessPage(props) {
  const { data } =
    useQuery<Pick<IQuery, "fetchUserLoggedIn">>(FETCH_USER_LOGGED_IN);


  return (
    <>
      <div>로그인에 성공하였습니다 !</div>
      <div>{data?.fetchUserLoggedIn.name}님 환영합니다!!!</div>
    </>
  );
}

export default withAuth(LoginSuccessPage)
import { useContext } from "react";
import { useRouter } from 'next/router'
import { GlobalContext } from "../../../../pages/_app";


export const withAuth = (Component) => (props) => {
    const router = useRouter()
    const{myAccessToken} = useContext(GlobalContext);

    if(!myAccessToken){
        alert("로그인 한 사람만 입장 가능합니다!! 로그인을 먼저 해주세요");
        router.push("22/01-login");
}
    
    return <Component {...props} />;
}

이거는 로그인을 했는지 안했는지 검증하는 컴포넌트와 로그인 완료페이지를 나누었다.

이 로그인 withAuth가 상위 HOC인거다. 로그인 완료페이지를 그릴때 app.tsx가 실행되고, 검증페이지인 withAuth가 먼저 실행되고 나서 LoginSuccessPage가 실행된다.

이거를 실현 시키는 방식이 export default withAuth(LoginSuccessPage) 부분인거다.
이렇게하면 로그인했는지 안했는지의 성공여부를 알아낼 수 있다. (오직 로그인을 했는지 안했는지의 역할)

container/presenter부분에서도 마찬가지로 presenter부분에 이 hoc를 넣으면
컨테이너를 실행, hoc를 실생, 프리젠터를 실행한다. 물론 props도 같은 순서로넘어갼다

그럼 export const withAuth = (Component) => (props) => 이걸 한번 알아보자

function aaa(){

    return function bbb(){
console.log("안녕하세요")
}
}

함수 안에서 함수를 리턴하는 구조인 것이다.

여기서 aaa를 실행시키면 안녕하세요가 나올까?
아니다 결론은 bbb 함수를 통채로 가져온다

aaa()
ƒ bbb(){
console.log("안녕하세요")
}

그럼 bbb안의 내용물을 가져오고 싶다면?
(aaa())(), aaa()() 같은 뜻이다
aaa(안의 내용물)(안의 내용물) 이런 뜻으로 생각하면될거같다?

(aaa())()
안녕하세요

이런 함수의 특징, 인자 또한 받을 수 있다.

function aaa(qwer) {
    const asdf = "asdasdasdas"
    return function bbb(zxcv){
    console.log("안녕하세요")
    console.log("qwer은", qwer)
    console.log("zxcv는", zxcv)
    console.log("asdf는", asdf)
}

}

aaa안에 무엇을 넣던 bbb가 출력된다

만약 aaa(123)(666)을 하면 zxcv안에 들어가게된다

aaa(123)(666)
안녕하세요
qwer은 123
zxcv는 666
asdf는 asdasdasdas

클로저

여기서 aaa는 외부함수 bbb안의 qwer, zxcv, asdf를 내부함수라고 한다

앞의 qwe이 컴포넌트 ,zxcv가 prpos인것.
외부함수에서 내부에 있는 함수에 접근할 수 있는걸 클로저라고한다. 위에 했던 함수를 단지 화살표함수롤 바꾼거다.
이 변환은 조금이따 더 하겠다.

HOC & HOF

const Container = () => {
  return (
    <>
      <div>컨테이너 입니다.</div>
      <Presenter aaa="철수" />
    </>
  );
};

const Presenter = (props) => {
  return <div>프리젠터 입니다. props: {props.aaa}</div>;
};

export default Container;

그냥 나눠서 쓸 수 있다

한 파일에 있는 컨테이너와 프리젠터를 나눔


함수형태로 넣음

const Container = () => {
  return (
    <>
      <div>컨테이너 입니다.</div>
      {Presenter({ aaa: "철수" })}
    </>
  );
};

function withAuth(Component) {
  return function 이름은상관없음(props) {
    return Component;
  };
}

const Presenter = (props) => {
  return <div>프리젠터 입니다. props: {props.aaa}</div>;
};

export default Container;

컨테이너 프리젠터 사이에 withAuth를 만들고, 그 안에 이름은 상관없음이라는 함수를 만듬 (그냥 만들어서 작동안함)

const Container = () => {
  return (
    <>
      <div>컨테이너 입니다.</div>
      {Presenter({ aaa: "철수" })}
    </>
  );
};

function withAuth(Component) {
  return function 이름은상관없음(props) {
    return Component;
  };
}

const Presenter = (props) => {
  return <div>프리젠터 입니다. props: {props.aaa}</div>;
};

export default Container;

//  withAuth(Component)     ==>   이름은상관없음
// (withAuth(Component))()  ==>  (이름은상관없음)()  ==>  Component

withAuth(component)를 실행, 그럼 이름은상관없음 함수가 나오게됨 (아까의 bbb)
만약 이름을 상관없음을 실행시키고싶다면
withAuth(component)()
실행은 console.log가 아니고 return Component인것, 결과는 Component이다.
만약 withAuth(Presenter면)() 역시 return Presenter를 해야함 결과적으로 Presenter가 나옴

const Container = () => {
  return (
    <>
      <div>컨테이너 입니다.</div>
      {withAuth(Presenter)({ aaa: "철수" })}
    </>
  );
};

아까 괄호로 만들었던것처럼 컨테이너 안에도 같은 방식으로 {withAuth(Presenter)({ aaa: "철수" })}
를 넣을 수 있다. 결과는 당연히 Presenter

여기서는 철수를 넣었으니
이름은상관없음 함수 안에 인자(argument)로 철수를 넣은것

그러면 프리젠터에서의 aaa는 철수가 됨

const Container = () => {
  return (
    <>
      <div>컨테이너 입니다.</div>
      {withAuth(Presenter)({ aaa: "철수" })}
    </>
  );
};

function withAuth(Component) {
  return function 이름은상관없음(props) {
    return <Component {...props} />;
  };
}

const Presenter = (props) => {
  return <div>프리젠터 입니다. props: {props.aaa}</div>;
};

export default Container;

//
//                      function 이름은상관없음(props){
//                          return <Presenter {...props} />
//                      }
//
//
// (withAuth(Presenter))({aaa: "철수"})  ==>  (이름은상관없음)({aaa: "철수"})
//
//                                           ==>  <Presenter {...props} />


withAuth의 return 부분에<Component {...props} />;로 스프레드로 뿌림

컨테이너에서 프리젠터로 props를 넘겨주고있었는데
지금은 컨테이너에서 withAuth로 갔다가 프리젠터로 가게됨
근데 그 와중에 넘겨주는 props(철수)는 변함이 없음
한번에 withAuth를 거치지 않고 가는 길은 없기 때문에 컨테이너에서 props, withAuth에서 props로 넘겨줘야함.
이걸로 한번 더 건너가야하는 이유가 생김
그래서 스프레드 식으로 그냥 받은 것을 그래도 넘겨주는 것


이걸 컨테이너/프리젠터로 나누면 이렇게된다
물론 withAuth도 따로 빼도 된다.

export default로 보냈기 때문에
컨테이너에서 AAA라는 이름으로 받아왔다. 그래서

{AAA({ aaa: "철수" })}

라고 쓸 수 있게된것.

withAuth에서는 이제 권한체크, 인증, 다른화면으로 넘기기 등등의 로직이 사용가능하다 .
그럼 그런 로직을 이름은상관없음 함수에다가 구현하면된다.

여기서 이름은상관없음을 화살표로 바꿔보자
연습으로 아까 하다 말은 aaa 안에 있는 bbb를 바꿔보자

function (Component){
	return function bbb(props){
    //로그인 검증 로직
    return "결과물"
  }
}

const aaa =(Component) => {
  return (props) => {
    //로그인 검증 로직
    return "결과물"
  }
}

근데 만약에 리턴 부분이 하나면 짧게 쓸 수 있다.
aaa를 withAuth로 써보자

const withAuth =(Component) => (props) {

    return <Component {...props}/>
}

이러면 아까 봤던 () => () 괄호 두개의 부분이 보이게된다.

import { useContext } from "react";
import { useRouter } from 'next/router'
import { GlobalContext } from "../../../../pages/_app";


export const withAuth = (Component) => (props) => {
    const router = useRouter()
    const{myAccessToken} = useContext(GlobalContext);

    if(!myAccessToken){
        alert("로그인 한 사람만 입장 가능합니다!! 로그인을 먼저 해주세요");
        router.push("22/01-login");
}
    
    return <Component {...props} />;
}

아하..?
이해가 안될수도 있지만 앞에는 컴포넌트, 뒤에는 props라고 알고 서서히 이해해가는 것이 좋다.
++ 또한 맨 밑에 export default이후 뭔가로 쌓여져 있다면
이런 원리로 사용됐음을 알고 있는 것이 좋다.

물론 사용자정의 hook으로 만들수도있지만 이렇게 하면 class형 컴포넌트는 쓸 수 없다.(이건 나중에 또함)

withAuth를 적용할때는 일단 page컴포넌트에만 두는 것이 좋다.

HOF는 또 뭐죠?

여태 HOC,함수형 컴포넌트는 return 부분에 JSX가 들어갔다.

만약 JSX가 들어가지 않거나 return 부분이 없다면
HOF인거다. (High Order Function )

export default function HofPage(){

    const onClickChild = (index) => (event) => {
    console.log(event.target.id); 
    }

// function onClickChild(event){
//  console.log(event.target.id); 
// }
    return(

        <>
        <h1>HOF 연습 페이지입니다!!</h1>
        <div>{["철수", "영희", "훈이"].map((el, index ) => (
            <div key={el} onClick={onClickChild(index)}>
                {el}
            </div>
        ))}
        </div>
        </>
    )
}
// onClickChild(event)로 함수를 실행해야함!!
// onClickChild(index)(event) 
// onClick={onClickChild(index)} 하게되면 알아서 (event)를 받아옴

Local storage

보안이 좋지 않지만 임시적으로 토큰을 브라우저의 저장공간에 받아두고 로그인과정에서 받았던 토큰을 새로고침해도 리프레쉬 토큰을 받아올 수 있도록 해본다.

이런 저장소는 Local storage, session storage, cookie가 있음 오늘은 local storage만 해본다

이렇게 볼 수 있는데 기존에 state에 저장됐던 것들을 이곳에 저장하는것이다.

  async function onClickLogin() {
    const result = await loginUser({
      variables: {
        email: myEmail,
        password: myPassword,
      },
    });
    localStorage.setItem(
      "accessToken",
      result.data?.loginUser.accessToken || ""
      )
    setAccessToken(result.data?.loginUser.accessToken); // 여기서 setAccessTocken 필요! (글로벌 스테이트에)
    // 로그인 성공된 페이지로 이동하기
    router.push('/22-02-login-success')
  }
  

localStorage.setItem(
"accessToken",
result.data?.loginUser.accessToken || ""
)

이 한줄을 코드에 넣으면 accTocken 을 로컬 스토리지에 저장한다고한다.
app.tsx에서

 const accessToken = localStorage.getItem("accessToken") || "";
  if(accessToken) setMyAccessToken(accessToken)

MyApp 부분에 이렇게 넣어줘야한다.

근데 왜 안되느냐?

  1. 브라우저에서 요청하면 front-end에서 한번 그려본다
  2. 브라우저에서 다시 한번 그리는데
  3. 이 과정에서 그리면서 비교과정(diffing)
  4. 최종적으로는 hydration을 통해서 다시 그린다

1번에서 문제가 생기게 된다. 로컬스토리지 자체가 프런트엔드가 아닌 브라우저에 있는 것이라서 이해할 수 없게된다.

(이건 react가 아닌 Next에 존재하는 문제다)

그럼 해결은 서버에서는 실행하지 말고 브라우저에서 실행시킬 수 있다.

typeof window !=== "undefined"
브라우저(window)가 정의되지 않음이 아님 -> 브라우저인지

if(process.browser){
브라우저인지

그런데 우리는 useEffect를 사용한다.

  useEffect(() =>  {
    const accessToken = localStorage.getItem("myAccessToken") || "";
    if(accessToken) setMyAccessToken(accessToken)

  }, [])

이후 로그인 변동에 대한 페이지 라우터를 맞게 설정하면
된다 된다..
이후 적용은 컴포넌트에 적용보다는 pages에다가 적용하는게 좋다

profile
개발자 새싹🌱 The only constant is change.

0개의 댓글