본문 바로가기

사이드 프로젝트

[React 프로젝트] 로그인 로직 설정.. (UseQuery, axios, typescript)

로그인 로직 작성기.. 

 

일단 간단하게 로직을 작성 해 보자면 

 

1. 로그인 (이 때 응답 값에서 받은 액세스 토큰 로컬스토리지에 설정)

2. 로그인 후 api 요청을 하다가 액세스 토큰이 만료 되어 에러가 남.

2. 쿠키 값에 있는 리프레시 토큰을 통해, 새로운 액세스 토큰 요청

3-1. 만약 리프레시토큰 또한 만료 되었으면 로그인 페이지로 이동

3.2 액세스 토큰을 새로 등록 했을 시엔, 다시 api 요청

 

 

 

로그인 시 공통으로 활용하는 api 설정

const api = axios.create({
  baseURL: `${process.env.REACT_APP_API_DOMAIN}`,
  headers: { Authorization: getAccessToken() },
  withCredentials: true,
})

withCredential을 통해 토큰을 안전하게 전달 받을 수 있음

 

 

 

로그인 api 요청 시 코드

더보기
더보기

 

const postLogin = async reqBody => {
  const res = await api.post(`auth-service/login`, reqBody)
  const accessToken = res.data.data.accessToken // 엑세스 토큰

  return setAccessToken(accessToken) // 로컬 스토리지에 엑세스 토큰 저장
}

 

소셜 로그인, 로그인 시 공통으로 활용하는 setAccessToken 함수

const setAccessToken = (token: string) => {
  const expirationDate = new Date()
  expirationDate.setDate(expirationDate.getDate() + 30)
  localStorage.setItem('accessToken', token)
}

export default setAccessToken

코드 양을 줄이기 위해 공통 함수로 만들어 사용했다. 

 

 

 

그리고 로그인 후에 활용하는 api 함수

useQuery를 통해 사용하는 함수다

// 조회 - 로그인 O
export const getDataTanstackWithToken: QueryFunction<ResponseData, QueryKey> = async ({
  queryKey,
  signal,
}: {
  queryKey: QueryKey
  signal: AbortSignal
}) => {
  checkHasToken() // 토큰 여부 확인

  const [, { searchParam, url }] = queryKey

  const fullUrl = searchParam ? `${url}${searchParam}` : url

  try {
    let response = await fetchWithToken('GET', fullUrl, signal)

    if (response.status === 401) {
      await getNewAccessToken() // Refresh token
      response = await fetchWithToken('GET', fullUrl, signal)
    }

    const data = await handleResponse(response)
    console.log('data' + data)

    return data
  } catch (error: Error | any) {
    const message = error instanceof Error ? error.message : '' // Check if error is an instance of Error
    throw new Error(message)
  }
}

 

 

api 요청 전에 토큰 존재 여부 확인 하는 함수

// 토큰 존재 여부 확인
function checkHasToken() {
  if (!hasToken()) {
    toast.error('다시 로그인해주세요')
    window.location.replace('/')
  }
}

// 토큰 존재 여부 확인
function hasToken() {
  const token = localStorage.getItem('accessToken')
  if (!token || (token && !token.length)) {
    return false
  } else {
    return true
  }
}

 

만약에 로컬스토리지에 액세스 토큰 자체가 없다면 => 사용자는 이전에 로그인을 시도한 적이 없음으로 판단

api를 타기도 전에 로그인 페이지로 이동 할 수 있게 로직을 작성했다.

 

api 요청 함수

// api 요청 - body x
const fetchWithToken = async (method: string, url: string, signal?: AbortSignal): Promise<Response> => {
  return fetch(url, {
    method: method,
    mode: 'cors',
    signal,
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + localStorage.getItem('accessToken'),
    },
  })
}

코드 양을 줄이기 위해 fetch 함수를 공통으로 사용하는 함수로 만들었다. 

 

새 액세스 토큰 요청 함수

export const getNewAccessToken = async () => {
  console.log('refresh!')
  if (window.location.href.includes('local')) {
    toast.error('다시 로그인해주세요')
    window.location.replace('/')
  } else {
    const data = await requestNewAccessToken()
    if (data === NO_REFRESH_TOKEN_CD) {
      toast.error('다시 로그인해주세요')
      window.location.replace('/')
    }
  }
}

// 새 액세스 토큰 요청
export const requestNewAccessToken = async () => {
  try {
    const response = await fetch(`${process.env.REACT_APP_API_DOMAIN}auth-service/token`, {
      method: 'GET',
      headers: {
        Accept: 'application/json',
      },
      credentials: 'include',
    })

    const data = await response.json()

    console.log(data)
    if (!response.ok) {
      const message = data?.message || data?.error
      if (message === NO_REFRESH_TOKEN_MSG) {
        return NO_REFRESH_TOKEN_CD
      }
    }
    let castObj

    if (data) {
      castObj = castRefreshTokenType(data)

      if (castObj) {
        console.log('토큰 변경')
        // console.log('토큰 변경' + castObj?.comPToken.token)
        setAccessToken(castObj?.comppiToken.token)
      }
    }

    return castObj?.comppiToken.token
  } catch (error: Error | any) {
    const message = error instanceof Error ? error.message : '' // Check if error is an instance of Error
    throw new Error(message)
  }
}

만약 401 에러가 떨어지면 => 액세스 토큰 만료로 

새 액세스 토큰을 받기 위해 getNewAccessToken 함수가 실행되는데, 

local에서 실행되는 경우는 서버, 클라이언트가 same domain이 아니기 때문에 쿠키 값을 전달 하지 못한다. 

그래서 바로 로그인 화면으로 리다이렉트를 하게 해두었고 

same domain인 경우에는 액세스 토큰을 새로 받을 수 있도록 api를 요청한다. 

 

요청을 한 이후에 새로운 액세스 토큰을 로컬스토리지에 등록한 후 다시 api 요청을 하면된다. 

만약api요청 이후에 error 가 나거나, 리프레시 토큰이 없다고 뜨면 로그인 화면으로 돌아간다.

 

 

 

같은 로직으로 axios 버전도 만들어 보았다.

export const tokenApi = axios.create({
  baseURL: `${process.env.REACT_APP_API_DOMAIN}`,
  mode: 'cors',
  headers: {
    'Content-Type': 'application/json',
  },
})

// 요청 인터셉터 설정
tokenApi.interceptors.request.use(
  config => {
    const accessToken = getAccessToken2()
    // config 객체를 직접 수정하지 않고 새로운 객체를 생성하여 반환
    const newConfig = {
      ...config,
      headers: {
        ...config.headers,
        Authorization: accessToken ? accessToken : config.headers.Authorization,
      },
    }
    return newConfig
  },
  error => Promise.reject(error),
)

tokenApi.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config

    if (error.response.data.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      try {
        const newToken = await getNewAccessToken() 

        // 원래 요청을 다시 시도
        return tokenApi({
          ...originalRequest,
          headers: {
            ...originalRequest.headers,
            Authorization: `Bearer ${newToken}`,
          },
        })
      } catch (err) {
        return Promise.reject(err)
      }
    }

    return Promise.reject(error)
  },
)