본문 바로가기

programming/React

Next.js 에서 Client Side Fetch 를 React-Query + 기본 Fetch 조합으로 사용하기

 

구현 조건

1. axios 대신 기본 fetch 를 사용

2. Authorization 에 accessToken 을 담아서 보내는 AuthFetch, 그냥 기본 fetch 인스턴스가 필요함

3. 로그인 후 받은 accessToken은 cookie 에 httpOnly 쿠키로 저장 -> js 단에서 가져다 쓸 수 없음

     - httpOnly 쿠키로 저장하지 않으면, next-cookie 의 getCookie() 등으로 자유롭게 가져다 쓸 수 있다.

4. next-auth의 useSession 을 통해서 accessToken 에 접근 가능

     - next-auth 의 useSession 을 통해서 접근할 수 있기 때문에 use** 형태의 훅 내부에 메서드를 사용하는 패턴으로 구현

 

단계별 구현

 

1. 기본 fetch 로 Client Side 데이터 fetch

  // 1. 기본 fetch
  useEffect(() => {
    fetch("https://api.coinpaprika.com/v1/coins")
      .then((res) => res.json())
      .then((data) => {
        console.log("data", data);
      });
  }, []);

 

 

2. 메서드의 재사용성을 위하여 hook 내부 method 로 구조 변경

1) useCheckout.ts 훅 생성

export const useCheckout = () => {
  
  const getCoins = () => fetch("https://api.coinpaprika.com/v1/coins").then((response) => response.json());

  return { getCoins };
};

 

2) 컴포넌트에서 메서드 호출

const { getCoins } = useCheckout();

// 2. useCheckout 내부 method 사용
useEffect(() => {
    getCoins().then((data) => {
      console.log("getCoins - data", data);
    });
}, []);

 

 

3. accessToken 를 헤더에 함께 보내는 AuthFetcher 사용

 

1) useFetchWithToken 훅 생성

import { useSession, signIn } from "next-auth/react";

interface FetchOptions extends RequestInit {
  headers?: HeadersInit;
}

export default function useFetchWithToken() {
  const { data: session, status } = useSession();

  const fetchWithToken = async (url: string, options: FetchOptions = {}): Promise<Response | undefined> => {
    if (status === "loading") {
      return;
    }
    if (status === "unauthenticated") {
      console.log("로그인이 필요합니다.");
      return;
    }

    const headers = {
      ...options.headers,
      "Content-Type": "application/json",
      Authorization: `Bearer ${(session?.user as any).accessToken}`,
    };

    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_CHAT_SERVER_API_URL}${url}`, {
        ...options,
        headers,
      });

      if (response.status === 401) {
        signIn();
      }

      return response;
    } catch (error) {
      console.error("Fetch error:", error);
      // Handle error accordingly
    }
  };

  return fetchWithToken;
}

 

2) useCheckout 훅에 method 추가 (fetchWithToken 을 사용하는 fetcher 메서드)

export const useCheckout = () => {
  const authFetcher = useFetchWithToken();

  const getCoins = () => fetch("https://api.coinpaprika.com/v1/coins").then((response) => response.json());

  const getAuthCoins = () => authFetcher("https://api.coinpaprika.com/v1/coins").then((response) => response.json());

  return { getCoins, getAuthCoins };
};

 

3) 컴포넌트 단에서 호출

  // 3. authFetcher 사용
  const { status } = useSession();
  useEffect(() => {
    getAuthCoins().then((data) => {
      console.log("getAuthCoins - data", data);
    });
  }, [status]);

 

4) 요청 헤더에 Authorization 확인 가능

 

 

4. react-query 사용하여 기본 fetch 호출

1번 로직에 react-query 추가

  // 4. useQuery 사용(기본 fetch)
  const { data } = useQuery({
    queryKey: ["test"],
    queryFn: () => fetch("https://api.coinpaprika.com/v1/coins").then((res) => res.json()),
  });
  useEffect(() => {
    console.log("useQuery - data", data);
  }, [data]);

 

 

5. react-query 에서 hook 내부 method 호출

2-1 에서 만들어 둔 getCoins 함수를 호출하여 사용

  // 5. useQuery 사용(useCheckout 내부 method 사용)
  const { data } = useQuery({
    queryKey: ["test"],
    queryFn: getCoins,
  });
  useEffect(() => {
    console.log("getCoins - data", data);
  }, [data]);

 

 

6. react-query 를 통해서 accessToken 를 헤더에 함께 보내는 AuthFetcher 사용

3-1 에서 만들어둔 useFetchWithToken 훅을 사용

useCheckout.ts

useFetchWithToken.ts 

동일하게 사용

 

컴포넌트 부분 코드 변경 -> useQuery 키값에 status 를 넣어서 세션의 status 값이 변경될때 재시도 될 수 있도록 함

// 6. useQuery 사용(authFetcher 사용)
  const { status } = useSession();
  const { data } = useQuery({
    queryKey: ["test2", status],
    queryFn: getAuthCoins,
  });

  useEffect(() => {
    console.log("getCoins - data", data);
  }, [data]);

 

 

 

<최종 모습>

1. MyComponent.tsx

import { useSession } from "next-auth/react";
import { useQuery } from "@tanstack/react-query";
import { useCheckout } from "@/shared/queries/checkout";


const { status } = useSession();
const { getAuthCoins } = useCheckout();
  
const { data } = useQuery({
    queryKey: ["test2", status],
    queryFn: getAuthCoins,
});

useEffect(() => {
	console.log("getCoins - data", data);
}, [data]);

 

 

2. useCheckout.ts

import useFetchWithToken from "@/hooks/useFetchWithToken";

export const useCheckout = () => {
  const authFetcher = useFetchWithToken();

  const getCoins = () => fetch("https://api.coinpaprika.com/v1/coins").then((response) => response.json());

  const getAuthCoins = () => authFetcher("/service/customers/niceid/oauth/token").then((response) => response.json());

  return { getCoins, getAuthCoins };
};

 

 

3. useFetchWithToken.ts

import { useSession, signIn } from "next-auth/react";

interface FetchOptions extends RequestInit {
  headers?: HeadersInit;
}

export default function useFetchWithToken() {
  const { data: session, status } = useSession();

  const fetchWithToken = async (url: string, options: FetchOptions = {}): Promise<Response | undefined> => {
    if (status === "loading") {
      return;
    }
    if (status === "unauthenticated") {
      console.log("로그인이 필요합니다.");
      return;
    }

    const headers = {
      ...options.headers,
      "Content-Type": "application/json",
      Authorization: `Bearer ${(session?.user as any).accessToken}`,
    };

    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_CHAT_SERVER_API_URL}${url}`, {
        ...options,
        headers,
      });

      if (response.status === 401) {
        signIn();
      }

      return response;
    } catch (error) {
      console.error("Fetch error:", error);
      // Handle error accordingly
    }
  };

  return fetchWithToken;
}