- Published on
NextJs 13 이후 - DATA FETCHING
- Authors
- Name
- piano cat
NextJs 13이후로 app 라우터를 사용하는 방식이 추가되었고, 현재 기존에 사용하던 page 라우터 방식과 app 라우터 방식, 두가지 방식으로 나누어졌다.
- app 라우터 를 사용하는 방식
- page 라우터를 사용하는 방식
새로 추가된 app 라우터를 사용하는 방식을 알아보자
NextJs에서 데이터를 요청하는 방법
NextJs에서 데이터를 요청하는 방법은 4가지가 있다.
- 서버에서
fetch
API를 사용하는 방법 - 서버에서
third-party libraries
를 사용하는 방법 - 클라이언트에서
Route Handler
를 거처 사용하는 방법 - 클라이언트에서
third-party libraries
를 사용하는 방법
fetch
API를 사용하는 방법
서버에서 NextJs는 fetch API 를 확장하여 서버에 요청할때 캐싱, 재검증 기능을 설계할수 있도록 만들었다.
리액트 컴포넌트 트리가 렌더링 될때마다 fetch API가 자동으로 memoize 되도록 되어있다.
Server Action
, Route Handler
안의 서버 컴포넌트에서 fetch
API를 사용할수 있다.
예시)
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
Caching Data
캐싱하여 저장된 데이터는 매번 데이터를 재 요청하지 않아도 되도록 돕는다.
기본적으로 NextJs는 서버에 요청해서 응답받는 값을 자동적으로 캐싱한다. 이는 빌드 시 또는 요청데이터를 가져올때 캐싱하고 각 데이터 요청에서 재사용할 수 있음을 의미한다.
// 'force-cache' is the default, and can be omitted
fetch('https://...', { cache: 'force-cache' })
POST
요청 또한 자동적으로 캐싱된다. 다만 Route Handler 안에 POST
메서드가 있지 않을경우엔 캐싱되지 않는다.
Revalidating Data
재검증은 마지막으로 요청한 데이터와 캐싱된 데이터와 일치하는지 검증한다. 이것은 나의 데이터를 변경하거나 마지막 최신정보를 보여줄때 유용하다.
캐싱된 데이터는 두가지 방법으로 재검증 할 수있다.
- 시간 기반의 재검증
일정시간이 지나면 데이터를 자동으로 재검증 한다. 이는 자주 변경되지않고 최신성이 중요하지않는 데이터에 유용하다.
- 요청 기반의 재검증
이벤트를 기반으로 데이터를 수동으로 재검증한다. 요청형 재검증에서는 태그기반 또는 경로 기반 접근방식을 사용하여 데이터 그룹을 한번에 재 검증할 수 있다. 이는 가능한 빨리 최신 데이터를 표시하려는 경우에 유용하다. 예) headless CMS 콘텐츠가 업데이트가 되는 경우
시간기반 재검증
일정 간격으로 데이터를 재검증하려면 리소스의 캐시 수명을 설정하는 next.revalidate
옵션을 사용할수 있다.
예)
fetch('https://...', { next: { revalidate: 3600 } })
요청 기반의 재검증
데이터는 요청시 서버액션에서 경로(revalidatePath
) , 또는 Route Handler 에서 태그(revalidateTag
) 방식을 사용할 수 있다.
revalidateTag
NextJs에는 경로 전반에 걸쳐 요청을 무효화하기 위한 캐시 태깅 시스템이 있다.
fetch
API를 사용할때fetch
하나 이상의 태그로 캐시항목에 태그를 지정할 수 있는 옵션이 있다.- 그런다음 호출하여
revalidateTag
해당 태그와 관련된 모든 항목의 유효성을 다시 검사할 수 있다.
예를들어 fetch
요청에 collection
태그를 추가한다.
// app/page.tsx
export default async function Page() {
const res = await fetch('https://...', { next: { tags: ['collection'] } })
const data = await res.json()
// ...
}
그런다음 서버작업을 호출하여 fetch
태그가 지정된 이 호출을 다시검증할 수있다.
// app/action.ts
'use server'
import { revalidateTag } from 'next/cache'
export default async function action() {
revalidateTag('collection')
}
오류 처리 및 재검증
데이터 재검증을 시도하는 동안 오류가 발생하면 마지막으로 성공적으로 생성된 데이터가 캐시되어 계속 제공된다. 다음 후속 요청에서 NextJs는 데이터 재검증을 다시 시도한다.
데이터 캐싱 선택 해제
fetch
API에 다음과 같은 경우 캐시되지 않습니다.
catch: 'no-store'
요청에 추가한다.revalidate: 0
옵션을 요청에 추가한다.fetch
요청이 해당 메서드를 사용하는 라우터 핸들러 내부에 있다.- headers, cookies 가 사용된 이후에
fetch
요청이 올때 - 라우터 핸들러에
const dynamic = 'force-dynamic'
옵션을 사용할때 - 라우터 핸들러에
fetchCache
옵션은 기본적으로 캐시를 건너 뛰도록 구성된다.
fetch
요청에 적용 (추천)
개별 개별 fetch
요청에 적용하는 방법은 아래와 같다. 이와 같이 설정하면 모든 요청에 캐싱이 적용이 되지않고 매 요청을 가져오게한다
//layout.js | page.js
fetch('https://...', { cache: 'no-store' })
fetch
요청에 적용
다중 한 라우트(page.js | layout.js) 내에서 여러 fetch
요청에 적용하는 방법은 Route Segment Config 속성을 입력하여 반영할 수 있다.
//layout.js | page.js
const fetchCache = 'force-no-store'
하지만 개별 fetch 요청에 적용하는 것을 추천한다. 이를 통해 캐싱 동작을 보다 세부적으로 제어 할 수 있기 때문이다.
third-party libraries
를 사용하는 방법
서버에서 Route Segment Config를 사용하여 해당 요청의 캐싱 및 재검증 동작을 구성할 수 있다.
예)
// layout.tsx | page.tsx | route.ts
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
export const maxDuration = 5
export default function MyComponent() {}
데이터가 캐싱되는지 여부는 경로 세그먼트가 정적으로 렌더링되는지 동적으로 렌더링되는지 에 따라 달라진다. 세그먼트가 정적(기본값) 인 경우 요청 출력은 경로 세그먼트 일부로 캐시되고 재검증된다. 세그먼트가 동적인 경우 요청 출력은 캐시되지않으며, 세그먼트가 렌더링 될때 모든 요청에서 다시 가져온다.
예)
//app/utils.ts
import { cache } from 'react'
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id })
return item
})
// app/item/[id]/layout.tsx
import { getItem } from '@/utils/get-item'
export const revalidate = 3600 // revalidate the data at most every hour
export default async function Layout({ params: { id } }: { params: { id: string } }) {
const item = await getItem(id)
// ...
}
// app/item/[id]/page.tsx
import { getItem } from '@/utils/get-item'
export const revalidate = 3600 // revalidate the data at most every hour
export default async function Page({ params: { id } }: { params: { id: string } }) {
const item = await getItem(id)
// ...
}
revalidate = 3600
옵션은 데이터가 최대 매 시간마다 캐시되고 재검증 된다. 즉, 함수가 두번 호출되더라도 데이터 베이스에 대한 getItem
함수는 한번만 수행된다.
Route Handler
를 거처 사용하는 방법
클라이언트에서 클라이언트 구성 요소에서 데이터를 가져와야 하는 경우는 클라이언트에서 Route Handler를 호출할 수 있다. Route Handler는 서버에서 실행되고 데이터를 클라이언트에 반환한다. 이는 API 토큰과 같은 민감한 정보를 클라이언트에 노출하고 싶지않을때 유용하다.
third-party libraries
를 사용하는 방법
클라이언트에서 SEO 성능 등을 위해서 서버에서 데이터를 가져오는게 바람직하지만 클라인언트에서 데이터를 가져오는게 필요하다면 SWR, React Query 를 사용하는 것을 권장한다. 이 라이브러리들은 자신의 API 에 memoizing 요청, 캐싱, 재검증 그리고 mutating 데이터 기능을 제공하기 때문이다.
추가로 react는 use
라는 훅을 도입했지만 fetch
API를 use
함수 안에 사용하는 것은 추천하지는 않는다. 이유는 클라이언트 컴포넌트가 여러번 리렌더링 될수 있기때문이다. use
는 promise
를 수락하는 wait와 유사한 개념이다. use
는 구성요소, hook, suspense와 호환되는 방식으로 함수에서 반환된 promise
를 처리한다.
예)
'use client'
import { Post } from '@/lib/types'
import { use } from 'react'
const getPosts = async (): Promise<Post[]> => {
const data = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await data.json()
return posts
}
export default function ClientPosts() {
const posts = use(getPosts())
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
정리
- fetch API 확장
- Server Component
- Client Component
- Static Data Fetching (SSG)
- Dynamic Data Fetching (SSR)
fetch API 확장
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
Server Component
import { Post } from '@/lib/types'
const getPosts = async (): Promise<Post[]> => {
const data = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await data.json()
return posts
}
export default async function Posts() {
const posts = await getPosts()
console.log(posts)
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Client Component
'use client'
import { Post } from '@/lib/types'
import { use } from 'react'
const getPosts = async (): Promise<Post[]> => {
const data = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await data.json()
return posts
}
export default function ClientPosts() {
const posts = use(getPosts())
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Static Data Fetching (SSG)
fetch('https://jsonplaceholder.typicode.com/posts', {
cache: 'force-cache',
})
Dynamic Data Fetching (SSR)
fetch('https://jsonplaceholder.typicode.com/posts', {
cache: 'no-cache',
})