[나만무/개발단계]Invalid hook call 에러 해결!

CHO WanGi·2025년 7월 5일

KRAFTON JUNGLE 8th

목록 보기
83/89

React의 Hook은 컴포넌트의 상태 관리를 편리하게 만들어주는 강력한 도구다.
하지만 정해진 규칙을 지키지 않으면 예상치 못한 오류를 만나게 된다.

이 글에서는 axios interceptor와 같이 React 컴포넌트가 아닌
일반 함수에서 Zustand Hook을 호출하여 발생했던 문제와,
getState 함수를 통해 이 문제를 해결한 경험을 공유하고자 한다.

React Hook의 핵심 규칙

먼저, React 공식 문서에서 강조하는 두 가지 핵심 규칙을 짚고 넘어가자.

1. Hook은 최상위(Top Level)에서만 호출해야 한다.

React는 렌더링마다 동일한 순서로 Hook이 호출될 것을 전제로 상태를 관리한다.
만약 조건문이나 반복문 내부에서 Hook을 호출하면,
렌더링 시점마다 Hook의 호출 순서가 달라질 수 있어 상태 추적에 심각한 오류가 발생할 수 있다.

function Bad({ cond }) {
  if (cond) {
    // 🔴 잘못된 사용: 조건부 내부에서 Hook 호출
    const theme = useContext(ThemeContext);
  }
  // ...
}

따라서 Hook은 조건문, 반복문, 중첩된 함수 내부에서 호출하면 안 된다.

2. Hook은 React 함수 내에서만 호출해야 한다.

Hook은 React 함수 컴포넌트 또는 다른 커스텀 Hook 안에서만 호출되어야 한다.
일반 JavaScript 함수에서 Hook을 호출하는 것은 규칙 위반이다.
내가 마주했던 문제가 바로 이 두 번째 규칙과 관련이 있다.

문제 상황: Axios Interceptor에서 Zustand 스토어 접근하기

프로젝트에는 캔버스별로 독립된 그룹 채팅 기능이 있다.
사용자가 채팅 버튼을 누르면 웹소켓 연결을 시도하는데,
이때 현재 캔버스를 식별하기 위한 canvas_id를 함께 전달해야 했다.

canvas_id는 Zustand 스토어에 저장되어 있었고,
나는 API 요청의 응답을 가로채는 axios interceptor에서
웹소켓을 재연결할 때 이 값이 필요했다.

다음은 문제가 발생했던 코드다.

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true, // 쿠키를 주고받기 위한 설정
});

// 응답 인터셉터 설정
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    // 401 Unauthorized 오류 발생 시 토큰 재발급 로직
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        // Access Token 재발급 요청
        const res = await apiClient.post('/auth/refresh');
        const newAccessToken = res.data.accessToken;
        
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;

        // 🔴 문제의 코드! React 컴포넌트가 아닌 곳에서 Hook 호출
        const canvas_id = useCanvasStore((state) => state.canvas_id);

        // 새로운 토큰으로 웹소켓 재연결
        socketService.disconnect();
        socketService.connect(canvas_id);

        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh Token마저 만료되면, 더 이상 인증을 유지할 수 없으므로 로그아웃 처리
        useAuthStore.getState().clearAuth();
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

export default apiClient;

문제 원인: React 함수가 아닌 곳에서 Hook 호출

문제의 원인을 파악하기 위해 코드를 다시 살펴본다.

const canvas_id = useCanvasStore((state) => state.canvas_id);

Zustand는 useCanvasStore와 같은 커스텀 Hook을 통해
스토어의 상태에 접근하는 것이 일반적이다.

하지만 위 코드의 apiClient.interceptors.response.use(...) 콜백 함수는
React 컴포넌트나 다른 Hook이 아닌,
일반적인 JavaScript 함수다.

따라서 이곳에서 useCanvasStore라는 Hook을 호출하는 것은
'Hook을 React 함수에서만 호출하세요'라는 규칙을 정면으로 위반하는 것이다.

해결책: getState()로 스토어 상태에 직접 접근

다행히 Zustand는 Hook을 사용하지 않고도 스토어의 상태에 접근할 수 있는 방법을 제공한다. getState() 메서드를 사용하면 이 문제를 간단히 해결할 수 있었다.

// 수정된 코드
const canvas_id = useCanvasStore.getState().canvas_id;

useCanvasStore.getState()는 Hook이 아닌 일반 함수이므로,
React의 렌더링 컨텍스트 외부에서도 안전하게 상태 값을 가져올 수 있다.

결론 및 회고

이번 경험을 통해 React Hook의 기본 규칙을 지키는 것이 얼마나 중요한지 다시 한번 깨달았다.

특히 컴포넌트 외부의 로직(axios interceptor, 이벤트 리스너 등)에서 상태 값에 접근해야 할 경우, useStore와 같은 Hook 대신 store.getState()와 같은 API를 활용해야 한다는 점을 명심하자....

profile
제 Velog에 오신 모든 분들이 작더라도 인사이트를 얻어가셨으면 좋겠습니다 :)

0개의 댓글