[React/Zustand] 리액트 훅은 함수 컴포넌트 내부에서만 호출될 수 있습니다.

2025. 3. 2. 16:38·트러블 슈팅

한 프로젝트에서 회원 관리 담당인 내가 로그아웃 로직을 구현했는데, 팀원이 여기에 상태 관리 코드를 추가했다.

const useUserStore = create<UserState>()(
  persist(
    (set, get): UserState =>
      ({
      user: null,
      role: null,
      isAuthenticated: false,
      isLogin: false,
      accessToken: null,
      setAccessToken: (token) => set({ accessToken: token }),
      setUser: (user) => set({ user }),

	// 생략
      
     logout: async () => {
        const { user } = get();
        const healthStore = useHealthStore();
        const galleryStore = useGalleryStore();

        if (!user) {
          console.error('로그인 해주세요.');
          return;
        }
        try {
          await getLogout(user.id);
          set({
            isLogin: false,
            isAuthenticated: false,
            user: null,
            accessToken: null,
          });
          healthStore.reset(); // 로그아웃시 건강 정보 store reset
          galleryStore.reset(); // 로그아웃시 갤러리 store reset
        } catch (error) {
          console.error(error);
        }
      },
      
      // 이하생략

Zustand를 사용해 userStore를 관리하는 과정에서, logout 함수 내에서 useHealthStore와 useGalleryStore를 호출하여 각각의 상태를 초기화하려고 했던 것 같다. 그러나 아래와 같은 오류가 발생했다!

" 리액트 훅은 함수 컴포넌트 내부에서만 호출될 수 있습니다. 이는 훅들이 리액트의 내부 상태와 라이프사이클에 접근하므로, 함수 컴포넌트 밖에서 호출하게 되면 예상치 못한 동작을 유발할 수 있기 때문입니다. "

-> logout 함수는 컴포넌트가 아닌 Zustand의 Store 내부에서 선언되었기 때문에 useStore과 같은 React Hook을 직접 사용할 수 없던 것이었다!

그럼 로그아웃을 하면서 두 store를 어떻게 reset해야될까...

✅ getState를 활용한 직접 접근

logout: async () => {
  const { user } = get();
  const healthStore = useHealthStore.getState();
  const galleryStore = useGalleryStore.getState();

  if (!user) {
    console.error('로그인 해주세요.');
    return;
  }
  try {
    await getLogout(user.id);
    set({
      isLogin: false,
      isAuthenticated: false,
      user: null,
      accessToken: null,
    });
    healthStore.reset(); // 건강 정보 store reset
    galleryStore.reset(); // 갤러리 store reset
  } catch (error) {
    console.error(error);
  }
}

-> 이렇게 Zustand Store의 getState()를 사용하면 훅을 호출하지 않고도 Store의 상태와 메서드에 접근할 수 있다!

❓ getState

- Zustand 같은 상태 관리 라이브러리에서 현재 Store의 상태를 직접 가져올 때 사용

- 👍장점

  1. React의 Hook은 컴포넌트 내부에서만 호출 가능하지만, getState()는 어디서든 호출할 수 있어 유연함.
  2. API 호출, 이벤트 핸들러, Zustand store 내부 등에서 상태를 가져와야 할 때 훅 없이 직접 접근 가능.
  3. useStore 같은 훅을 사용하면 값이 변경될 때마다 리렌더링될 수 있지만, getState()는 상태를 직접 가져오기 때문에 불필요한 리렌더링을 방지할 수 있음.
  4. 한 Store에서 다른 Store의 상태를 변경할 때 getState()를 사용하면 쉽게 접근 가능.

- 👎단점

  1. useStore 훅과 달리 getState()는 상태 변화에 반응하지 않음. 즉, getState()로 가져온 값이 변경되어도 자동으로 UI가 업데이트되지 않음.
  2. useStore 훅을 사용하면 React DevTools에서 상태 변화를 추적하기 쉽지만, getState()를 남용하면 예상치 못한 상태 변화가 발생할 수 있음.
  3. getState()를 호출하는 시점의 상태를 가져오기 때문에, 예상과 다르게 이전 상태를 참조할 가능성이 있음.

✅ logout 페이지를 별도로 만들어 이동

import React, { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import useUserStore from '../stores/UserStore';
import useHealthStore from '../stores/HealthStore';
import useGalleryStore from '../stores/GalleryStore';


function LogoutPage() {
  const { isLogin, logout } = useUserStore();
  const ResetHealth = useHealthStore().reset
  const ResetGallery = useGalleryStore().reset

  useEffect(() => {
    logout()
    ResetHealth()
    ResetGallery()
  }, []);

  function render() {
    return <Navigate to="/" replace={true} />
  }

  return isLogin ? null : render()
}

export default LogoutPage;

팀원분은 이렇게 로그아웃 페이지를 따로 구현해서 로그아웃 버튼이 있는 곳에서 리디렉션을 시키는 방법을 제안했다.

이렇게 구현한다면 React의 Hook 규칙을 어기지 않으면서, 컴포넌트 단위 관리로 의존성을 줄일 수 있다.

하지만 추가적인 페이지 이동을 해야하고, 여러 곳에서 Store 초기화가 필요할 경우 코드가 중복될 가능성이 있다.

store 초기화가 오래 걸려 UX를 저하시키거나, 다른 곳에서 Store 초기화를 하지 않았기에 위 코드를 유지했다. 

✅ 기존 페이지에서 로그아웃 실행

근데 생각해보면 로그아웃 버튼이 있는 페이지에서 reset을 할 수도 있는 거 아닌가?

async function handleLogout() {
  const userStore = useUserStore();
  const healthStore = useHealthStore();
  const galleryStore = useGalleryStore();

  await userStore.logout();
  healthStore.reset();
  galleryStore.reset();
};

React 함수 컴포넌트 내부에서 사용하니까 규칙을 위반하지 않아 getState를 사용하지 않아도 된다.

하지만 getState()를 쓰면 어디서든 사용 가능하니 더 권장되는 방식인 것 같기도 하다...

그리고 리셋할 저 두 store들은 상태 변화를 추적할 일이 없으니까 굳이 useStore를 사용할 필요가 없긴 하다.

async function handleLogout() {
  const userStore = useUserStore.getState();
  const healthStore = useHealthStore.getState();
  const galleryStore = useGalleryStore.getState();

  await userStore.logout();
  healthStore.reset();
  galleryStore.reset();
};

결론은 나라면 이렇게 로그아웃을 하는 페이지에서 getState를 사용해 reset해주는 것을 채택했을 것 같다!

저작자표시 비영리 변경금지 (새창열림)

'트러블 슈팅' 카테고리의 다른 글

[Axios] 서버에서 사용자가 업로드한 파일을 받아오지 못하는 에러 핸들링  (0) 2025.05.03
[Next.js + Zustand] localStorage hydration 에러 핸들링  (0) 2025.05.02
[Node.js] prisma 클라이언트 초기화 오류 해결하기  (1) 2025.04.16
[Next.js] Image 경고(LCP, 종횡비) 해결하기  (0) 2025.04.10
'트러블 슈팅' 카테고리의 다른 글
  • [Axios] 서버에서 사용자가 업로드한 파일을 받아오지 못하는 에러 핸들링
  • [Next.js + Zustand] localStorage hydration 에러 핸들링
  • [Node.js] prisma 클라이언트 초기화 오류 해결하기
  • [Next.js] Image 경고(LCP, 종횡비) 해결하기
버그잡는고양이발
버그잡는고양이발
주니어 개발자입니다!
  • 버그잡는고양이발
    지극히평범한개발블로그
    버그잡는고양이발
  • 전체
    오늘
    어제
    • 분류 전체보기 (382)
      • React (16)
      • Next.js (5)
      • Javascript (5)
      • Typescript (4)
      • Node.js (2)
      • Cs (16)
      • 트러블 슈팅 (5)
      • Html (1)
      • Css (3)
      • Django (0)
      • vue (0)
      • Java (2)
      • Python (0)
      • 독서 (1)
      • 기타 (3)
      • 백준 (192)
      • swea (31)
      • 프로그래머스 (30)
      • 이코테 (4)
      • 99클럽 코테 스터디 (30)
      • ssafy (31)
      • IT기사 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 인기 글

  • 태그

    99클럽
    코딩테스트준비
    항해99
    개발자취업
    Til
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
버그잡는고양이발
[React/Zustand] 리액트 훅은 함수 컴포넌트 내부에서만 호출될 수 있습니다.
상단으로

티스토리툴바