11. Next.js에서 최신 데이터를 보여주기
11. Next.js에서 최신 데이터를 보여주기

11. Next.js에서 최신 데이터를 보여주기

Authors
Date
Tags
Published
Published
Slug

업데이트가 안된다..??

필자는 table기반의 관리 프로그램을 개발중이다.
데이터의 업데이트가 빈번하게 일어날 수 있는 애플리케이션이므로 최신 데이터를 보여주는것이 중요할 것이다.
개발을 하던 중, 우연히 최신 데이터가 변경되지 않는 것을 발견하였다. 🤨
 
notion image
  1. 애플리케이션을 2개 실행한다.
  1. 왼쪽 애플리케이션에서 데이터를 업데이트 한다.
  1. 오른쪽 애플리케이션에서 다른 페이지로 이동한 후 다시 원래 페이지로 돌아온다.
데이터가 업데이트될 것을 기대했지만, 데이터는 업데이트되지 않았다.

 
 

원인이 무엇일까??

원인은 Next.js의 cache와 관련이 있었다.
이를 이해하기 위해서는 먼저 Next.js의 랜더링 전략에 대해 이해할 필요가 있다.
 

Static Rendering

  • 기본적으로 사용되는 랜더링
  • 빌드타임에 랜더링 됨
  • 랜더링 결과는 캐시되어 CDN을 통해 전달된다
  • 정적인 블로그 게시물 또는 제품 설명 페이지와 같이 사용자 맞춤형 콘텐츠가 필요하지 않은 곳에 사용됨

    Dynamic Rendering

    • 각 유저가 요청할 때 랜더링 됨
    • 사용자 맞춤형 데이터 또는 콘텐츠가 필요한 곳에 사용됨
       
      그렇다면 Table컴포넌트는 Static Rendering일까 Dynamic Rendering일까?
      Table이 포함된 페이지에서 캐시를 하지 않는 요청을 한다면 Dynamic Rendring이 이뤄질 것이다.
      📄
      요청이 캐시되지 않는조건
      fetch requests are not cached if:
      • The cache: 'no-store' is added to fetch requests.
      • The revalidate: 0 option is added to individual fetch requests.
      • The fetch request is inside a Router Handler that uses the POST method.
      • The fetch request comes after the usage of headers or cookies.
      • The const dynamic = 'force-dynamic' route segment option is used.
      • The fetchCache route segment option is configured to skip cache by default.
      • The fetch request uses Authorization or Cookie headers and there's an uncached request above it in the component tree.
       
       
       
      그렇다면, 데이터 요청이 어떻게 이루어지는지 코드를 살펴보자
       
      import prisma from "@/app/lib/prismaClient"; import { unstable_noStore as noStore } from "next/dist/server/web/spec-extension/unstable-no-store"; export async function fetchPorts() { noStore(); try { const fetchFilteredPorts = await prisma.port.findMany(); return fetchFilteredPorts; } catch (error) { console.error("Failed to fetch ports:", error); throw new Error("Failed to fetch ports."); } }
      데이터 요청은 대부분 위와 같은 형식으로 이루어진다.
       
      특이한점은 unstable_noStore() 함수를 사용했다는 것이다.
       
      이 함수를 호출하면 fetch옵션에 cache: 'no-store' 를 추가한것과 같은 효과를 얻을 수 있다.
      따라서 캐시되지 않는 데이터 요청이 발생하며, fetchPorts를 호출하는 페이지는 dynamic rendering이 발생한다.
       
      💬
      ㅇㅋ 그럼 Table컴포넌트가 포함된 페이지에서 dynamic rendering이 발생하는것은 알겠다. 👌
       
       
       
       
      그 다음으로 static rendring과 dynamic rendering의 Router cache 동작 방식의 차이를 이해해야한다.
      📄
      Router cache의 지속시간 (문서 중 일부)
      • Automatic Invalidation Period: The cache of an individual segment is automatically invalidated after a specific time. The duration depends on whether the route is statically or dynamically rendered:
        • Dynamically Rendered: 30 seconds
        • Statically Rendered: 5 minutes
      While a page refresh will clear all cached segments, the automatic invalidation period only affects the individual segment from the time it was last accessed or created.
      By adding prefetch={true} or calling router.prefetch for a dynamically rendered route, you can opt into caching for 5 minutes.
       
       
      dynamic rendering에서는 캐시가 30s동안 유지된다.
      그렇기에, 30s 이내에 다시 페이지로 돌아오면 캐시된 데이터를 보여준다는 뜻이다.

       
       

      두번째 문제

      그렇다면 30s가 지난 후 페이지 전환을 하면 어떻게 될까?
       
      notion image
       
      1. 각 페이지를 새로고침한다
      1. 30s를 기다린다.
      1. 왼쪽 애플리케이션에서 데이터를 업데이트한다.
      1. 오른쪽 애플리케이션에서 다른 페이지로 이동한 후 다시 돌아온다.
      이번에는 데이터가 잘 업데이트 된다.
      그런데 이상한점은 한 번 30s가 지난 이후로는 페이지 이동 시 다시 30s를 기다릴 필요 없이 매 번 업데이트가 이루어진다는 점이다. 😒
       
       
      매 30s마다 캐시가 초기화되고 캐시가 없을 때 페이지 전환시 새로운 캐시를 저장하는것으로 이해했기에 이상하다고 생각했다.
       
      결국 서칭 끝에 관련 이슈를 찾을 수 있었다.
      Inconsistent router cache behavior
      Updated Mar 6, 2024
      결론만 말하자면, Link컴포넌트의 prefetch={true}를 추가하여 해결할 수 있다고 한다.
       
      그리고 얼마전에 이슈가 해결된 듯 보인다.
      Fix instant loading states after invalidating prefetch cache
      Updated Apr 3, 2024
       
      v14.2.0-canary.32 버전에 pr이 적용된듯 하다.
       
      필자는 Next.js 14.0.2를 사용 중이었고, 베타 버전인 canary 버전으로 업데이트 해보기로 했다.
      yarn add next@canary
       
      이제 30s 마다 새로운 데이터로 업데이트 되는것을 확인할 수 있었다. 😄

       
       

      그래서 최신 데이터를 잘 보여주는가..?

      아직 근본적인 문제인 최신 데이터를 보여주지 않는 문제를 해결하진 못했다.
      사용자는 페이지 전환을 해도 최대 30초 이전의 데이터를 보게된다.
       
       
      필자는 이 문제에 대한 적합한 해결책을 찾기 위해 여러 Next.js 이슈와 토론글을 살펴보았다..🫠
      해당 토론은 router cache가 30s 동안 유지되는것을 0s로 바꿀 수 있다면 문제가 해결되는지에 대한 토론이며
      staletime이라는 새로운 개념을 추가하는것으로 결론지어진듯 보인다. 😲
       
       
      (staletime을 소개하는 글)
       
       
      add experimental client router cache config
      Updated Apr 17, 2024
      staletime은 아직 개발중인 기능이며, 현재 canary에 일부 머지된 듯 보인다. (2024.04.01 기준)
       

       
       
       

      당장 써보고 싶지만..

      하지만 최신 canary 버전에서도 아직 release되지 않았다.. 아무래도 staletime을 쓰려면 조금 기다려야 할 듯 하다. 😭
       
      가장 공격적인 해결책으로, Table컴포넌트에서 router.refresh()를 호출하는 방법이 있다.
       
       
      const router = useRouter(); useEffect(() => { router.refresh(); }, [router]);
      테이블 컴포넌트에 위 코드를 추가하여 router cache가 페이지 이동할때마다 초기화 되도록 하였다.
       
       
       
      notion image
      페이지 이동시 새로운 데이터를 잘 불러오는것을 볼 수 있다. 😀
       
       
       
      하지만 router.refresh()는 컴포넌트 캐시(탐색한 모든 세그먼트를 보유하며, 스크롤 위치를 유지한 채 부분 렌더링 및 앞/뒤로 이동하는 데 사용됨)를 초기화 하기 때문에 앞/뒤로 페이지 이동 시 스크롤이 유지되지 않는 문제점이 생길 수 있다.

       
       

      +그런데.. 왜 하필 30s일까?

      문제는 이제 해결이 되었지만, dynamic rendering에서 캐시가 30s 동안만 유지되는 이유가 궁금했다.
      이 30s는 staleTime이 도입되기 전까진 변경할 수 없다. 30s로 정해진 이유가 무엇일까?
      그 이유 또한 토론글에서 찾을 수 있었다.
      📄
      There is prior art and research associated with this timing, e.g. Facebook has this timing when you click around the application. react-query is adopting this timing as well.
      페이스북, react-query의 선행 기술 및 연구에 따라 30s로 결정한 듯 보인다.
       
       
       
       
       

      ++ 데이터를 더 빠르게 보여주기 (supabase)

      supabase에는 realtime 이라는 기능을 제공한다.
      supabase dashboard 에서 Table Editor탭에 들어가면 각 테이블마다 우측 상단에 realtime 버튼이 있을 것이다.
      이 버튼을 눌러 활성화 하면 실시간으로 업데이트 되는 데이터를 보여줄 수 있다.
       
      const supabase = createClient(); useEffect(() => { const channel = supabase .channel("realtime quotations") .on( "postgres_changes", { event: "*", schema: "public", table: "quote", }, () => { router.refresh(); }, ) .subscribe(); return () => { supabase.removeChannel(channel); }; }, [supabase, router]);
      위와같이 useEffect 훅을 사용하여 subscribe 매서드를 호출하도록 하면
      notion image
      네트워크 탭에 이렇게 소켓연결이 활성화 되며, 변경 이벤트를 감지할 수 있다.