2023. 4. 12 ~ 13

Junghan Lee·2023년 4월 13일
0

TIL Diary

목록 보기
35/52

index

로그인(역사, JWT 로그인)
단방향 암호화(해싱), 양방향 암호화
토큰을 같이 보내주는 방법
JWT 토큰과 조작 불가능성 이해(회원가입, 로그인 진행, JWT이상한점)
로그인과 Recoil연결
새로고침 이후 재접속 과정의 이해
브라우저에 accessToken 저장하기
(브라우저 저장소: localStorage)
Next.js의 렌더링 방식
권한분기(스택, 큐, 스코프 체인, 클로저, 호이스팅)
함수를 리턴하는 함수 HOF
HOC(Higher Order Component)&withAuth


Intro

로그인, 검색해봐도 방법이 너무 많다
비밀번호를 그대로 DB에 저장해? 암호화?

브라우저 저장소 왜이렇게 많아?

state(변수) : 새로고침 시 초기화
LocalStorage : 껐다 켜도 남아있음
Session Storage : 브라우저 껐다 켜면 초기화

쿠키 : 껐다 켜도 남아있으나 만료 시간을 줄 수 있음(만료시 삭제)
보안기능(httpOnly, Secure) 강화, 자동으로 서버와 연동 가능

JWT토큰은 조작이 불가능하다. 하지만 정보 조회는 쉬워서 중요한 정보는 저장하지 말아야 한다.

로그인의 역사

쿠키, 세션 로그인

첫 번째 로그인)
브라우저에서 특정 email, pwd가지고 로그인을 하게 되면 백엔드로 loginAPI요청이 날아가고 백엔드에서는 해당 유저가 있는지 데이터베이스에 확인 후 있으면 session에 저장해 두게 되고 이후에 특정한 id를 부여해 브라우저로 보내준다.

이렇게 보내진 아이디는 해당 유저가 뭔가를 요청할 때 본인이 누군지 식별할 수 있도록 id를 함께 넣어서 보내준다.

이렇게 유저의 정보(id)를 백엔드 서버로 받다 보니 한번에 여러 명의 정보를 받기엔 한계가 있었고 이를 보완하기 위해 백엔드 컴퓨터를 scale-up(컴퓨터의 성능을 올려줌) 해주었다.

두 번째 로그인)
백엔드 컴퓨터의 성능을 올려주어도 더 많은 유저의 접속이 동시에 일어나면 여전히 서버의 부하를 초래했고 그래서 백엔드 컴퓨터를 복사하는 방법이 등장했다. 이 방법은 유저의 정보가 담기는 백엔드 컴퓨터를 복사해 여러 대의 컴퓨터로 백엔드 서버의 부하를 분산해주었다.

그러나 이 방법 또한 문제가 있었는데, 백엔드 컴퓨터를 복사할 때는 세션까지 scale out(같은 성능의 컴퓨터를 추가) 이 안되기 때문에 기존의 로그인 정보를 가지고 있던 백엔드 컴퓨터가 아니면 로그인 정보가 없다는 문제였다. 또 백엔드 컴퓨터를 복사해도 데이터베이스는 하나이기 때문에 결국 DB로 부하가 몰리는 병목 현상이 일어났고 이 문제를 해결하기 위해 세 번째 로그인 방법이 사용되었다.

세 번째 로그인)
이 방법은 위의 세션을 scale-out해오지 못하는 문제점을 보완한 방법이자 데이터베이스에 부하가 몰리는 병목 현상을 해소한 방법으로 현재 널리 쓰이는 방법이다. 세션을 복사하지 못하자 로그인 정보를 데이터베이스에 저장하는 것인데 결국에 이 역시 데이터베이스의 부하를 초래하게 된다. 보완을 위해 데이터베이스를 복사할 수도 있다는 생각이 들 수 있지만 이는 비용 문제가 발생하기 때문에 비효율적이다.

따라서 위의 문제점은 데이터를 쪼개면서 해결하게 된다. 데이터를 쪼개는 데는 2가지의 방법이 있는데 수직으로 쪼개는 " 수직 파티셔닝 "과 수평으로 쪼개는 " 수평 파티셔닝(샤딩) " 이 그 방법이다.

그러나 여기에도 문제점은 있다. 데이터베이스는 컴퓨터를 껐다 켜도 날아가지 않기 때문에 데이터가 디스크에 저장되어 안전하지만 느리다. 이렇게 디스크에 저장된 데이터를 추출해 오는 것을 DB를 긁는다(scrapping)라고 표현한다. 이를 해결하기 위한 방법으로는 Redis라는 메모리에 저장하는 임시 데이터베이스에 저장해 두는 것인데 redis는 메모리에 저장하기 때문에 디스크보다 빠르다.

이렇게 저장된 특정 ID(토큰)을 다시 브라우저로 돌려주게 되고 돌려받은 토큰은 브라우저 저장공간에 저장해 두고 어떤 행동을 할 때 토큰을 같이 보내주어 사용자가 누구인지 식별한다.
cf) stateless => 백엔드 컴퓨터에 상태를 가지고 있지 않음
<-> stateful : scale-out하기 힘들다.
병목 현상 (bottleneck) : 느려짐

JWT 로그인

네 번째 로그인)
로그인 정보를 굳이 서버나 데이터베이스에 저장해야 할까? 는 의문에서 JWT 토큰이 탄생했다. (JSON Web Token)

JWT 토큰은 유저 정보를 담은 객체(const qqq = { id: a@a.com, isLogin: true})를 문자열로 만들어 암호화한 후 암호화된 키(accessToken)을 브라우저에 준다. 받아온 암호화된 키는 브라우저 저장소에 저장해 두었다가 유저의 정보가 필요한 API를 사용할 때 보내주면 해당하는 키를 백엔드에서 복호화해서 사용자를 식별한 후 접근이 가능하게 한다. JWT 토큰에는 해당 토큰이 발급 받아온 서버에서 정상적으로 발급을 받았다는 증명을 하는 시그니쳐를 가지고 있다. 따라서 사용자의 정보를 DB를 거치지 않고도 식별할 수 있게 되었다.

복호화 : 암호화한 것을 다시 암호로 돌리는 것

단방향 암호화(해싱), 양방향 암호화


로그인을 하고 로그인 정보를 조회했을 떄 브라우저에 비밀번호를 fetch할 수 없어야 한다. (비밀번호를 알아내는 것이 불가능해야 함) DB의 비밀번호를 알아낼 수 있게 되면, 해킹으로 인해 민감한 정보를 노출할 위험이 커진다. 따라서 비밀번호나 계좌번호같은 민감정보는 백엔드에 저장할 때 그대로 저장하지 않는다. 그렇다면 어떻게 저장할까?

양방향 암호화
양방향 암호화는 JWT같이 복호화가 되는 암호화로 암호화, 복호화 모두 가능한 것이다.

반면 단방향 암호화(해싱) 은 암호화는 가능하나 복호화는 되지 않는 것을 의미한다. 예) 275719 -암호화-> 779 // 앞부터 2개씩 끊어 10으로 나눈 나머지를 적어놓은 것이다. 원래 정보가 뭔지 모르게 만드는 것으로 이는 '다대일' 이라고 하며 이는 레인보우 테이블로 무작정 다 대입해 복호화하는 경우(무차별 대입 공격)도 있다. 이 부분을 보완하기 위해 조금 더 어려운 알고리즘을 추가하기도 한다. 따라서 민감한 정보를 저장할 땐 해킹을 당해도 알아볼 수 없도록 단방향 암호화를 사용해 저장하게 된다.(비밀번호 + 임의의 문자열; '솔트' 라고 부르며 이 상태로 hash를 한다. 또 여기에 salt 추가해 또 hash(무수히 많이 반복; 보통 2의 10제곱;1024번) -> Bcrypt Hash

tip)
authentication(인증, 로그인해 토큰을 받아오는 과정, login), authorization(인가, 리소스에 접근할 수 있도록 토큰을 확인하는 과정, fetchUser)

토큰을 같이 보내주는 방법

http header에 토큰을 넣어 보내면 된다.

위처럼 인가를 보내주면 된다. 위에서 사용되는 Bearer는 관례상 사용하는 것으로 필수 사항은 아니다. basic으로 받고 있다면 basic으로 보내면 된다.

JWT 토큰과 조작 불가능성 이해

회원가입과 로그인 모두 Docs를 보고 진행해야 함


Docs를 보면 accessToken을 받아와야 하는데 받아온 토큰 안에는 유저의 로그인 기록이 저장되어 있으므로 유저의 정보를 확인해야 하는 API를 사용할 때, accessToken을 첨부해 보내면 백엔드에서 유저 정보를 확인 후 해당하는 API를 사용할 수 있도록 해준다.

JWT의 이상한 점 이 있는데 https://jwt.io/ 를 참고하면 Encoded, decoded 부분을 볼 수 있다. 여기서 Encoded 부분에 accessToken을 넣으면..

받아온 토큰을 넣어 보면 decoded 부분에 토큰에 대한 정보가 모두 보인다. 이처럼 누군가 토큰을 탈취해 해당 사이트에 넣어 보면 토큰의 정보를 알 수 있다. 즉 jWT토큰은 암호환은 했으나 누구든 열어볼 수는 있다는 것이다. 따라서 JWT토큰에 중요한 데이터를 저장하면 될까?

Encoded(암호화), Decoded(복호화; 암호를 푸는 것)

tip) JWT토큰의 구성(header: 토큰의 타입과 암호화시 사용한 알고리즘 정보, payload:토큰 발행정보(누구인지, 언제 발행되었는가? 언제 만료되나?), signature:비밀번호)

그럼 jwt 사용하면 안돼?
누구든 복호화가 가능하기 때문에 보안을 위해 토큰의 만료 시간을 짧게 주었으나 복호화된 정보에는 토큰의 만료 시간이 명시되어 있었다. 즉 조작할 수도 있다는 것이다.
그러나 이런 조작을 미연에 방지하기 위해 JWT는 signature(토큰의 비밀번호)를 사용하기 떄문에 토큰의 내용을 조작하기 위해서는 토큰의 비밀번호를 알아야 한다. 해당 비밀번호는 백엔드에서 생성하며 알 수 없다.

로그인 & Recoil 연결

로그인 후 로그인 성공 페이지까지 구현하기 위해 로그인 폴더와 로그인 성공 폴더 두 개가 필요하다.

// login 폴더의 index.tsx 화면 그려주기
import {useMutation,gql} from "@apollo/client"
import {ChangeEvent} from "react"

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 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
		}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의 경우 타입 추론이 불가능하므로 앞에는 받아올 타입을 뒤에는 보내줄 타입을 직접 적어준다. 입력하는 방법은 IMutation에서 로그인 유저의 타입을 Pick한다.

tip) pick과 같이 쓰이는 타입 omit, patial
셋 모두 utility 타입으로 omit은 특정 데이터를 빼고 모두 데려와 주는 역할, patial은 모두 가지고 오지만 모두 ?를 붙여 가지고 오는 것

catch 부분에서 오류가 뜨는 경우) 타입스크립트의 버전에 따른 차이

...코드
catch(error){
	if(error instanceof Error)Modal.error({content : error.message})
	}


로그인 결과 : accessToken을 받는다.
loginsuccess에서 로그인한 유저의 정보를 가져옴 -> 유저의 정보를 가져오는 api 사용을 위해 http header 부분에 accessToken을 첨부해 요청해야 함

accessToken global state(Recoil)에 저장하기

// 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>
	)
}

global state에 저장된 accessToken header 연동하기

//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>
	)
}

uploadLink에 토큰을 추가: 모든 컴포넌트에서 로그인 관련 토큰을 추가해 보내주도록 만든 것으로 로그인을 하지 않은 사람은 토큰 자리에 토큰이 없고 로그인을 한 사람은 토큰을 가지고 있다. 그렇다면 저 토큰 자리에 토큰은 뭐로 채워주나? global state, 즉 Recoil에 accessToken을 저장해 두고 사용하고 싶은 컴포넌트 전체를 감싸준 후 필요한 곳에서 꺼내 사용

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

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

	return (
		<RecoilRoot>
        <ApolloSetting>
          <Global styles={globalStyles} />
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </ApolloSetting>
      </RecoilRoot>
	)
}
// 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] =useRecoilState(accessTokenState)

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

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

새로고침 후 재접속 과정 이해하기

accessToken을 변수에 넣어 사용했지만 새로고침을 하면 로그인 정보가 날아가는 이유는 이전에 그려주었던 state변수들이 날아갔기 때문이다. 그럼 accessToken은 어디에 저장해야 할까? "브라우저"에 저장해야 하며 이 공간을 브라우저 저장소라고 한다.
(실무에서 사용하는 방식은 아님, refreshToken이 실무에서 사용하는 방법)

브라우저에 accessToken 저장하기

// 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>
	)
}

로그인 후 받아온 토큰을 브라우저에 저장해둘 수 있도록 localStorage.setItem(key, value) 을 이용해 브라우저에 accessToken 저장! 그러나 실제로 accessToken이 저장되는 곳은 recoilState의 accessToken이라는 변수에 저장되어 있다. 새로고침을 하면 app 전체가 가장 먼저 새로 그려지기 때문에 여전히 accessToken 변수가 새로 그려지게 됨. 따라서 login Page에서 브라우저에 저장해둔 accessToken을 getItem을 통해 가져와 setAccessToken에 다시 넣어 브라우저에 저장된 토큰으로 바꿔주어야 함.

그러나 이 방식에서 localStorage is not defined라는 에러 발생... 이유는?
next.js의 렌더링 방식 때문

tip) 브라우저 저장소의 종류와 특징
1) 쿠키 : 영구 저장 가능, 만료시간 지정 가능
2) localStorage: 영구 저장 가능( 임시로 사용하는 저장소)
3) sessionStorage: 브라우저 종료시 사라짐
이외에도 다양함

검사 -> application -> storage ; 확인 가능

localStorage 사용 방법
key, value 넣어주기!
저장 : localstorage.setItem("key", "value")
꺼내오기 : localstorage.getItem("key")

Next.js의 렌더링 방식

next.js는 diffing과 hydration 과정을 거쳐 화면 렌더링;
흔히 말하는 서버사이드 렌더링과는 살짝 다르며 프리 렌더링과 더 관련이 있다.


브라우저에서 특정 페이지 요청 시, 프론트엔드 서버에서는 해당 페이지의 html, css, js를 미리 그려본 후(프리 렌더링) 브라우저로 html, css, js 파일을 던져준 후 이를 브라우저에서 렌더링한다. 그리고 브라우저에서 그린 내용과 프론트엔드 서버에서 렌더링한 내용을 얼마나 차이가 나는지 비교하게 되는데 이 과정을 diffing이라고 하며 diffing을 통해 비교한 후 최종적으로 반영해 렌더링하게 되는 과정을 hydration이라고 한다.(비교하고 덮어쓰기)a

localStorage는 브라우저에만 있는데 서버에서 먼저 화면을 그려본다?? 이래서 오류가 생기는 것이었으며 이를 해결하려면 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>
	)
}

권한 분기

로그인 인증 이후에는 이에 따른 권한 분기가 이루어지는데 작게는 로그인을 한 사람과 하지 않은 사람부터 로그인에 등급을 매기면 운영자로 로그인한 사람 판매자로 로그인한 사람 등 다양하게 권한을 분리할 수 있다.

이를 진행하기 위해서는 스택, 큐에 대한 지식이 필요하다.

스택이란?
출입구가 하나인 우물 형태의 데이터 구조

스택은 출입구가 하나이기 때문에 가장 처음에 입력된 함수가 가장 나중에 스택을 빠져나가게 되며 이는 First In Last Out(FILO)라고 한다.

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


First In First Out(FIFO)

스코프 체인

// 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()에 브레이크 포이트를 걸어두고 함수 안으로 하나씩 들어오니 callstack에 aaa 함수가 쌓여있다.

스코프의 글로벌 부분을 보면 바나나가 3으로 잘 들어오고 있다. 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, bbb 생성되어 있다. bbb는 스택의 가장 위에 있기 때문에 가장 먼저 콜스택을 빠져나간다. 스코프 부분을 보면 local은 bbb함수를 의미한다.

bbb함수에서 콘솔로 apple이라는 변수를 찍어보려 하나 bbb 안에는 apple이 없고 자바스크립트는 이를 위해 상위 스코프로 올라간다. 그러나 closure??

closure를 보면 apple: 10이 적혀 있다.

클로저란 bbb 함수 스코프 안에 apple 변수가 없어 aaa라는 상위 함수의 스코프로 올라가 apple이라는 변수를 찾게 된다. aaa 함수는 bbb의 closure가 되며 bbb가 apple을 찾아 올라갈 수 있는 이유는 실행컨텍스트가 외부 환경 요소를 수집하기 때문이다. 즉, 클로저는 상위 함수와 해당함수(bbb)가 선언된 스코프, 즉 상위 함수를 둘러싼 환경이다.

bbb함수에서 aaa함수 스코프로 올라가는 과정에서 스코프 체인이 일어난다.

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

호이스팅?

// 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을 보니 아직 실행되지도 않았는데 qqq = undefined? ; 이는 호이스팅 때문...

var였다면 접근도 가능했겠으나 let, const는 Temporal Dead Zone(TDZ)에 들어가 있기 때문에 접근은 불가하다.

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

함수를 리턴하는 함수 : HOF

HOC를 사용하기 위한 함수를 리턴하는 함수

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

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

콘솔에는 저는 aaa에요만 출력이 되고 반환값으로 bbb 함수가 있다.
콘솔에 저는 bbb에요를 출력하려면? bbb함수를 호출해주면 된다.

aaa()()로 적어주면 됨.

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에 넣은 인자값

각 변수를 함수의 파라미터로 넘기게 되면 실행시 넘겨준 인자값으로 콘솔 출력..
bbb에 apple이 없음에도 받아오는 이유 : 클로져

// 화살표 함수로 변경
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)

High Order Component (HOC)

이는 클로저부터 확장된 개념으로
다른 컴포넌트보다 먼저 실행되는 상위의 컴포넌트라고 생각하면 된다.

aaa컴포넌트의 HOC(bbb)({qqq:"철수"})를 보면 함수를 리턴하는 함수의 호출방법과 비슷하다.

bbb부분에는 apple이 들어왔었고, {qqq:"철수"} 부분에는 banana가 들어왔었다.

HOC함수의 매개변수 부분에 component가 들어와 있는데 component파라미터에 들어올 컴포넌트는 bbb컴포넌트이다.

HOC함수에서 리턴되고 있는 함수를 보면
리턴 함수의 매개변수로 props가 들어가고 있고 해당 props에는 함수 호출 시 넣어준 인자값인 {qqq:"철수"}가 들어오게 된다. 밑으로 내려와 익명함수의 리턴값을 보면 내부에 컴포넌트가 없음에도 불구하고 컴포넌트를 리턴하는데 이는 클로저로 외부의 컴포넌트를 가지고 올 수 있기 때문이다.

withAuth만들어 보기

권한을 체크하는 Hoc를 직접 만들어 보면..

// 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} />
}

eslint 문제 발생 시) "react/display-name":"off"

component&props 부분의 에러는 generic을 통해 해결

권한체크 Hoc을 만들었다면 적용하고 싶은 페이지에 적용하면 됨!

// loginSuccessPage -> withAuth 적용하기 

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

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

export default withAuth(LoginSuccessPage)

이러면 컴포넌트가 실행되기 전에 권한 체크 컴포넌트가 먼저 실행된다!

profile
Strive for greatness

0개의 댓글