[Challenge] 로그인 구현 lv.3

KoEunseo·2023년 3월 17일
0

challenge

목록 보기
5/9

키워드

react
react-router-dom
typescript
Cookie
Local Storage
Session Storage
auth

로그인과 관련된 개념들

Session 세션이란 ?

사용자의 로그인 이후 로그아웃 혹은 로그인 만료까지의 시간
세션방식 로그인
사용자 로그인이 유효한 시간동안 서버에 세션아이디를 기록해두고 인증에 사용한다.

간단히 말해 데이터 조각이라 할 수 있다.
Http요청 시 서버에서 보내준다. sid(세션아이디)와 sid가 유효한지에 대해서만 서버가 보관하고 있다가 이후 요청이 들어오면 sid만 요청과 함께 전송한다.
반면 토큰(JWT)에는 유저정보 등 모든 데이터가 들어있다. 시크릿키를 가지고 디코딩해 유효한지 판단함. !쿠키는 프론트에서 직접 넣어주는 값이 아님!
쿠키 관련 정책은 백에서 한다.

  • SameSite : None, Lax(get 등 허용), Strict
  • HttpOnly : 자바스크립트로 접근할 수 없도록 한다.(프론트에서 건들 수 없다.) http요청으로만 접근이 가능하다.
  • Secure : https로만 요청하도록 한다.

Cors

출처가 다른 정보에 대해 어떻게 처리할 것인가에 대한 정책.
어떤 origin을 허용할건지 백에서 추가한다.
프론트에서 할 수 있는 건 로컬환경에서 cors 검증을 해제하는 것인데 임시방편일 뿐이다.

아래 코드를 터미널에 입력해서 CORS 검증을 해제한 크롬창을 띄울 수 있다.

open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security

CORS 정책(백엔드)

app.enableCors({
  origin: url,
  methods: ['GET'],
  credentials: true
})
app.use(session({
  secret: 'cors 정책 예제',
  resave: false,
  saveUninitialized: true,
  cookie: {
    maxAge: 24*6*60*10000,
    sameSite: 'lax',
    httpOnly: true
  },
}))

세션기반 로그인 호출하기

login type

type LoginRes = 'success' | 'fail';
export interface LosinReq {
  username: string;
  password: string;
}

login api

export const login = async (args: LoginReq): Promise<LoginRes> => {
  const res = await fetch(`${BASE_URL}/auth/login`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      credentials: 'include' //기존 방법과 비교했을때 withCredential 부분이 추가되었다.
    },
    body: JSON.stringify(args)
  })

  return res.ok ? 'success' : 'fail'
}

login page

const loginSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  const formData = new FormData(e.currentTarget)
  
  const loginRes = await login({
    username: formData.get('username') as string,
    password: formData.get('password') as string
  })
  
  if (loginRes === 'fail') {
    return;    
  }
  router('/home') //useNavigate를 사용하여 로그인 성공시 home으로 이동한다.
}

로그인 상태기반 페이지 분기설정

어떤 페이지는 로그인한 유저에게만 보여야 하고, 어떤 페이지는 로그인되어있다면 절대 볼 수 없다. 예를 들면 마이페이지 같은 경우는 로그인을 해야만이 볼 수 있고, 로그인 페이지는 로그인되어있지 않아야만 볼 수 있다.

이전에 팀 프로젝트를 하면서 로그인을 담당했던 분이 처음부터 마지막까지 고생하셨다. 지금은 다른 수업을 듣고 있어서 리팩토링에 참여는 못하고 계시지만... 초기에 리팩토링에 참여를 해주셨는데 이때 로그인 상태를 기반으로 접근 가능한 페이지인지 아닌지를 구현하는데 전체적인 페이지에 한땀한땀 하드코딩을 하셨었다... 😭 그래서 더 이번 프리온보딩 과정이 인상깊게 남았다. 여튼간 과정을 분석해보자면

01. GET /profile : 유저 정보 가져오기

  1. 세션에 저장되어있는 유저 정보를 반환한다.
  2. credentials: 'include'옵션이 활성화되어있다면 자동으로 로그인여부를 검증하게 된다.
    이때 유저정보를 받는 데 성공했는지 여부만 확인하면 된다.

02. 라우터를 이용해 로그인이 필수인 페이지를 설정한다.

03. 이때 로그인 '상태'를 갖고있는 프레임 컴포넌트를 먼저 만들어놓아야한다.

이 프레임은 페이지 이동시마다 로그인 여부를 확인하는 함수를 실행할 것이다.
멘토님께선 GeneralLayout이라고 만들어두셨는데 내가 영어가 짧아서 의미가 잘 와닫지 않는 것 같다.
그래서 AuthCheckedFrame.tsx라고 하겠다.
이 프레임의 구조에 대해 대략적으로 설명하자면

const AuthCheckedFrame: React.FC<children: {children: React.ReactNode}> = ({children}) => {
  //useState를 이용해 유저프로필을 세팅
  const [userProfile, setUserProfile] = useState<User | null>(null);
  const fetchUserProfile = useCallback(async () => {
    //페이지 이동마다 로그인 여부를 확인할 함수.
    //useCallback을 사용해 리렌더링시 함수가 재생성되지 않도록 한다.
    const userProfileRes = await getCurUserInfoFn() //유저정보 가져오는 함수 실행
    if (userProfileRes === null) {
      return; //여기선 로그인페이지로 리다이렉트하면 좋다.
    }
    setUserProfile(userProfileRes)
  },[])
  
  useEffect(() => {
    //children(이동된 페이지)의 값이 변경될때마다(렌더링될때마다) 실행된다.
    fetchUserProfile()
  }, [children])
  
  if(!userProfile) return (로딩중);
  
  return (
    <div>
      <sidebar />
      <div>{children}</div> //여기에 로그인이 필요한 페이지들이 들어간다.
    </div>
  )
}

02. 라우터로 다시 돌아가자.

인증이 필요한 페이지만 위 프레임으로 감싸서 전달하면 된다.

type

interface RouterElement {
  id: number
  path: string
  label: string
  element: React.ReactNode
  withAuth?: boolean
}

router list

위 타입에 기반해 라우터 리스트를 작성한다.

router

reac-router-dom에서 제공하는 createBrowserRouter를 사용해 라우팅 해준다.

export const routers: RemixRouter = createBrowserRouter(
  routerData.map((router) => {
    if (router.withAuth) { //로그인 필수 페이지는 여기 분기로
      return {
        path: router.path,
        element: <AuthCheckedFrame>{ router.element }</AuthCheckedFrame>
      }
    } else { //로그인 여부 상관 없다면 여기 분기로 빠진다.
      return {
        path: router.path,
        element: router.element
      }
    }
  })
)

RemixRouter는 무엇인가 보니 import { Router as RemixRouter } from '@remix-run/router/dist/router' 경로가 이렇게 되어있다. remix-run이라는 패키지가 있는데, react-router-dom에서의 Router와 구별을 하기 위함이 아닐까 싶다.

라우터 리스트를 사용해 인증이 필요한 페이지만 네비게이션에 표시할 수도 있다.

04. 이제 드디어 Login페이지로 간다.

  1. 이미 로그인된 유저인지 확인한다.
  const isLoggedIn = async (): Promise<boolean> => {
    const userProfileRes = await getCurUserInfoFn()
    return userProfileRes !== null
  }
  1. 이미 로그인된 상태라면 home으로 라우팅한다.(혹은 의도한 다른 페이지)
  2. 아니라면 위에서 구현해놓은 loginSubmit 함수를 사용한다.
const loginSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  const formData = new FormData(e.currentTarget)
  // 로그인했는지 확인
  const checkUserLogged: boolean = await isLoggedIn();
  if (checkUserLogged) router('/home');
  // 로그인
  const loginRes = await login({생략})
  .. 생략 ..
}

최대한 로그인 관련된 것만 정리하려고 했는데, 보다보니 useRouter 훅이라던가 remix router라던가 reduce 사용법이라던가... 꿀팁이 흐른다. 특히 reduce는 잘 안썼는데 너무 유용한 것 같다. 꼭 정리해야겠다.

0개의 댓글