
로그인은 크게 인증과 인가로 나누어 볼 수 있음
[인증] = API 요청을 통해 사용자임을 인증 받는 단계, 이 단계를 통해 백엔드에서 고유한 id값을 응답으로 받게 됨
[인가] = 매번 API 요청 할 때마다, 받아놓은 id값을 함께 보내어 백엔드에서 지금 요청을 하는 자가 누구인지를 식별 가능하게 함 (id값이 일치하다면 인가 진행 됨)

브라우저에서 특정 email, password로 로그인을 하게 되면 백엔드로 login API 요청이 날라가고, 백엔드에서 해당 유저가 있는지 DB에서 확인 후 있다면 유저의 정보를 session에 따로 저장하게 됨
이후 해당 유저에 특정 id값을 부여한 다음 이를 응답과 함께 보내줌
*이후 프론트에서 유저가 API 요청을 할 때마다 이 부여된 id를 함께 보냄으로써 백엔드에서 누가 요청하는 것인지를 식별할 수 있게 해 줌
[BUT] => 유저 정보를 백엔드 서버로 받다 보니 한 번에 여러명의 정보를 받는 데에는 한계가 있음
*이 점을 해결하기 위해 백엔드 컴퓨터의 성능을 scale-up 하게 됨

[BUT]
-1. 이 경우 로그인 이후 API 요청하는 컴퓨터와 생성된 session이 저장된 컴퓨터가 같아야만 로그인 정보를 가져올 수 있음
ex. A컴퓨터로 처음 로그인 하면 A컴퓨터에 로그인 정보 session이 저장됨, 이후 특정 API를 요청 시 만약 A컴퓨터가 아닌 B컴퓨터로 요청이 들어가지면 B컴퓨터에는 로그인 정보가 담긴 session이 없기에 정보를 가져오지 못함
-2. 백엔드 컴퓨터를 복사해도 결국 DB는 하나이기에, 오히려 DB에 부하가 몰리는 병목현상 발생
위 문제점들을 해결하기 위해 로그인 정보를 session이 아닌 DB에 저장하기 시작
다만 DB의 부하가 발생하는 것은 똑같았고, 이를 해결하기 위해 데이터를 쪼개기 시작하면서 서버 부하문제를 해결
DB 데이터 쪼개기 (파티셔닝)
-수직 쪼개기 (수직 파티셔닝)
-수평 쪼개기 (수평 파티셔닝)
[BUT]
-DB는 데이터들이 disk에 저장되기 때문에 안전하지만 속도가 느림
-즉, 로그인 정보를 DB 데이터가 저장된 disk 에서 추출(scrapping)해 오기 때문에 속도가 느림
앞의 disk의 속도 문제를 해결하기 위해 로그인 정보를 DB가 아닌 Redis 라는 임시 저장소에 저장하는 방식으로 바꾸게 됨
이렇게 백엔드에 로그인 정보(현재 상태정보)를 두지 않고(stateless), 임시저장소에 저장해 두었다가 필요할 때마다 여기서 데이터를 꺼내오면서 속도 문제도 해결하게 됨
로그인 정보를 굳이 서버나 DB에 저장해야 되는가 라는 의문과 함께 새로운 저장방식으로 탄생한 것이 JWT 토큰을 이용한 로그인 방식

응답으로 받은 암호화 키 (accessToken)는 브라우저 저장소에 저장되고, 이후 API 요청 시 해당 accessToken을 백엔드로 보내고, 백엔드는 이를 복호화 해서 객체 안에서 유저 정보를 식별한 후 접근을 허용하게 됨
*즉, 암호화 된 키 (accessToken)이 고유한 id 값이 되는 거라 볼 수 있음!
*이를 통해 로그인 정보를 굳이 DB에서 찾아보지 않고도, 바로바로 식별 가능하게 됨 (JWT 토큰을 복호화 하면 객체 데이터 안에 그대로 담겨 있는 유저 정보를 활용)
로그인 이후, 로그인 정보를 fetch 할 때 브라우저에서 비밀번호 정보는 fetch 할 수 없어야 함 (비밀번호는 알아내는 것이 불가능해야 함)
해커가 DB를 해킹했을 때, 비밀번호 같은 민감한 정보에 그대로 접근이 가능하면 안되기 때문에 백엔드에서는 이런 민감한 정보를 그대로 저장하지 않고, 암호화시켜 저장함
암호화는 가능하나 복호화가 안되는 암호화 방식
만약 하나의 알고리즘으로 복호화가 가능하다면 해당 알고리즘으로 DB의 모든 사용자의 민감한 정보에 해커가 접근할 수 있기 때문에, 복호화 방식의 경우의 수를 늘려서 원래 정보에 접근이 어렵도록 함
*하지만 이 또한 레인보우 테이블 같은 방식으로 24시간 컴퓨터를 돌려서 복호화를 할 수도 있기에 아주 안전한 방식은 아님
해싱(Hashing) 사용
Hashing = 어떤 수학적 연산에 원본 데이터를 매핑시켜 완전히 다른 데이터값으로 변환시키는 것
salt(임의의 문자열)를 추가해서 반복적으로 hashing
ex. [비밀번호+salt]를 hash -> 한 것을 다시 [hash한 것 + salt]를 hash -> 한 것을 또 다시 [또 한것 + salt]를 hash...반복
*이 경우 현재의 컴퓨터 성능으로는 복호화가 어렵기 때문에 안전함

로그인 이후 인가(요청 API 보내기)할 때마다 사용자임을 식별할 수 있는 고유 id (토큰)을 보내주어야 함
이 토큰 정보는 header에 Authorization key에 담아서 보내줄 수 있음
토큰 앞에 사용되는 Bearer 는 관례상 사용하는 것이며 이 부분은 백엔드 개발자와 상의해서 사용해야 함

문제는 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>
)
}
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 가 가능하게 됨이를 해결하기 위한 방안으로 영구적인 저장을 위해 두 가지 방법 사용 가능
1. 브라우저의 local Storage 에 저장
*보안에 취약한 편이라 실무적인 방법은 아님 (임시방편)
2. refreshToken 사용
*실무적인 방법
+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 라는 에러 발생!

브라우저에 주소를 입력하고 들어가면 우선 프론트 서버 (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가 아니라면 현재 브라우저에서 실행중인 것3. useEffect로 실행하기 (자주 쓰이는 방식)
// 3. 프리렌더링 예제 - 프리렌더링 무시 방법
useEffect(() => {
const result = localStorage.getItem("accessToken") ?? "";
setAccessToken(result);
}, []);
// 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>
)
}
로그인 인증 이후에는 이에 따른 권한분기가 이루어 짐
로그인을 한 사람, 안 한사람, 운영자로 로그인 한 사람, 판매자로 로그인 한 사람 등 다양하게 권한을 분리가 가능

하나의 출입구로, 가장 먼저 입력된 함수가 가장 나중에 스택을 빠져나가는 형태
FILO (First In Last Out)

양쪽으로 출입구가 있어 가장 먼저 입력된 함수가 가장 먼저 빠져 나감
FIFO (First In First Out)
// 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 => function 을 return
HOC => JSX Element 를 return
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)
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)}>
onClickpage 함수의 두번째 매개변수 자리에 event를 넣어서 받아와서 사용 가능
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 로그인 체크 컴포넌트가 먼저 실행되고, 로그인 여부를 검사하게 됨