권현우의 프로필 사진

Hyunwoo

Nextjs Server Fetching

Nextjs Server Fetching Test

2025.09.07

  • nextjs
  • react
  • framework

Nextjs Server Side fetching cache test

Nextjs Server Fetching cache를 실제로 테스트해보았다.

일단 요청시점을 return 하는 api를 만들었다. (Nextjs의 api routes 기능을 사용)

// app/api/clock route.ts
import { NextResponse } from 'next/server'
 
export async function GET() {
  // 매 요청마다 값이 달라지는지 확인하기 위한 타임스탬프
  return NextResponse.json({
    now: new Date().toISOString(),
  })
}

그리고 위 api를 호출하는 페이지를 만들었다. a는 10초 캐시, b는 매번 새 요청을 하도록 설정했다.

여기서 중요한 사항은 해당 작업은 server component에서만 가능하다.

// app/rsc-cache/page.tsx
import Link from 'next/link'
 
export default async function Page() {
  // A) 10초 캐시
  const a = await fetch('http://localhost:3000/api/clock', {
    next: { revalidate: 10 },
  }).then((r) => r.json())
 
  // B) 매번 새 요청
  const b = await fetch('http://localhost:3000/api/clock', {
    next: { revalidate: 0 },
  }).then((r) => r.json())
 
  return (
    <div style={{ padding: 20 }}>
      <p>a: {JSON.stringify(a, null, 2)}</p>
      <p>b: {JSON.stringify(b, null, 2)}</p>
      <br />
      <Link href="/rsc-cache">재요청</Link>
    </div>
  )
}

결과는 다음과 같다.

rsc-cache

재요청을 눌렀을 때 a는 10초 이내에 재요청하면 캐시된 값이 나오고, 10초가 지나면 새로운 값이 나오고, b는 재요청을 누를 때마다 매번 새로운 값이 나오는 것을 확인할 수 있었다.

<Link href={/rsc-cache?ts={Date.now()}}>재요청</Link> 처럼 query string을 다르게 주어도 결과는 똑같았다.

10초 캐시한 값은 여전히 10초 이내에 재요청하면 캐시된 값이 나오고, 10초가 지나면 새로운 값이 나오고, b는 재요청을 누를 때마다 매번 새로운 값이 나오는 것을 확인할 수 있었다.

// app/rsc-cache/page.tsx
import Link from 'next/link'
 
export default async function Page() {
  // A) 10초 캐시
  const a = await fetch('http://localhost:3000/api/clock', {
    next: { revalidate: 10 },
  }).then((r) => r.json())
 
  // B) 매번 새 요청
  const b = await fetch('http://localhost:3000/api/clock', {
    next: { revalidate: 0 },
  }).then((r) => r.json())
 
  return (
    <div style={{ padding: 20 }}>
      <p>a: {JSON.stringify(a, null, 2)}</p>
      <p>b: {JSON.stringify(b, null, 2)}</p>
      <br />
      <Link href={`/rsc-cache?ts=${Date.now()}`}>재요청</Link>
    </div>
  )
}

Server Fetching 한 데이터를 Context 에 주입하여 Client Component에서 사용하면 어떻게 될까?

일단 TimeContext, TimeProvider를 만들었다.

'use client'
import { createContext } from 'react'
export type TimeValue = { a: string; b: string }
export const TimeContext = createContext<TimeValue | null>(null)
 
export const TimeProvider = ({ initialValues, children }: { initialValues: TimeValue; children: React.ReactNode }) => {
  return <TimeContext.Provider value={{ a: initialValues.a, b: initialValues.b }}>{children}</TimeContext.Provider>
}

생성한 TimeProvider에 server fetching 한 데이터를 initialValues로 주입하였다. (Child 컴포넌트에서 TimeContext를 사용하여 값을 읽는다.)

import Link from 'next/link'
import { TimeProvider } from './_components/store'
import { Child } from './_components/Child'
 
export default async function Page() {
  // A) 10초 캐시
  const a = await fetch('http://localhost:3000/api/clock', {
    next: { revalidate: 10 },
  }).then((r) => r.json())
 
  // B) 매번 새 요청
  const b = await fetch('http://localhost:3000/api/clock', {
    next: { revalidate: 0 },
  }).then((r) => r.json())
 
  return (
    <div>
      <TimeProvider
        initialValues={{
          a: String(a.now),
          b: String(b.now),
        }}
      >
        <>
          <div style={{ padding: 20 }}>
            <p>a: {JSON.stringify(a)}</p>
            <p>b: {JSON.stringify(b)}</p>
            <br />
            <Link href={`/rsc-cache?ts=${Date.now()}`}>재요청</Link>
          </div>
          <div>
            <Child />
          </div>
        </>
      </TimeProvider>
    </div>
  )
}

Child 컴포넌트는 다음과 같다. 그리고 router.refresh() 버튼을 만들어서 눌렀을 때 Context 값이 바뀌는지 확인해보았다.

'use client'
 
import { useContext } from 'react'
import { TimeContext } from './store'
import { useRouter } from 'next/navigation'
 
export const Child = () => {
  const time = useContext(TimeContext)
  const router = useRouter()
 
  return (
    <div style={{ padding: 20 }}>
      <p>a : {time?.a}</p>
      <p>b : {time?.b}</p>
      <br />
      <button
        onClick={() => {
          router.refresh()
        }}
      >
        router.refresh() 버튼
      </button>
    </div>
  )
}

결과는 다음과 같다.

rsc-cache-context

router.refresh() 버튼을 눌렀을 때 a는 10초 이내에 재요청하면 캐시된 값이 나오고, b는 router.refresh() 버튼을 누를 때마다 매번 새로운 값이 나오는 것을 확인할 수 있었다. 즉, server fetching 한 데이터를 Context 에 주입하여 Client Component에서 사용해도 동일하게 동작하는 것을 확인할 수 있었다.

Context의 값을 Client Component의 state에 복사하여 사용해도 동일하게 동작할까?

기존 Child 컴포넌트에 const [stateA, setStateA] = useState(time?.a) 와 같이 context 값을 state에 복사하여 테스트 해보았다.

'use client'
 
import { useContext } from 'react'
import { TimeContext } from './store'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
 
export const Child = () => {
  const time = useContext(TimeContext)
  const router = useRouter()
 
  const [stateA, setStateA] = useState(time?.a)
 
  return (
    <div style={{ padding: 20 }}>
      <p>a : {time?.a}</p>
      <p>stateA : {stateA}</p>
      <p>b : {time?.b}</p>
      <br />
      <button
        onClick={() => {
          router.refresh()
        }}
      >
        router.refresh() 버튼
      </button>
    </div>
  )
}

결과는 다음과 같다.

rsc-cache-context-state

router.refresh() 버튼을 눌렀을 때 context 에서 복사한 stateA 값은 최초 렌더링 시점의 값에서 변하지 않는 것을 확인할 수 있었다.

state를 동기화 하려면 어떻게 해야할까?

useEffect를 사용하여 context 값이 바뀔 때 state를 동기화 하면 된다.

useEffect(() => {
  setStateA(time?.a)
}, [time?.a])

이 방법외에도 component의 key를 강제로 변경하여 컴포넌트를 리마운팅 할 수 있다. 하지만 이방법은 비용적 측면에서 권장되지 않는다.