On a couch

[메인 프로젝트] 토큰 검증 커스텀훅 / invalid hook call 에러 본문

코드스테이츠 FE/프로젝트

[메인 프로젝트] 토큰 검증 커스텀훅 / invalid hook call 에러

couch 2022. 9. 28. 18:29

문제 상황

난생 처음 token 로그인 기능을 만들며 여기저기 부딪혔다.

token 유효성 검사 기능은 여기저기에서 다 쓰이다보니까  TokenAuth.js 라는 파일에 전부 만들어두고 필요한 곳에서 import해서 쓰려고 했다.
그래서 TokenAuth.js 라는 파일에 onLoginSuccess, onLogout 등등 함수를 만들고 네비게이션, 글쓰기 버튼 등에서 불러와 봤다.
문제는 onLoginSuceess, onLogout 함수들이 유저 정보를 redux에 저장하기 위해 useDispatch 훅을 사용하는데, 여기서 invalid hook call 에러가 나고 있다는 것이다.

hook을 함수 최상위에서 써야 한다는 것도 알겠고, 문서를 읽으면서 'hook call'의 기준이 const dispatch = useDispatch() 이 선언문이라고 이해했다.
처음 코드 상태로는 onLogout 함수가 handleLogout 안에 들어가면서 '이벤트핸들러 함수 내부에서 호출'하는 경우가 되었다고 판단해, 선언문을 import해온 곳의 함수 컴포넌트 최상단으로 옮겼는데 이 경우 uncaught reference 에러가 난다.


시도한 것들

시도 #1 : const dispatch = useDispatch()를 onLogout 함수 바깥(TokenAuth.js 파일 최상단)에 선언해 봤다.

   => invalid hook call 에러 난다. hook이 함수 밖에 있어서 그런 것 같다.

 

시도  #2 : dispatch를 사용할 곳은 GNB 컴포넌트니까, const dispatch = useDispatch()를 함수를 사용할 GNB 컴포넌트의 최상단에 선언하고, TokenAuth.js 에서는 선언하지 않는다.

   => uncaught reference 에러 난다. eslint가 TokenAuth.js에서는 'dispatch가 정의되지 않았다'고 하고, GNB.js에서는 'dispatch 정의해 두고 안 쓴다'고 난리다.

 

시도  #3 : 이 TokenAuth.js 안에 있는 함수들도 어떤 컴포넌트 형식으로 감싸줘야 하나..? 근데 그러면 export가 안 되지 않나..? 아니면 클래스로 만들어서 메소드 형식으로 export 해야 하나..?

  => 생각만 해 보고 실행은 못 해봤다. 

 

시도 #4 : const dispatch = useDispatch()를 GNB 최상단에 선언하고, onLogout 함수 안의 내용을 긁어다가 GNB의 handleLogout()안에 넣어두고 쓰고 있다. 그런데 이러면 전혀 재활용도 안 되고 변경사항 생길 때마다 '인증 관련 기능 어느어느 컴포넌트에 넣어뒀더라' 하면서 다 찾아다녀야 한다 ㅠㅠㅠㅠ

 

시도 #5 : 'uncaught reference error'라는 에러 이름을 떠올리다 보니 결국 dispatch가 useDispatch의 리턴값에 접근하지 못하는 스코프의 문제다 싶어서 const dispatch = useDispatch()를 GNB 최상단에 선언하고, 여기서 받은 dispatch(의 주소값)를 onLogout 함수의 인자로 전달했더니 잘 작동했다. 호출하는 모든 컴포넌트에서 dispatch를 선언해 전달해야 하는 점, 실행이 연결된 함수에도 계속 전달해야 하는 점 (onAccessTest(dispatch) => onRefresh(dispatch)) 등이 매끄럽지 않아 보이지만 현재로서는 최선이다.

 

앞으로 시도할 해결방법으로 2가지를 생각중인데,

(1) axios에 interceptor 라는 미들웨어 사용법을 공부해 적용하거나

https://velog.io/@lgj9172/JWT-access-token-refresh-token-auto-refresh-axios%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%86%A0%ED%81%B0-%EC%9E%90%EB%8F%99-%EB%B0%9C%EA%B8%89

 

JWT, access token, refresh token, auto refresh, axios를 이용한 토큰 자동 발급 받기

일반적으로 로그인을 하면 서버에서는 클라이언트에 아이디/비밀번호 대신에 request 할 때 사용할 수 있는 토큰을 준다.request 할 때 마다 아이디와 비밀번호를 사용하게 된다면 보안상 문제가 될

velog.io

(2) 커스텀 훅을 만드는 것이다.

커스텀 훅은 항상 뭔가 변경되는 값을 추리해서 뱉어내는 용도로 만드는 것만 봐서 이런 경우(단순히 다른 hook을 불러오기 위한 경우)에 쓰는 게 맞는지 모르겠다... 고 생각했는데 검색해보니 사례가 있는 것 같아서 활용할 수 있을지 읽어봐야겠다!

https://velog.io/@dev-redo/React-%ED%95%A8%EC%88%98%EB%A5%BC-%EB%A6%AC%ED%84%B4%ED%95%98%EB%8A%94-hook%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0

 

[React] 함수를 리턴하는 hook을 만들어 이벤트 핸들러 로직 분리하기

해당 포스팅은 함수를 리턴하는 hook을 만들어 컴포넌트에서 이벤트 핸들러 로직을 분리하는 방법에 대해 포스팅한다.

velog.io


결과

시도 #5 의 상태로 사용하는 데 문제가 없는지 궁금해서 코드스테이츠 질문 게시판에도 질문하고 멘토님께도 여쭤봤다.

https://github.com/codestates-seb/agora-states-fe/discussions/386

아마 import해오는 함수가 hook을 필요로 하는 이런 경우가 흔하지는 않은지 당황스러워하며 '이런 방식으로 시도해 봤을 때는 안 됐나요? 저렇게 해 봐도 안 되나요?' 하셨다. (즉 웬만해선 이런 구조의 코드를 짜지는 않는다는 거다)

결국 실행에 문제가 없고 가독성에도 문제가 없다며 두쪽에서 모두 ok 판정을 받다.

 

찜찜하기는 하지만 코드 사용이 '틀리지는' 않았다는 사실은 안심이 되었고 '일단 돌아가는 코드를 짠다'는 1차 목표에는 부합했다.

하지만 결정적으로 이 인증 코드와 그에 수반되는 다른 동작들을 합치면서 비동기적 처리 방식이 문제가 됐다.

예를 들자면 나는 access 검증 실패 => refresh 검증 => 토큰 갱신 완료 => 개인정보 받아오기 => 화면에 그리기 와 같은 순서로 진행시키고 싶었는데

각 인증 단계가 별개의 함수로 떨어져있고 각각 axios라는 비동기 처리를 진행하다보니 access 검증 실패 => 개인정보 받아오기 실패 => 화면에 에러 표시 =>refresh 검증 => 토큰 갱신 완료 같은 순서가 된 것이다.

멘붕이 와서 이래저래 검색하다가 하나의 커스텀훅에 일련의 인증과정을 몰아넣고 async/await로 최대한 동기 함수화 했다.

import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import { getCookieToken } from '../data/Cookie'
import { onLoginSuccess, onLogout } from '../components/Account/TokenAuth'

const useGetAuth = (tryAuth) => {
  const dispatch = useDispatch()
  const [authCheck, setAuthCheck] = useState(null)
  const [authLoading, setAuthLoading] = useState(true)

  useEffect(() => {
    const controller = new AbortController()
    const fetchData = async () => {
      console.log('토큰 확인 과정 시작')
      if (authLoading === false) setAuthLoading(true)
      try {
        const accessCheck = await axios({
          method: 'get',
          url: 'v1/authenticationTest',
          signal: controller.signal,
        })
        console.log(accessCheck)
        if (accessCheck.data.auth === 'Okay') {
          console.log('Access 인증 통과')
          setAuthCheck(true)
        }
      } catch (err) {
        console.log(err.message)
        //refresh 시도
        const refresh_token = getCookieToken()
        const refreshCheck = await axios({
          method: 'get',
          url: '/v1/users/validation',
          headers: { Refresh: refresh_token },
        })
        if (refreshCheck.data.token_status === 'RE_ISSUED') {
          console.log('Refresh 성공')
          onLoginSuccess(
            dispatch,
            refreshCheck.headers.new_authorization,
            refreshCheck.headers.new_refresh
          )
          setAuthCheck(true)
          console.log('authcheck를 true로 변경')
        } else {
          onLogout(dispatch)
          setAuthCheck(false)
        }
      } finally {
        setAuthLoading(false)
        console.log('로딩상태 종료')
      }
    }

    if (tryAuth !== false) fetchData()

    //useeffect cleanup function
    return () => controller.abort()
    //eslint-diable-next-line
  }, [tryAuth])

  return { authLoading, authCheck }
}

export default useGetAuth

회고

완성된 커스텀훅도 잘 만들어진 것이라고는 절대 생각하지 않는다.

각 과정이 이미 axios.then().catch()로 모두 연결되어 있고 개중에는 axios가 if문 안에 감싸져 있는 경우도 있었는데, 이걸 async/await와 try/catch문을 써서 하나로 리팩토링 하는 과정이 눈 돌아가게 어지러웠다.

그러면서 내가 각 문법에 대한 이해도 부족하고 Promise를 다루는 것이 너무 미숙하다는 걸 깨닫고 놀랐다.

이제 then 문법은 너무 익숙하게 써서 잘 쓰는가보다 했는데 말 그대로 익숙해졌을 뿐이었던 것 같다.

프로젝트 끝나면 처음부터 async/await로 axios 사용하는 방식을 다시 연습해봐야겠다.

 

(22.10.22)

기술 면접을 준비하면서 js를 다시 공부하다가 문득 이 hook 참조 문제를 클로저 구조로 해결했으면 어땠을까 하는 생각이 들었다.

리팩토링하면서 시도해 봐야겠다.

function onLoginSuccessOuter(){
 const navigate = useNavigate()
 return function onLoginSuccess(){
 	axios()
    .than(
    	navigate('/')
    )
 }
}