개요
리팩토링 계획 중 getStaticProps, getServerSideProps를 사용해 서버 측에서 데이터를 요청하는 방법을 해보려고 하던 중 난관에 봉착했다.
처음 마주한 문제는 getStaticProps, getServerSideProps의 경우 page에서 요청해야 된다는 내용이었다.
해당 오류에 맞게 어서 최상단의 Page에서 요청을 했으나 이번엔 다른 문제가 발생했다.

Next 13 버전부터는 해당 함수들을 지원하지 않고 다른 데이터 요청 방법을 사용해야한다고 한다.
뭐가 문제인지를 확인하고, 적절한 data fetching 방식을 알아보자.
getStaticProps 사용 불가?
아래 사진을보면 Next 공식문서에서 Pages Router 방식과 App Router에서 사용되는 방식의 차이를 보여준다.
그래서 일부 블로그에서 getStaticProps, getServerSideProps는 더 이상 지원하지 않는다 하는 곳이 많아 헷갈렸는데
App디렉터리 구조가 아닌 Pages 디렉터리 구조를 사용 중이라면 여전히 사용이 가능한 것 같다.


getStaticProps
pages 디렉토리 구조를 사용하고 있지는 않지만, app router 방식과의 차이를 알기 위해 배웠다.
페이지에서 getStaticProps 함수를 export하면, Next.js는 해당 함수를 통해 반환된 props를 사용해 빌드 시 페이지를 미리 렌더링 한다.
import type { InferGetStaticPropsType, GetStaticProps } from 'next'
type Repo = {
name: string
stargazers_count: number
}
export const getStaticProps = (async (context) => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const repo = await res.json()
return { props: { repo } }
}) satisfies GetStaticProps<{
repo: Repo
}>
export default function Page({
repo,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return repo.stargazers_count
}
getStaticProps의 구성요소는 다음과 같다.
- context :getStaticProps 호출 시 동적으로 전달되는 데이터로 다음 속성들이 포함될 수 있다.
* params : getStaticPaths를 사용해 동적 라우팅이 설정된 경우 URL 경로에 해당하는 파라미터를 포함한다.
* preview : 프리뷰 모드를 활성화하면 true로 설정되고 초안 데이터나 특별 콘텐츠를 노출 할 때 사용된다.
* previewData :프리뷰 모드에서 전달되는 추가 데이터가 담긴다.
- return values : getStaticProps함수가 반환하는 객체로 props, redirect, notFound같은 속성들을 포함한다.
* props : 페이지 컴포넌트에서 사용되는 키-값 쌍을 의미하고 이는 Json.Stringufy로 직렬화가 가능한 데이터 여야한다.
* revalidate : 기본 값은 false로, 페이지가 다시 생성하도록 하는 시간을 의미한다. (단위 초)
* redirect : 리디렉션의 경로인 destination과 영구적임을 알려주는 permanent속성으로 이루어진 객체이다.
그렇다면 getStaticProps로 페이지를 불러오는 경우는 언제일까?
getStaticProps는 HTML파일과 JSON 파일을 생성해 정적 사이트를 미리 만들기에 이 파일들은 CDN에 캐싱되어 사용자의 페이지 요청 시 빠르게 렌더링 된다.
이런 장점들을 생각 했을 때 다음과 같은 상황에 사용된다
- 유저의 요청 전 빌드 시간에 렌더링 되는 페이지에 데이터가 필요한 경우
- Ex) 블로그의 게시물 목록, e-commerce의 제품목록처럼 사전 정의된 데이터 기반 페이지 생성 시
- Ex) 블로그의 게시물 목록, e-commerce의 제품목록처럼 사전 정의된 데이터 기반 페이지 생성 시
- SEO를 위해 페이지를 미리 렌더링 해야 하고, 빠른 속도를 요구하는 경우
- 데이터가 공개적으로 캐시 될 수 있는 경우
- Ex) 사용자 별 데이터가 아닌 경우 모든 사용자가 동일한 데이터를 보는 경우 CDN에서 캐시해 빠르게 제공이 가능하다.
getStaticProps server-side에서 사용하기
getStaticProps는 오직 server-side에서만 동작하기에 브라우저에 제공되는 JS 번들에 포함되지 않는다.
따라서, 데이터 베이스 쿼리 요청 또한 브라우저로의 전송 없이 가능하다.
만약 정적 페이지 생성과 동적 api routes용도로 사용해야 하는 경우 다음과 데이터 요청 비동기 함수를 공유하도록 정의한다.
// lib 폴더에 저장해 getStaticProps와 API routes에서 공유해서 사용되도록 함
export async function loadPosts() {
const res = await fetch('https://.../posts/')
const data = await res.json()
return data
}
import { loadPosts } from '../lib/load-posts'
// 해당 함수는 서버사이드에서 작동
export async function getStaticProps() {
// api 폴더로의 데이터 요청 없이 getStaticProps에서 즉시 요청
const posts = await loadPosts()
// 반환된 Props는 페이지 컴포넌트로 전달 됨.
return { props: { posts } }
}
이렇게 하는 경우 getStaticProps와 API route에서 동일한 데이터를 가져올 때 별도의 API를 중복호출 하는 것을 방지하고 성능 개선이 가능해진다.
만약 API routes를 통해 데이터를 요청하는 경우가 없는 경우 getStaticProps에서 즉시 데이터를 요청해도 무방하다.
정적 HTML, JSON 생성
getStaicProps를 사용해 페이지를 빌드 타임에 정적 생성 시 HTML뿐만 아니라 JSON파일도 생성한다.
이 JSON 파일은 클라이언트 사이드에서의 페이지 전환 시 사용된다.
클라이언트 사이드의 페이지 전환인 next/link, next/router를 사용해 페이지 전환 시 getStaticProps는 다시 호출되지 않고 이미 빌드된 JSON 파일이 사용되어 해당 파일은 미리 계산된 데이터를 포함하기에 빠르게 페이지 전환이 되곤 한다.
또한, ISG(증분 정적 생성)을 사용하는 경우 getStaticProps는 백그라운드에서 실행되어 페이지의 JSON 파일을 생성하고 이 과정에서 여러 번 동일한 요청은 발생할 수 있지만 이는 의도된 동작으로 일정 주기로 갱신 또는 새로 생성이 가능하다.
getStaticProps 사용하려면?
getStaticProps를 사용하려면 먼저 위에서 말한 것처럼 page 디렉터리 구조를 사용해야 한다.
page에서만 exported 되어야 하며 page가 아닌 파일인 _app, _document, _error에선 사용이 불가하다.
마지막으로, getStaticProps를 사용할 땐 standalone function으로 사용해야 한다. 페이지 컴포넌트의 프로퍼티로 추가 시 올바르게 작동하지 않는다.
getServerSideProps
getServerSideProps는 페이지가 요청될 때마다 데이터를 가져와 렌더링 하는 함수이다. 페이지 요청 시마다 서버에서 데이터를 동적으로 처리해 결과를 실시간으로 렌더링 하도록 한다.
getStaticProps와 마찬가지로 SSR 되며, Page 컴포넌트에서 exporting 해 사용이 가능하다.
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
type Repo = {
name: string
stargazers_count: number
}
export const getServerSideProps = (async () => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const repo: Repo = await res.json()
// props를 통해 데이터를 전달한다.
return { props: { repo } }
}) satisfies GetServerSideProps<{ repo: Repo }>
export default function Page({
repo,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<main>
<p>{repo.stargazers_count}</p>
</main>
)
}
getStaticProps와의 차이점이라면, 실시간 데이터 처리 및 동적 데이터 사용 목적에 맞춰진 함수라는 점.
언제 사용할까
getServerSideProps는 다음 같은 상황에 사용된다.
1. 페이지에 표시할 데이터가 사용자 요청에 따라 달라지는 경우
ex) 로그인한 사용자에 따른 데이터 변경
2. 실시간 업데이트가 필요한 페이지의 경우
3. SEO 최적화가 필요한 경우 : 마찬가지로 서버에서 렌더링 된 HTML을 클라이언트로 보내기에 SEO에 유리하게 작용한다.
에러 핸들링
getServerside 내부에서 에러가 발생하면 pages/500.js를 보여준다. 개발모드에선 이 파일은 사용되지 않고, 개발 error overlay가 대신에 보인다.
app 디렉터리에서의 data fetching - server
data fetching 방식이 기존에 내가 사용하던 방식이라 익숙하다.
다만, 아래 코드에선 RSC에서의 데이터 요청으로 useEffect 같은 응답 상태 관리 없이 데이터요청만 하는 코드이다.
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
fetch로부터 받아오는 response의 경우 기본적으로 캐싱되지 않는다.
해당 경로에서 Dynamic api가 사용되지 않는다면, 이전에 본 getStaticProps와 마찬가지로 next build 단계에서 정적페이지로 미리 렌더링 된다.
데이터의 갱신을 원하는 경우, ISR의 사용을 통해 업데이트될 수 있다.
prerendering을 원치 않는 경우
export const dynamic = 'force-dynamic'
위 코드를 명시해 주면 되고 만약 cookies, headers 또는 searchParams 같은 page props를 통해 값을 불러오는 경우엔 자동으로 동적으로 렌더링 되며 위의 force-dynamic을 지정해주지 않아도 된다.
app 디렉터리에서의 data fetching - client
기본적으로 NEXT.js에서 권장하는 방식은 위의 server-side에서 fetch 하는 방법이지만, 클라이언트 사이드에서 요청하는 경우 다음과 같이 요청한다.
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://api.vercel.app/blog')
const data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
코드와 같이 useEffect에 fetch함수를 넣어 요청하거나 인기 있는 리액트 라이브러리인 SWR, React Query방식을 권장한다.
동일 데이터 재사용하기(캐싱)
Next.js에선 generateMetadata, generateStaticParams 같은 api 사용 시 여러 함수에서 재사용이 가능하다.
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
const res = await fetch(`https://api.vercel.app/blog/${id}`, {
cache: 'force-cache',
})
const post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())
return posts.map((post: Post) => ({
id: String(post.id),
}))
}
이를 통해 같은 데이터를 여러 번 요청하지 않고 효율적으로 처리가 가능하다.
Next.js에서 fetch를 사용하면 데이터를 메모이제션해 캐시 할 수 있고, force-cache 옵션을 추가 시 동일한 URL에 대한 요청은 여러 번 호출해도 실제 요청은 한 번만 실행되고 그 이후엔 캐싱된 데이터를 반환한다.
다만, 기존과 다르게 Next 15부턴 기본 캐시 동작이 no-store이기에 캐싱된 값을 받으려면 따로 지정을 해야 한다.
내 코드 변경하기
기존 내 코드 중 블로그 포스트들을 가져오는 DailyPostContainer에 대한 fetch 방식에 변화를 주고 결과의 차이를 보자.
기존 코드
'use client';
const DailyPostContainer = () => {
const [postData, setPostData] = useState<LandingPostTypes[]>([]);
useEffect(() => {
const fetchPost = async () => {
const response = await callGet('/api/main/dailyPost');
response.isSuccess && setPostData(response.result);
};
fetchPost();
}, []);
return (
<div className="flex-col-center w-full gap-y-5">
<div className="w-full flex items-end justify-between px-3 py-4 border-b border-[#cbcaca]">
<p className="text-2xl font-semibold">{MAIN_CONTENTS_TITLE[0]}</p>
</div>
{postData.length === 0 ? (
<div className="w-full h-[748px]">
<NoneContent />
</div>
) : (
<div className="w-full flex-wrap flex gap-x-12 gap-y-5">
{postData.map((post) => (
<DailyPost key={post.id} post={post} />
))}
</div>
)}
</div>
);
};
export default DailyPostContainer;
NextJS 문서에서 나온 클라이언트 사이드 요청 방식으로 useEffect 내부에서 fetch(해당 코드에선 callGet이 fetch 메서드별로 작성한 함수)로 요청해 useState로 응답을 관리하는 방식이다.
네트워크 탭 결과 확인

물론 헤더에 포함된 요청까지 포함해서 요청 20건, 1.8mb, 11.3mb의 리소스, 417밀리 초가 완료되는데 걸리고 로드는 324밀리 초가 걸렸다
변경된 코드
const DailyPostContainer = async () => {
const response = await callGet(`${SERVER_URL}/api/blogs/landings/latest`);
const postData: LandingPostTypes[] = response.isSuccess
? response.result
: [];
return (
<div className="flex-col-center w-full gap-y-5">
<div className="w-full flex items-end justify-between px-3 py-4 border-b border-[#cbcaca]">
<p className="text-2xl font-semibold">{MAIN_CONTENTS_TITLE[0]}</p>
</div>
{postData.length === 0 ? (
<div className="w-full h-[748px]">
<NoneContent />
</div>
) : (
<div className="w-full flex-wrap flex gap-x-12 gap-y-5">
{postData.map((post) => (
<DailyPost key={post.id} post={post} />
))}
</div>
)}
</div>
);
};
export default DailyPostContainer;
다음과 같이 클라이언트 사이드 컴포넌트가 아닌 서버 사이드 컴포넌트에서 api를 요청하도록 방식에 변화를 주며, use client 또한 없앴다.
네트워크 탭 결과 확인

요청 19건, 1.8mb, 11.2mb의 리소스, 475밀리 초가 완료되는데 걸리고 로드는 332밀리 초가 걸렸다
비슷하다 생각이 들어 생각해 보니 header에 있는 유저 부분은 클라이언트 사이드에서 불러오기에 해당 요청시간까지 포함하기에 결과적으로 거의 유사하게 불러오는 것을 볼 수 있었다. 헤더 부분을 제외하고 요청해 보았다.
최종 네트워크 탭 결과 확인


확실히 현저한 차이가 발생했고 CSR(좌)의 경우 완료가 470밀리 초가 걸렸지만 SSR(우)의 경우 168초 밖에 안 걸린 것을 볼 수 있다.
추가로, 큰 차이로 네트워크 탭에 우측의 경우 dailypost에 해당하는 파일은 받아오지 않는 것을 볼 수 있다.
이는 브라우저에서의 요청은 클라이언트 요청으로, 서버에서 이미 받아와 prerendering 한 dailyPost의 경우 가져올 필요가 없는 것이다.
결론
지금까지 난 내가 Next JS를 쓰면서 단순히 useState, useEffect로 데이터를 요청하면서 SSR이 잘 되고 있겠지 나태한 생각을 가졌다.
그러나 실제로 뜯어보니, 하나도 되지 않고 대부분의 데이터들을 useEffect와 함께 요청하는 방식을 사용해 Next의 이점을 활용하지 못하고 있었다.
나머지 요청도 정적인 페이지의 경우 해당 방식으로 코드를 수정해서 좋은 성과를 얻도록 하고, 앞으로 NextJS를 활용하는 프로젝트에서도 이와 같이 상황에 맞는 data Fetching 방식과 캐싱 전략을 사용하도록 해야겠다.
참고
https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props
Data Fetching: getStaticProps | Next.js
Fetch data and generate static pages with `getStaticProps`. Learn more about this API for data fetching in Next.js.
nextjs.org
'코딩 > Flex프로젝트' 카테고리의 다른 글
재테크 블로그 프로젝트 (12) - 리팩토링 [Next.js caching ] (0) | 2025.01.05 |
---|---|
재테크 블로그 프로젝트 (10) - 리팩토링 [폰트 감소 번들사이즈 최종] (0) | 2025.01.02 |
재테크 블로그 프로젝트 (9) - 리팩토링 [번들 사이즈 줄이기] (0) | 2024.12.29 |
재테크 블로그 프로젝트 (8) - 리팩토링 [번들 사이즈 측정] (1) | 2024.12.28 |
재테크 블로그 프로젝트 (7) Lighthouse 성능 측정 및 개선 (0) | 2024.11.24 |
개요
리팩토링 계획 중 getStaticProps, getServerSideProps를 사용해 서버 측에서 데이터를 요청하는 방법을 해보려고 하던 중 난관에 봉착했다.
처음 마주한 문제는 getStaticProps, getServerSideProps의 경우 page에서 요청해야 된다는 내용이었다.
해당 오류에 맞게 어서 최상단의 Page에서 요청을 했으나 이번엔 다른 문제가 발생했다.

Next 13 버전부터는 해당 함수들을 지원하지 않고 다른 데이터 요청 방법을 사용해야한다고 한다.
뭐가 문제인지를 확인하고, 적절한 data fetching 방식을 알아보자.
getStaticProps 사용 불가?
아래 사진을보면 Next 공식문서에서 Pages Router 방식과 App Router에서 사용되는 방식의 차이를 보여준다.
그래서 일부 블로그에서 getStaticProps, getServerSideProps는 더 이상 지원하지 않는다 하는 곳이 많아 헷갈렸는데
App디렉터리 구조가 아닌 Pages 디렉터리 구조를 사용 중이라면 여전히 사용이 가능한 것 같다.


getStaticProps
pages 디렉토리 구조를 사용하고 있지는 않지만, app router 방식과의 차이를 알기 위해 배웠다.
페이지에서 getStaticProps 함수를 export하면, Next.js는 해당 함수를 통해 반환된 props를 사용해 빌드 시 페이지를 미리 렌더링 한다.
import type { InferGetStaticPropsType, GetStaticProps } from 'next'
type Repo = {
name: string
stargazers_count: number
}
export const getStaticProps = (async (context) => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const repo = await res.json()
return { props: { repo } }
}) satisfies GetStaticProps<{
repo: Repo
}>
export default function Page({
repo,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return repo.stargazers_count
}
getStaticProps의 구성요소는 다음과 같다.
- context :getStaticProps 호출 시 동적으로 전달되는 데이터로 다음 속성들이 포함될 수 있다.
* params : getStaticPaths를 사용해 동적 라우팅이 설정된 경우 URL 경로에 해당하는 파라미터를 포함한다.
* preview : 프리뷰 모드를 활성화하면 true로 설정되고 초안 데이터나 특별 콘텐츠를 노출 할 때 사용된다.
* previewData :프리뷰 모드에서 전달되는 추가 데이터가 담긴다.
- return values : getStaticProps함수가 반환하는 객체로 props, redirect, notFound같은 속성들을 포함한다.
* props : 페이지 컴포넌트에서 사용되는 키-값 쌍을 의미하고 이는 Json.Stringufy로 직렬화가 가능한 데이터 여야한다.
* revalidate : 기본 값은 false로, 페이지가 다시 생성하도록 하는 시간을 의미한다. (단위 초)
* redirect : 리디렉션의 경로인 destination과 영구적임을 알려주는 permanent속성으로 이루어진 객체이다.
그렇다면 getStaticProps로 페이지를 불러오는 경우는 언제일까?
getStaticProps는 HTML파일과 JSON 파일을 생성해 정적 사이트를 미리 만들기에 이 파일들은 CDN에 캐싱되어 사용자의 페이지 요청 시 빠르게 렌더링 된다.
이런 장점들을 생각 했을 때 다음과 같은 상황에 사용된다
- 유저의 요청 전 빌드 시간에 렌더링 되는 페이지에 데이터가 필요한 경우
- Ex) 블로그의 게시물 목록, e-commerce의 제품목록처럼 사전 정의된 데이터 기반 페이지 생성 시
- Ex) 블로그의 게시물 목록, e-commerce의 제품목록처럼 사전 정의된 데이터 기반 페이지 생성 시
- SEO를 위해 페이지를 미리 렌더링 해야 하고, 빠른 속도를 요구하는 경우
- 데이터가 공개적으로 캐시 될 수 있는 경우
- Ex) 사용자 별 데이터가 아닌 경우 모든 사용자가 동일한 데이터를 보는 경우 CDN에서 캐시해 빠르게 제공이 가능하다.
getStaticProps server-side에서 사용하기
getStaticProps는 오직 server-side에서만 동작하기에 브라우저에 제공되는 JS 번들에 포함되지 않는다.
따라서, 데이터 베이스 쿼리 요청 또한 브라우저로의 전송 없이 가능하다.
만약 정적 페이지 생성과 동적 api routes용도로 사용해야 하는 경우 다음과 데이터 요청 비동기 함수를 공유하도록 정의한다.
// lib 폴더에 저장해 getStaticProps와 API routes에서 공유해서 사용되도록 함
export async function loadPosts() {
const res = await fetch('https://.../posts/')
const data = await res.json()
return data
}
import { loadPosts } from '../lib/load-posts'
// 해당 함수는 서버사이드에서 작동
export async function getStaticProps() {
// api 폴더로의 데이터 요청 없이 getStaticProps에서 즉시 요청
const posts = await loadPosts()
// 반환된 Props는 페이지 컴포넌트로 전달 됨.
return { props: { posts } }
}
이렇게 하는 경우 getStaticProps와 API route에서 동일한 데이터를 가져올 때 별도의 API를 중복호출 하는 것을 방지하고 성능 개선이 가능해진다.
만약 API routes를 통해 데이터를 요청하는 경우가 없는 경우 getStaticProps에서 즉시 데이터를 요청해도 무방하다.
정적 HTML, JSON 생성
getStaicProps를 사용해 페이지를 빌드 타임에 정적 생성 시 HTML뿐만 아니라 JSON파일도 생성한다.
이 JSON 파일은 클라이언트 사이드에서의 페이지 전환 시 사용된다.
클라이언트 사이드의 페이지 전환인 next/link, next/router를 사용해 페이지 전환 시 getStaticProps는 다시 호출되지 않고 이미 빌드된 JSON 파일이 사용되어 해당 파일은 미리 계산된 데이터를 포함하기에 빠르게 페이지 전환이 되곤 한다.
또한, ISG(증분 정적 생성)을 사용하는 경우 getStaticProps는 백그라운드에서 실행되어 페이지의 JSON 파일을 생성하고 이 과정에서 여러 번 동일한 요청은 발생할 수 있지만 이는 의도된 동작으로 일정 주기로 갱신 또는 새로 생성이 가능하다.
getStaticProps 사용하려면?
getStaticProps를 사용하려면 먼저 위에서 말한 것처럼 page 디렉터리 구조를 사용해야 한다.
page에서만 exported 되어야 하며 page가 아닌 파일인 _app, _document, _error에선 사용이 불가하다.
마지막으로, getStaticProps를 사용할 땐 standalone function으로 사용해야 한다. 페이지 컴포넌트의 프로퍼티로 추가 시 올바르게 작동하지 않는다.
getServerSideProps
getServerSideProps는 페이지가 요청될 때마다 데이터를 가져와 렌더링 하는 함수이다. 페이지 요청 시마다 서버에서 데이터를 동적으로 처리해 결과를 실시간으로 렌더링 하도록 한다.
getStaticProps와 마찬가지로 SSR 되며, Page 컴포넌트에서 exporting 해 사용이 가능하다.
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
type Repo = {
name: string
stargazers_count: number
}
export const getServerSideProps = (async () => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const repo: Repo = await res.json()
// props를 통해 데이터를 전달한다.
return { props: { repo } }
}) satisfies GetServerSideProps<{ repo: Repo }>
export default function Page({
repo,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<main>
<p>{repo.stargazers_count}</p>
</main>
)
}
getStaticProps와의 차이점이라면, 실시간 데이터 처리 및 동적 데이터 사용 목적에 맞춰진 함수라는 점.
언제 사용할까
getServerSideProps는 다음 같은 상황에 사용된다.
1. 페이지에 표시할 데이터가 사용자 요청에 따라 달라지는 경우
ex) 로그인한 사용자에 따른 데이터 변경
2. 실시간 업데이트가 필요한 페이지의 경우
3. SEO 최적화가 필요한 경우 : 마찬가지로 서버에서 렌더링 된 HTML을 클라이언트로 보내기에 SEO에 유리하게 작용한다.
에러 핸들링
getServerside 내부에서 에러가 발생하면 pages/500.js를 보여준다. 개발모드에선 이 파일은 사용되지 않고, 개발 error overlay가 대신에 보인다.
app 디렉터리에서의 data fetching - server
data fetching 방식이 기존에 내가 사용하던 방식이라 익숙하다.
다만, 아래 코드에선 RSC에서의 데이터 요청으로 useEffect 같은 응답 상태 관리 없이 데이터요청만 하는 코드이다.
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
fetch로부터 받아오는 response의 경우 기본적으로 캐싱되지 않는다.
해당 경로에서 Dynamic api가 사용되지 않는다면, 이전에 본 getStaticProps와 마찬가지로 next build 단계에서 정적페이지로 미리 렌더링 된다.
데이터의 갱신을 원하는 경우, ISR의 사용을 통해 업데이트될 수 있다.
prerendering을 원치 않는 경우
export const dynamic = 'force-dynamic'
위 코드를 명시해 주면 되고 만약 cookies, headers 또는 searchParams 같은 page props를 통해 값을 불러오는 경우엔 자동으로 동적으로 렌더링 되며 위의 force-dynamic을 지정해주지 않아도 된다.
app 디렉터리에서의 data fetching - client
기본적으로 NEXT.js에서 권장하는 방식은 위의 server-side에서 fetch 하는 방법이지만, 클라이언트 사이드에서 요청하는 경우 다음과 같이 요청한다.
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://api.vercel.app/blog')
const data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
코드와 같이 useEffect에 fetch함수를 넣어 요청하거나 인기 있는 리액트 라이브러리인 SWR, React Query방식을 권장한다.
동일 데이터 재사용하기(캐싱)
Next.js에선 generateMetadata, generateStaticParams 같은 api 사용 시 여러 함수에서 재사용이 가능하다.
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
const res = await fetch(`https://api.vercel.app/blog/${id}`, {
cache: 'force-cache',
})
const post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())
return posts.map((post: Post) => ({
id: String(post.id),
}))
}
이를 통해 같은 데이터를 여러 번 요청하지 않고 효율적으로 처리가 가능하다.
Next.js에서 fetch를 사용하면 데이터를 메모이제션해 캐시 할 수 있고, force-cache 옵션을 추가 시 동일한 URL에 대한 요청은 여러 번 호출해도 실제 요청은 한 번만 실행되고 그 이후엔 캐싱된 데이터를 반환한다.
다만, 기존과 다르게 Next 15부턴 기본 캐시 동작이 no-store이기에 캐싱된 값을 받으려면 따로 지정을 해야 한다.
내 코드 변경하기
기존 내 코드 중 블로그 포스트들을 가져오는 DailyPostContainer에 대한 fetch 방식에 변화를 주고 결과의 차이를 보자.
기존 코드
'use client';
const DailyPostContainer = () => {
const [postData, setPostData] = useState<LandingPostTypes[]>([]);
useEffect(() => {
const fetchPost = async () => {
const response = await callGet('/api/main/dailyPost');
response.isSuccess && setPostData(response.result);
};
fetchPost();
}, []);
return (
<div className="flex-col-center w-full gap-y-5">
<div className="w-full flex items-end justify-between px-3 py-4 border-b border-[#cbcaca]">
<p className="text-2xl font-semibold">{MAIN_CONTENTS_TITLE[0]}</p>
</div>
{postData.length === 0 ? (
<div className="w-full h-[748px]">
<NoneContent />
</div>
) : (
<div className="w-full flex-wrap flex gap-x-12 gap-y-5">
{postData.map((post) => (
<DailyPost key={post.id} post={post} />
))}
</div>
)}
</div>
);
};
export default DailyPostContainer;
NextJS 문서에서 나온 클라이언트 사이드 요청 방식으로 useEffect 내부에서 fetch(해당 코드에선 callGet이 fetch 메서드별로 작성한 함수)로 요청해 useState로 응답을 관리하는 방식이다.
네트워크 탭 결과 확인

물론 헤더에 포함된 요청까지 포함해서 요청 20건, 1.8mb, 11.3mb의 리소스, 417밀리 초가 완료되는데 걸리고 로드는 324밀리 초가 걸렸다
변경된 코드
const DailyPostContainer = async () => {
const response = await callGet(`${SERVER_URL}/api/blogs/landings/latest`);
const postData: LandingPostTypes[] = response.isSuccess
? response.result
: [];
return (
<div className="flex-col-center w-full gap-y-5">
<div className="w-full flex items-end justify-between px-3 py-4 border-b border-[#cbcaca]">
<p className="text-2xl font-semibold">{MAIN_CONTENTS_TITLE[0]}</p>
</div>
{postData.length === 0 ? (
<div className="w-full h-[748px]">
<NoneContent />
</div>
) : (
<div className="w-full flex-wrap flex gap-x-12 gap-y-5">
{postData.map((post) => (
<DailyPost key={post.id} post={post} />
))}
</div>
)}
</div>
);
};
export default DailyPostContainer;
다음과 같이 클라이언트 사이드 컴포넌트가 아닌 서버 사이드 컴포넌트에서 api를 요청하도록 방식에 변화를 주며, use client 또한 없앴다.
네트워크 탭 결과 확인

요청 19건, 1.8mb, 11.2mb의 리소스, 475밀리 초가 완료되는데 걸리고 로드는 332밀리 초가 걸렸다
비슷하다 생각이 들어 생각해 보니 header에 있는 유저 부분은 클라이언트 사이드에서 불러오기에 해당 요청시간까지 포함하기에 결과적으로 거의 유사하게 불러오는 것을 볼 수 있었다. 헤더 부분을 제외하고 요청해 보았다.
최종 네트워크 탭 결과 확인


확실히 현저한 차이가 발생했고 CSR(좌)의 경우 완료가 470밀리 초가 걸렸지만 SSR(우)의 경우 168초 밖에 안 걸린 것을 볼 수 있다.
추가로, 큰 차이로 네트워크 탭에 우측의 경우 dailypost에 해당하는 파일은 받아오지 않는 것을 볼 수 있다.
이는 브라우저에서의 요청은 클라이언트 요청으로, 서버에서 이미 받아와 prerendering 한 dailyPost의 경우 가져올 필요가 없는 것이다.
결론
지금까지 난 내가 Next JS를 쓰면서 단순히 useState, useEffect로 데이터를 요청하면서 SSR이 잘 되고 있겠지 나태한 생각을 가졌다.
그러나 실제로 뜯어보니, 하나도 되지 않고 대부분의 데이터들을 useEffect와 함께 요청하는 방식을 사용해 Next의 이점을 활용하지 못하고 있었다.
나머지 요청도 정적인 페이지의 경우 해당 방식으로 코드를 수정해서 좋은 성과를 얻도록 하고, 앞으로 NextJS를 활용하는 프로젝트에서도 이와 같이 상황에 맞는 data Fetching 방식과 캐싱 전략을 사용하도록 해야겠다.
참고
https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props
Data Fetching: getStaticProps | Next.js
Fetch data and generate static pages with `getStaticProps`. Learn more about this API for data fetching in Next.js.
nextjs.org
'코딩 > Flex프로젝트' 카테고리의 다른 글
재테크 블로그 프로젝트 (12) - 리팩토링 [Next.js caching ] (0) | 2025.01.05 |
---|---|
재테크 블로그 프로젝트 (10) - 리팩토링 [폰트 감소 번들사이즈 최종] (0) | 2025.01.02 |
재테크 블로그 프로젝트 (9) - 리팩토링 [번들 사이즈 줄이기] (0) | 2024.12.29 |
재테크 블로그 프로젝트 (8) - 리팩토링 [번들 사이즈 측정] (1) | 2024.12.28 |
재테크 블로그 프로젝트 (7) Lighthouse 성능 측정 및 개선 (0) | 2024.11.24 |