어서와, 개발은 처음이지?

브라우저 onfocus시에 react server component revalidate 하기 본문

Node

브라우저 onfocus시에 react server component revalidate 하기

오지고지리고알파고포켓몬고 2024. 4. 28. 04:02
반응형

영상으로 보기 - https://youtu.be/8QtXKioQbl4

 

1. 들어가기 전에

nextjs 13.4부터 본격적으로 도입된 app router와 react server componets 이후로 

위젯 주도 개발 방법론 (https://alexei.me/blog/widget-driven-development/) 에서 언급하는 api wrapper layer가 하던일을 next fetch가, 무려 server 에서만 실행되는 코드로 동작할 수 있게 되었는데요.

 

 

이로인해 이제는 react query나 swr같은 라이브러리를 코드 베이스에서 제거하더라도 (어느정도 추가 구현에 대한 불편함은 있을 수 있지만) 이런 방법론을 구현할 수 있게 되었습니다.

번들 사이즈로부터 훨씬 더 자유로워지면서요!

 

2. 코드를 보자

우선 본문에서 언급하려고하는 '브라우저에 onfocus 되었을때 revalidate'하는 기능의 기본 메커니즘은

// app/_components/Revalidator.tsx
"use client";

import { useEffect } from "react";

export default function Revalidator({
  revalidate,
}: {
  revalidate: () => void;
}) {
  useEffect(() => {
    const focusHandler = () => revalidate();

    window.addEventListener("focus", focusHandler);
    return () => window.removeEventListener("focus", focusHandler);
  }, [revalidate]);

  return null;
}

형태로 구현할 수 있습니다.

 

다만 여기서 revalidate함수로 무엇을 넣을까에 대한 부분이 핵심이 될텐데

app router 이후의 nextjs에서는 몇가지 캐시 구간이 존재하고, 이 캐시 구간을 동적으로 revalidate 시키는 방법은 크게 router.refresh(client), revalidatePath(server), revalidateTag(server) 메소드가 있습니다.

 

 

이때, router.refresh는 data fetching 영역까지 영향을 주지못하기 때문에 revalidatePath를 revalidate 함수로 주입하는 형태로 구현해보도록 하겠습니다.

앞에서 언급한대로 revalidatePath는 server action에서 실행되어야하므로 다음과 같이 정의합니다.

 

// app/page.tsx
import Revalidator from "./_components/Revalidator";

export default async function Home() {
  async function revalidate(path = "/") {
    "use server";

    revalidatePath(path);
  }

  return (
    <>
      <Revalidator revalidate={revalidate} />
    </>
  );
}

 

이 상태로 현재 시간을 나타내는 api를 호출하도록 구현했을때, 브라우저에 포커싱이 갈때마다 의도대로 페이지가 갱신되는 모습을 볼 수 있습니다. (분량 관계상 생략했지만 router.refresh를 사용하면 변경되지않는 모습을 보실 수 있습니다.)

 

// app/page.tsx
import Revalidator from "./_components/Revalidator";

export default async function Home() {
  const res = await fetch(
    "https://worldtimeapi.org/api/timezone/Asia/Seoul" // 서울 시간을 반환하는 api 입니다
  ).then((res) => res.json());
  
  
  async function revalidate(path = "/") {
    "use server";

    revalidatePath(path);
  }

  return (
    <>
      <Revalidator revalidate={revalidate} />
      {res.datetime}
    </>
  );
}

 

 

3. 좀 더 확장해보자

단순히 onfocus에서 그치지 않고, 무언가 mutate되는 순간에 동적으로 revalidate를 호출할 수 있도록, 이 코드를 전역 store로 변경하면 다음과 같은 형태로 구현할 수 있습니다.

 

// app/_components/Revalidator/client.tsx
"use client"

import { createContext, useContext, useEffect } from "react";

const RevalidatorContext = createContext<{
  revalidatePath: (path?: string) => void;
}>({ revalidatePath: () => {} });

export function RevalidatorProvider({
  revalidate,
  children,
}: {
  revalidate: (path?: string) => void;
  children: React.ReactNode;
}) {
  useEffect(() => {
    const focusHandler = () => {
      console.log("focused");
      revalidate();
    };

    window.addEventListener("focus", focusHandler);
    return () => window.removeEventListener("focus", focusHandler);
  }, [revalidate]);

  const revalidatePath = (path?: string) => {
    revalidate(path);
  };

  return (
    <RevalidatorContext.Provider value={{ revalidatePath }}>
      {children}
    </RevalidatorContext.Provider>
  );
}
// app/_components/Revalidator/server.tsx

import { revalidatePath } from "next/cache";
import { RevalidatorProvider } from "./client";

export function RevalidatorServer({ children }: { children: React.ReactNode }) {
  async function revalidate(path = "/") {
    "use server";

    revalidatePath(path);
  }

  return (
    <RevalidatorProvider revalidate={revalidate}>
      {children}
    </RevalidatorProvider>
  );
}

 

이것을 root layout에 위치시켜주면 원하는 곳에서 원하는 mutate를 실행하고 컴포넌트를 revalidate 시킬 수 있겠습니다.

// app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";
import { RevalidatorServer } from "./_components/Revalidator/server";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <RevalidatorServer>{children}</RevalidatorServer>
      </body>
    </html>
  );
}
// app/_components/Form.tsx
"use client";

import { useRevalidatorContext } from "./Revalidator/client";

export default function Form() {
  const { revalidatePath } = useRevalidatorContext();

  const handleSubmit = async () => {
    await fetch('...', { method: "PUT", /* ... */ });
    revalidatePath("/");
  }

  return <form onSubmit={handleSubmit}>
    {/* ... */}
  </form>
}

 

다만 한가지 주의할 점은 이렇게 revalidate 시키게 되면 상황에 따라 서버 리소스가 증가할 수 있는 부분을 유의하셔야합니다.

revalidatePath("/")은 best practice가 아니라 간단하게 전체 페이지를 revalidate하는 일종의 trick으로써 소개해드린 부분이기 때문에 실제 서비스에 적용할 땐 ux 대비 비용 등을 충분히 고민해보시거나, 프로덕트에 적합한 캐시 관리 기법을 고민해보시는게 좋을 것 같습니다.

 

4. 마무리

https://github.com/vercel/next.js/discussions/54075#discussioncomment-6737635 이 글을 보시면 nextjs에 app router가 도입되면서 생긴 캐시 레이어에 대해 다양한 의견들을 볼 수 있는데, 만약 next나 react 진영에서 이런 방향성을 고수한다면 앞으로 캐시 레이어 관점에서도 최적화 방법을 고민해 볼 요소가 많아질 것 같습니다.

 

아직 다양한 케이스를 커버할 정도의 범위에 rsc를 사용하고있진 않은데, 유의미한 사례가 좀 더 취합된다면 추가로 공유드려볼 수 있도록 하겠습니다.

 

Comments