Next.js 에서 SWR 좀 더 우아하게 사용하기 - (3 / 3)

신대현·2022년 9월 22일
5

SWR

목록 보기
3/3
post-thumbnail

재사용 가능한 hook 만들기

SWR은 타입스크립트를 완벽하게 지원하고 있습니다. 예제를 보도록 하겠습니다

// swr/types.d.ts
export interface SWRResponse<Data = any, Error = any> {
    data?: Data;
    error?: Error;
    mutate: KeyedMutator<Data>;
    isValidating: boolean;
}
import useSWR, { type SWRResponse } from "swr";

interface User {
  name:string
  age:string
}

export const useUser = (): SWRResponse<User> => {
  const swr = useSWR<User>(`/api/user`)
  return swr
}
  • useUser라는 hook 을 만들어보았습니다
  • Deduplication 기능을 가지고 있기에 useUser을 어느 컴포넌트에서 사용하더라도 네트워크 요청은 한 번밖에 하지 않을 겁니다.
  • SWR은 타입스크립트를 완벽하게 지원합니다
  • 제네릭 으로 타입을 파라미터로 넘길 시 SWRResponse이란 타입으로 return 되는 타입까지 추론해 주는 모습을 볼 수 있습니다.

인증

❌❌❌❌
// page.tsx
import { type GetServerSideProps } from "next";
import { unstable_serialize } from "swr";

export const Page = () => {
  const { data } = useSWR<User>([`/api/user`,Token]);
  return (
    <>
      <div>{data.data.name}</div>
      <div>{data.data.age}</div>
    </>
  );
};

export const getServerSideProps: GetServerSideProps = async () => {
  const userData = await fetcher(`/api/user`,Token);
  return {
   props: {
    	fallback:{
    		['/api/user']:userData,
      	}
    },
  };
};
  • 이전 글을 안 보셨다면 꼭 보고 오시길 바랍니다.
  • 위의 예제를 봤을 때는 전혀 문제없을 거 같지만 pre-render가 안될 뿐만 아니라 불필요한 호출을 요청할 수도 있습니다
useSWR<User>(`/api/user`);

👆 단일 key를 사용했을 경우의 cache 된 data

useSWR<User>([`/api/user`,Token]);

👆Complex Keys로 넣었을 경우 cache 된 data

  • 2개의 차이점이 보이시나요 pre-render를 위해선 key 값이 같아야 한다는 걸 이전 포스팅을 보셨다면 알고 계실 겁니다
props: {
    	fallback:{
    		['/api/user']:userData,
      	}
    },
  • 이전 코드를 다시 들여다보면은 fallback에 [/api/user] key에 userData를 넘겨주고 있는 모습입니다. 이렇게 되면은
    useSWR 에서의 key 는 [@"/api/user" , "Token"] 으로 되어있고
    getServerSideProps 에서는 [/api/user] 으로 fallback key를 넘겨 주어 key가 일치하기 않기 때문에 pre-render가 되질 않습니다.
  • 이걸 해결하기 위해 있는 것이 Complex Keys 입니다. 바로 예제를 보도록 하겠습니다
✅✅✅✅
// page.tsx
import { type GetServerSideProps } from "next";
import { unstable_serialize } from "swr";

export const Page = () => {
  const { data } = useSWR<User>([`/api/user`,Token]);
    return (
    <>
      <div>{data.data.name}</div>
      <div>{data.data.age}</div>
    </>
  );
};

export const getServerSideProps: GetServerSideProps = async () => {
  const userData = await fetcher(`/api/user`,Token);
  return {
   props: {
    	fallback:{
    		[unstable_serialize(['/api/user',Token])]: userData,
      	}
    },
  };
};
  • useSWR 은 array 나 function 타입을 key로 사용할 수 있습니다. 이 타입의 키를 사용하기 위해선 fallback key들을 unstable_serialize와 함께 직렬화해주어야 동일한 key 를 가질 수 있습니다

Intersection Observer로 ✨심플하게 최적화 해보기

  • 위와 같이 작은 디바이스 크기의 화면이 있다고 가정해보겠습니다
  • 현재 상황은 /api/user에 대한 데이터를 화면에 보이지도 않은 상태인데 메인에서 호출하고 있는 상태입니다.
  • 유저가 스크롤도 내려보기도 전에 해당 페이지를 떠나는 경우를 가정해 본다면 불필요한 호출이 될 것입니다.
  • 해당 데이터가 필요한 UI 요소가 유저 화면에 보일 경우 API 호출을 하도록 최적화 작업을 해보도록 하겠습니다.
// useOnScreen.ts
import { useState, useEffect, type RefObject } from "react";

interface Params {
  isOnce?: boolean;
}

/** IO Custom hooks
 * @param {HTMLElement} ref - HTML Element
 * @param {boolean} prarms.isOnce - true 일 경우 IO 가 헌반만 체크된 후 요소 감시를 멈춤니다. 
 * */
export default function useOnScreen<T>(ref: RefObject<T>, { isOnce = true }: Params = {}) {
  const [isIntersecting, setIntersecting] = useState<boolean>(false);

  useEffect(() => {
    if (!ref.current) return;
    const observer = new IntersectionObserver(([entry], _observer) => {
      if (!isOnce) {
        setIntersecting(entry.isIntersecting);
        return;
      }
      if (entry.isIntersecting) {
        setIntersecting(true);
        _observer.disconnect();
      }
    });
    if (ref.current) {
      observer.observe(ref.current);
    }
    return () => {
      observer.disconnect();
    };
  }, [ref.current]);

  return isIntersecting;
}

Intersection Observer API를 사용하기 재사용 하기 위해 간단한 hook을 만들겠습니다

// page.tsx
import UserInfo from "~/components/Template/UserInfo";

const MainPage = () => {
  return (
    <>
      <Container>아무 의미 없는 글인데 /api/user Data는 밑에 안 보이는곳에 있어요 👇👇</Container>
      <Container2>👇👇👇👇👇👇👇</Container2>
      <UserInfo />
    </>
  );
};

// ~/components/Template/UserInfo
import { useRef } from "react";
import useSWR, { type SWRResponse }  from "swr";

import useOnScreen from "~/hooks/useOnScreen";

interface User {
  name: string;
  age: number;
}

// SWR custom hook
const useUser = (visible: boolean = true): SWRResponse<User> => {
  const swr = useSWR<User>(visible ? [`/api/user`, "Token"] : null);
  return swr;
};

const UserInfo = () => {
  const obRef = useRef<HTMLDivElement>(null);
  const visible = useOnScreen<HTMLDivElement>(obRef);
  
  const { data: userData } = useUser(visible);
  
  if (!userData) return <div ref={obRef}>loading</div>;
  
  return (
    <div>
      <div>{userData?.name}</div>
      <div>아직은{userData?.age}</div>
    </div>
  );
};

  • Intersection Observer API가 UI 요소를 관찰하였을 경우 API를 호출하는 모습입니다.
  • SWR의 Conditional Fetching 은 key에 null 값을 입력받을 경우 SWR은 요청을 시작하지 않습니다
    그 부분을 Intersection Observer를 활용하여 최적화 작업을 해보았습니다.

다음 포스팅에서는 useSWRInfinite 과 React 18 의 Suspense 를 다루어 보도록 하겠습니다

참고

https://www.typescriptlang.org/
https://swr.vercel.app/ko

profile
프론트엔드 개발자 입니다

2개의 댓글

comment-user-thumbnail
2022년 9월 28일

3개 이어진 포스트 모두 읽어봤는데 되게 깔끔하게 잘 작성하셨네요.
덕분에 잘 배우고 갑니다:)

답글 달기
comment-user-thumbnail
2022년 11월 27일

intersection observer를 이용해서 불필요한 api 호출을 방지하는게 굉장히 인상적이네요! 잘 배웠습니다!

답글 달기