React Hook과 SWR로 편리하게 유저 인증 상태 관리하기

2025년 8월 11일

도입 배경

최근들어 React를 가지고 프로젝트를 진행하는 경우가 많아졌다. 이 프로젝트들의 경우 한가지 공통점이 있었는데, 그건 바로 사용자 인증이다. 기능 자체는 주로 JWT나 세션을 통해서 구현하지만, Protected Route나 Navigation Bar에서 사용자 정보를 표시하는 등의 경우를 위해 이러한 상태를 저장하는 것이 필요했다.

그래서 초기에는 사용자 인증 정보가 필요한 시점마다 인증 정보가 있어야만 접근할 수 있는 특정 API Endpoint를 호출하여 응답 결과에 따라 분기처리를 하는 방식으로 진행했다. 하지만 이 방법의 경우 사용자 정보가 필요한 모든 곳에 해당 로직을 삽입해야 하고, (구현 방식에 따라 다르지만) 데이터 갱신 처리를 직접 해야한다는 단점이 존재했다. 그렇기에 React Hook을 통해 인증 로직을 컴포넌트 어디서든 재사용성할 수 있도록 추상화하고, SWR을 사용하여 자동화된 데이터 갱신 처리를 지원하는 기능을 구현해보고자 한다.

React Hook이란?

구현하기에 앞서 React Hook에 대해 간단히 짚고 넘어가보도록 하겠다.

Functions starting with use are called Hooks. useState is a built-in Hook provided by React. You can find other built-in Hooks in the API reference. You can also write your own Hooks by combining the existing ones.

(출처: https://react.dev/learn#using-hooks)

React 공식 문서에 따르면 React Hook이란, use로 시작하는 함수를 Hook이라고 정의하고 있다. 이 Hook의 대표적인 예로는 우리가 매우 자주 사용하는 useState가 있다. 이름 말고도 다른 차이가 하나 더 존재하는데, Hook의 경우 컴포넌트(또는 다른 Hook)의 상단부에서만 호출할 수 있다는 제약사항이 있다. 따라서 만약에 조건문이나 반복문 안에서 Hook을 사용하려면 해당 로직을 새로운 컴포넌트로 추출하여 그 컴포넌트에서 호출하는 방식으로 사용하라고 공식 문서에서는 설명하고 있다. 우리의 경우 useAuth라는 커스텀 Hook을 만들어서 앞서 언급한 인증 로직을 구현할 예정이다.

SWR이란?

SWR은 먼저 캐시(stale)로부터 데이터를 반환한 후, fetch 요청(revalidate)을 하고, 최종적으로 최신화된 데이터를 가져오는 전략입니다.

기능

  • 빠르고, 가볍고, 재사용 가능한 데이터 가져오기
  • 내장된 캐시 및 요청 중복 제거
  • 실시간 경험
  • 포커스시 재검증
  • 네트워크 회복시 재검증
  • ...

(출처: https://swr.vercel.app/ko)

설명에서 알 수 있듯이 SWR은 데이터를 보다 효율적으로 가져올 수 있는 기능을 지원하는 React Hook의 일종이다. 우리의 경우는 이 중에서도 캐시 기반의 데이터 공유, 재검증(revalidation), 요청 중복 제거 기능을 활용할 예정이다.

인증 상태 관리를 예로 들어보자. 로그인 후 사용자 정보를 가져오는 API를 호출했다고 가정하면, SWR은 그 결과를 내장된 캐시에 저장해둔다. 이후 다른 컴포넌트에서 동일한 데이터를 요청한 경우, 네트워크 요청을 새로 보내는 대신 캐시에 있는 데이터를 우선적으로 반환한다. 그 결과 중복 요청 없이 빠르게 사용자 정보를 조회할 수 있게 된다. 인증 상태의 경우 언제든지 바뀔 수 있기 때문에 이런 기능을 활용하면 인증 상태를 최신으로 유지할 수 있다.

구현

이제 앞서 설명한 내용을 실제 코드로 구현해보자. 다음은 useAuth 커스텀 Hook의 예시이다.

const useAuth = () => {
    const router = useRouter();
    const { data, error } = useSWR<GlobalResponse<UserResponse>>("/api/user", { revalidateOnFocus: true });
    const user = data?.result || null;

    useEffect(() => {
        if (user?.fresh) toast.success("성공적으로 로그인했습니다.");
    }, [user]);

    const signOut = async () => {
        try {
            const signOutResponse = await api.post<GlobalResponse<SignOutResponse>, {}>("/api/auth/sign_out", {});
            if (!signOutResponse.data.status) return toast.error("로그아웃 처리 중 오류가 발생했습니다.");
            mutate("/api/user"); // revalidate
            router.replace("/");
            toast.success("성공적으로 로그아웃했습니다.");
        } catch (e) {
            console.error("Failed to sign out:", e);
        }
    };
    return { user, isLoading: !error && !data, signOut };
};

export default useAuth;

1. 사용자 정보 불러오기

useSWR Hook을 통해 로그인 API를 호출하여 사용자 정보를 가져온다. 옵션의 경우 revalidateOnFocus를 주었는데, 이 옵션의 경우 사용자가 브라우저 탭을 전환하거나 페이지에 돌아왔을 때 자동으로 사용자 정보를 갱신할 수 있도록 한다. 기본값이 true이기 때문에 생략해도 되지만 명시적으로 적어주었다. 참고로 해당 예시에서 사용한 API 응답 예시는 다음과 같다.

{
  status: true,
  result: {
    email: "john@doe.com",
    name: "John Doe",
    role: "MEMBER",
    fresh: true
  }
}

2. 로그인 직후 처리

useEffect Hook 내부에서는 user.fresh 여부를 확인하여, 새로 로그인한 경우 토스트 메시지를 띄우도록 구현하였다. 이런 식으로 Hook 내부에서 부가적인 처리도 수행함으로써 컴포넌트 단에서 불필요한 중복 처리를 줄일 수 있다.

3. 로그아웃 처리

Hook에서는 함수도 호출할 수 있는데, 해당 예시에서는 signOut 함수가 있다. signOut 함수는 로그아웃 API를 호출한 뒤, 성공적으로 처리되면 mutate("/api/user")를 호출한다. 이를 통해 SWR이 보유하고 있던 기존 캐시를 무효화하고, 최신 상태로 강제 갱신한다. 이는 로그아웃 상태를 바로 전역으로 반영시키기 위함이다. 이후 useRouter Hook을 통해 메인 페이지로 리다이렉트한 후, 토스트 메시지를 출력한다.

4. 로딩 상태 관리

마지막으로, isLoading 속성의 경우 dataerror의 상태를 바탕으로 로딩 상태를 나타낸다. 이를 통해 Hook을 사용하는 컴포넌트에서 쉽게 로딩 상태를 제어할 수 있게 된다.

마무리

이처럼 useAuth Hook을 구현하면 인증 상태와 관련된 복잡한 로직을 한 곳에 모아둘 수 있고, SWR을 통해 데이터 최신화까지 자동으로 처리할 수 있다. 이를 활용하면 Protected Route 같은 기능도 간단하게 구현할 수 있다. 마지막으로 해당 예시 하나를 공유하면서 글을 마무리하고자 한다.

const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
    const { user, isLoading } = useAuth();
    const [showLoginModal, setShowLoginModal] = useState(false);

    useEffect(() => {
        if (!isLoading && !user) {
            setShowLoginModal(true); // user가 없는 경우(= 로그인되어있지 않은 경우 Modal 출력)
        }
    }, [user, isLoading]);

    return (
        <>
            <LoginModal showModal={showLoginModal} setModal={setShowLoginModal} />
            {/* user가 있는 경우 하위 컴포넌트 출력, 없는 경우 아무것도 출력하지 않음 */}
            {user ? children : null}
        </>
    );
};

export default ProtectedRoute;

댓글