chart.cyh-lab.com을 만들면서 주식 데이터를 외부 API로 받아오는 부분을 작업했다. Next.js App Router는 fetch를 자동으로 캐싱해준다고 해서 별생각 없이 썼는데, 예상과 전혀 다르게 동작해서 한참 헤맸다. 그 과정을 기록해둔다.
문제 상황
서버 컴포넌트에서 주식 시세 데이터를 fetch로 가져왔다.
// app/stock/[ticker]/page.tsx
export default async function StockPage({ params }: { params: { ticker: string } }) {
const res = await fetch(`https://api.example.com/stock/${params.ticker}`)
const data = await res.json()
return <StockChart data={data} />
}처음에는 잘 되는 것 같았다. 근데 실시간 데이터라 페이지를 새로 고침해도 값이 안 바뀌는 걸 뒤늦게 발견했다. Next.js가 fetch 결과를 캐싱해버려서 한번 받아온 데이터를 계속 쓰고 있었던 거다.
Next.js App Router의 fetch 캐싱 기본 동작
Pages Router 때는 getStaticProps, getServerSideProps로 캐싱 전략을 명시적으로 정했다. App Router에서는 fetch 자체에 캐싱이 내장되어 있다.
기본값이 cache: 'force-cache'라는 게 핵심이다. 즉, 따로 설정하지 않으면 빌드 타임에 fetch한 결과를 계속 쓴다.
// 기본값 — 한 번 받으면 계속 캐싱
fetch('https://api.example.com/data')
// 캐싱 안 함 — 매 요청마다 새로 받아옴
fetch('https://api.example.com/data', { cache: 'no-store' })
// n초마다 재검증 (ISR)
fetch('https://api.example.com/data', { next: { revalidate: 60 } })주식 데이터는 실시간이 아닌 5~10분 단위 갱신이어도 괜찮았기 때문에, revalidate를 300으로 설정했다.
const res = await fetch(`https://api.example.com/stock/${params.ticker}`, {
next: { revalidate: 300 }, // 5분마다 재검증
})Route Segment Config으로 페이지 단위 설정
fetch마다 옵션을 붙이는 게 번거로우면 페이지 파일 상단에 Route Segment Config을 쓸 수 있다.
// app/stock/[ticker]/page.tsx
export const revalidate = 300 // 이 페이지의 모든 fetch에 적용
export default async function StockPage({ params }) {
const res = await fetch(`https://api.example.com/stock/${params.ticker}`)
// ...
}이렇게 하면 해당 페이지의 모든 fetch에 동일한 revalidate가 적용된다. 여러 API를 호출하는 페이지라면 이 방법이 훨씬 깔끔하다.
개발 환경에서 캐싱이 다르게 동작한다
한 가지 더 헷갈렸던 게 있다. 로컬에서 next dev로 개발할 때는 캐싱이 거의 적용되지 않는다. 매 요청마다 새로 fetch한다. 그래서 개발할 때 문제가 없다가 배포하고 나서야 캐싱 이슈가 드러났다.
프로덕션 동작을 로컬에서 확인하려면 next build && next start로 빌드 후 실행해야 한다. 귀찮더라도 배포 전에 한 번은 확인하는 게 낫다.
결론이라기보다 주의사항
App Router에서 fetch 캐싱은 강력하지만, 기본값이 force-cache라는 걸 인지하고 쓰지 않으면 실시간 데이터를 다루는 페이지에서 의도하지 않은 동작이 생긴다. 특히 Pages Router 경험이 있으면 더 헷갈리기 쉽다. 처음 App Router로 넘어올 때 이 부분을 제대로 읽고 시작했으면 삽질이 줄었을 텐데.