23일차 - storage, 권한분기

류연찬·2023년 5월 7일
0

Codecamp FE07

목록 보기
23/39

새로고침과 재접속

기존에는 accessToken을 변수에 넣어 두고 사용했습니다.

하지만 새로고침을 하게 되면 로그인 정보가 모두 날아가 다시 로그인을 해야 했습니다.

새로고침을 하면 왜 로그인 정보가 없어질까요?

새로고침은 브라우저에서 해당 주소로 다시 접속한 것과 같습니다. 즉, 새로운 html, css, js를 다시 받아온다는 것입니다.

따라서 이전에 만들어줬든 state변수들이 날아가기 때문에 accessToken 또한 날아갔던 것 입니다.

브라우저 저장소

이를 방지하기 위해 accessToken을 브라우저에 저장해야합니다.

브라우저에는 저장할 수 있는 공간이 있으며 이를 브라우저 저장소라고 합니다.

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

  • 쿠키(Cookie) : 영구 저장이 가능하며, 만료시간을 지정할 수 있습니다.
  • localStorage : 영구 저장이 가능합니다.
  • sessionStorage : 브라우저 종료 시 사라집니다.

이번에는 localStorage에 토큰을 저장하여 사용해 보겠습니다.

localStorage 사용 방법

localStorage의 사용 방법은 keyvalue를 넣어주는 방법으로 사용합니다.

  • 저장할 때 : localStorage.setItem("key", "value")
  • 꺼내올 때 : localStorage.getItem("key")

브라우저에 accessToken 저장하기

// login 폴더의 index.tsx

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

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

const LoginPage = () => {
	const [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{
			const result = await loginUser({
				variables: {
					email: email,
					password: password
				}
			})
          
			const accessToken = result.data?.loginUser.accessToken
            
			if(setAccessToken) {
              setAccessToken(accessToken || "")
              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>
	)
}

export default LoginPage

우선 로그인 후 받아온 토큰을 브라우저에 저장해둘 수 있도록 localStorage.setItem() 을 이용해 브라우저에 accessToken을 저장해주세요.

그러나 실제로 accessToken이 저장되는 곳recoil의 recoilState의 accessToken이라는 변수에 저장되어있습니다.

새로고침을 하게되면 app전체가 제일 먼저 새로 그려지기 때문에 여전히 accessToken변수가 새로 그려지게 됩니다.

따라서 우리가 login페이지에서 브라우저에 저장해둔 accessToken을 getItem을 통해 가져와, setAccessToken에 다시 넣어주어 브라우저 저장된 토큰으로 바꿔주셔야합니다.

그런데 그대로 했는데 localstorage is not defined 라는 에러가 뜹니다.

이 부분은 next.js의 렌더링 방식때문에 발생됩니다

Next.js의 렌더링 방식

next.js의 렌더링 방식은 서버사이드 렌더링 방식을 사용하고 있습니다.

서버사이드 렌더링이란 서버에서 페이지를 모두 그려준 후 브라우저로 보내주는 것을 말합니다.

이렇게 보내준 페이지를 브라우저에서 그린내용과 프론트엔드 서버에서 그린 것을 비교하는 diffing과, 비교한 후 그것들을 최종적으로 반영해주는 hydration을 거쳐 렌더링 되게 됩니다.

이제 우리는 오류의 이유를 알고 있습니다.

localStorage는 브라우저에만 있는데, 서버에서 먼저 화면을 그려보기 때문에 발생하는 오류입니다.

이를 해결하기 위해서는 useEffect를 사용해 렌더링 이후에 실행되도록 하겠습니다.

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

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

const ApolloSetting = (props) => {
	const [accessToken, setAccessToken] = useRecoilState(accessTokenState)

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

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

	return (
		<ApolloProvider client={client}>
			{props.children}
		</ApolloProvider>
	)
}

export default ApolloSetting

이렇게 렌더링 이후에 들어오도록 까지 완료하셨다면, 로그인 후 새로고침을해도 토큰이 날아가지 않습니다.

권한분기

로그인 인증이후에는 이에따른 권한 분기가 이루어집니다.

아주 작게는 로그인을 한 사람로그인을 하지 않은 사람을 얘기할 수 있겠죠.

추가적으로 로그인에 등급을 매기면, 운영자로 로그인 한 사람, 판매자로 로그인 한 사람, 거래처 사장님으로 로그인 한 사람 등 다양하게 권한을 분리할 수 있습니다.

스택(Stack)

출입구가 하나인 우물 형태의 데이터 구조입니다.

스택은 출입구가 하나이기때문에 가장 처음에 입력된 함수가 가장 나중에 스택을 빠져나가게 됩니다.

이를 우리는 First In Last Out 이라고 하며, 앞글자를 따 FILO라고도 합니다.

큐(Queue)

양방향 출입이 가능한 파이프 형태의 데이터 구조입니다

큐는 스택과 다르게 출입구가 나뉘어있어 가장 먼저 입력된 함수가 가장 먼저 빠져나갑니다.

이를 우리는 First In First Out이라고 하며, 앞글자를 따 FIFO라고도 합니다.

스코프 체인(Scope Chain)

// closure.html 파일
<!DOCTYPE html>
<html lang="ko">
	<head>
		<title>클로저 실습</title>
		<script>
			function aaa() {
				banana = 3
				console.log(banana)
			}
          
			aaa();
		</script>
	</head>
	<body>
	</body>
</html>

위의 코드가 어떻게 실행되는지 하나하나 보기 위해 개발자 도구의 source를 통해 확인해보도록 하겠습니다.

실행하는 곳에서 브레이크 포인트를 걸어두고 시작합니다.

브레이크 포인트는 코드가 빠르게 실행되다가 멈추고 싶은 코드에서 멈추게 하기 위해 사용합니다.

우측 상단의 버튼 중 1번 버튼은 실행하지 않고 뛰어넘도록 하는 것이고, 2번 버튼은 실행과정을 하나하나 살펴 볼 수 있도록 도와주는 버튼입니다.

aaa() 에 브레이크 포인트를 걸어두고 함수 안으로 하나씩 들어오니 Call Stackaaa 함수가 쌓여있습니다.

이제 콜스택 위의 스코프에 집중해도록 하겠습니다.

스코프의 글로벌 부분을 보니, 바나나가 3으로 잘 들어오고있습니다.

여기서 알 수 있는 것은 local에 바나나가 있었다면 local에 바나나가 들어와 있었겠지만, 없었기 때문에 글로벌까지 찾아 올라간 것 입니다.

우리는 이런 것을 보고 스코프 체인이라고 합니다.

스코프 체인이란 local 스코프에 없으면 global 스코프에는 있나 스코프를 찾아 올라가는 것을 의미합니다.

클로저(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>

코드를 살짝 변형해 위의 코드로 다시 실행과정을 살펴보도록 하겠습니다.

브레이크 포인트를 다시 aaa() 에 두고 새로고침 해주시면 실행됩니다.

그리고 한줄한줄 실행 할 수 있는 버튼을 클릭해 함수안으로 들어와주세요.

그럼 위와 같이 콜스택에 aaabbb가 생성되어있음을 볼 수 있습니다.

bbb는 스택의 가장 위에 있기때문에 가장먼저 콜스택을 빠져나가게됩니다.

이제 다시 위로 올라와 다시 스코프 부분에 집중해볼까요?

여기서의 local은 bbb함수를 의미 합니다.

우리는 bbb 함수에서 콘솔로 apple 이라는 변수를 찍어보려고 합니다. 하지만 bbb 안에는 apple 이란 변수가 없습니다.

따라서 자바스크립트는 apple 이라는 변수를 찾기위해 위의 스코프로 올라간다고 했습니다.

그런데 아까는 보지 못했던 Closure가 생겼습니다.

closure 를 열어보시면 apple: 10 이 적혀있습니다.

클로저란?

bbb 함수 스코프 안에 apple 이라는 변수가 없어 aaa 라는 상위함수의 스코프로 찾아 올라가 apple 이라는 변수를 찾게 됩니다.

aaa 함수는 bbb 의 closure가 되며 bbbapple 을 찾아 올라갈 수 있는 이유는 우리가 이전에 배웠던 실행컨텍스트가 외부 환경 요소를 수집하기 때문입니다.

즉, 클로저라고 함은 상위 함수와, 해당함수(여기서는 bbb 함수)가 선언된 스코프 즉 상위함수를 둘러싼 환경이 되겠습니다.

클로저를 배우는 과정에서 스코프 체인의 과정이 보이는 것 같지 않나요?

맞습니다.bbb 함수에서 aaa 함수 스코프로 올라가는 과정에서 스코프 체인이 일어난다고 보시면 됩니다.

따라서 스코프체인과 클로저는 함께 공부하시는 것이 좋습니다.

💡 스코프 체인과 클로저 간단 정리

  • 클로저(Closure) : 상위 함수 + 상위 함수의 lexical enviroment(상위 함수를 둘러싼 환경)
  • 스코프 체인(Scope Chain) : 바로 쉬 함수 스코프뿐만 아니라 global 스코프까지 찾아 올라가는 과정

호이스팅

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

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

				const qqq = 3
			}
          
			aaa();
		</script>
	</head>
	<body>
	</body>
</html>

위의 코드에 qqq 변수를 추가해서 다시 aaa() 에 브레이크 포인트를 걸어 실행 후 aaa 함수 안으로 들어가보겠습니다.

스코프의 local을 보니 아직 실행되지도 않았는데 qqqundefined 로 들어와 있습니다.

이는 우리가 지난시간에 배운 호이스팅 때문입니다.

var 였다면 접근도 가능했겠지만, letconstTDZ(Temporal Dead Zone)에 들어가 있기 때문에 접근은 불가능합니다.

또한 클릭을 하게되면 스코프와 콜스택의 상태가 변화합니다. 이를 우리는 실행 컨텍스트라고 합니다.

함수를 리턴하는 함수

HOC를 사용하기 위해 함수를 리턴하는 함수르 알아보도록 하겠습니다.

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

  return function bbb() {
    console.log("저는 bbb예요")
  }
}

위의 코드의 결과는 무엇일까요?

아마 콘솔에는 “저는 aaa예요” 만 출력이 되고, 반환값으로 bbb 함수가 있을 것 같습니다.

그럼 콘솔에 “저는 bbb예요” 를 출력하려면 어떻게 해야될까요?

“저는 bbb예요” 를 출력하려면 bbb 함수를 호출을 해주셔야 콘솔에 출력이 됩니다.

bbb 까지도 실행하는 방법은 aaa()() 로 적어주시면 됩니다.

이렇게 실행해주시고 콘솔을 보시면 aaa , bbb 콘솔을 모두 출력된 것을 볼 수 있습니다.

그럼 아래에서 조금 더 자세히 알아보도록 하겠습니다.

function aaa() {
  const apple = 10

  return function bbb() {
    const banana = 5

    console.log(banana)
    console.log(apple)
  }
}

aaa()()

// 실행 결과
// 5
// 6

위의 함수는 파라미터를 사용해 조금 더 간결하게 바꿀 수 있습니다.

// 함수 선언식
function aaa(apple) {
  return function bbb(banana) {
    console.log(banana)
    console.log(apple)
  }
}

aaa(2)(3)

// 실행 결과
// 2 => aaa에 넣은 인자값
// 3 => bbb에 넣은 인자값

이렇게 각 변수를 함수의 파라미터로 넘기게 되면, 실행시 넘겨준 인자값으로 콘솔이 출력되게 됩니다.

bbbapple 이 없음에도 불구하고 받아오는 이유는 클로저 때문입니다.

위의 함수를 화살표 함수로 바꿀수도 있습니다.

// 화살표 함수로 변경
const aaa = (apple) => {
  return (banana) => {
    console.log(apple)
    console.log(banana)
  }
}

aaa(2)(3)

이렇게 화살표함수로 바꿔줄 수 있고, 여기서 중괄호와 리턴사이에 아무것도 없다면 중괄호를 생략해서 적을 수 있습니다.

이렇게 까지하면 최종적으로 아래와 같은 모양이 됩니다.

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

aaa(2)(3)

HOC

HOCHigher Order Component개념은 위의 클로저로부터 확장된 개념입니다.

HOC(Higher Order Component)

HOC는 상위에 있는 컴포넌트로 다른 컴포넌트보다 먼저 실행되는 컴포넌트라고 생각하시면 됩니다.

다른 컴포넌트보다 먼저 실행되게 하려면 어떻게 해야할까요?

여기서 우리가 위에서 배웠던 함수를 리턴하는 함수를 활용하게 됩니다.

Aaa 컴포넌트HOC(Bbb)({qqq:”철수”}) 를 보시면 우리가 배웠던 함수를 리턴하는 함수의 호출방과 비슷합니다.

즉, Bbb 부분에는 apple 이 들어왔었고, {qqq:”철수”} 부분에는 banana가 들어왔었습니다.

그럼 이부분을 참고해 먼저 실행할 컴포넌트를 보겠습니다.

HOC 함수의 매개변수(parameter) 부분에 component가 들어와 있습니다.

component 파라미터에 들어올 컴포넌트는 Bbb 컴포넌트가 되겠습니다.

그리고 HOC함수에서 리턴되고 있는 함수를 보도록 하겠습니다.

리턴 함수의 매개변수로 props가 들어가고 있고, 해당 props 에는 함수 호출시 넣어준 인자값인 {qqq:”철수”} 가 들어오게 됩니다.

밑으로 내려와 익명함수의 리턴값을 보도록 하겠습니다.

내부에 컴포넌트가 없음에도 불구하고 컴포넌트를 리턴하고 있습니다.

이는 클로저로 외부의 컴포넌트를 가지고 올 수 있기때문에 사용이 가능합니다.

0개의 댓글