getServerSideProps는 페이지 요청마다 다시 실행된다
- CS
- Next.js
들어가며
"getServerSideProps는 서버에서 데이터를 가져와 props로 넘기는 함수"라는 설명은 완벽한 설명은 아니다. 프로젝트에서 이 함수를 써왔지만, 정확히 언제, 몇 번 실행되는지 설명하려면 막히는 지점이 있었다.
특히 헷갈렸던 것은 새로고침 없이 next/link로 페이지를 이동할 때였다. 클릭 한 번으로 이동했으니 getServerSideProps는 최초 접속 시점에만 실행되고, 그 뒤로는 클라이언트가 알아서 화면을 채운다고 생각했다. 하지만 Network 탭을 보면 서버로 향하는 요청이 하나 새로 뜨는 것을 확인할 수 있었다. 게다가 그 요청의 경로는 주소창에 보이는 URL과는 전혀 달랐다.
GET /_next/data/{buildId}/repo.json이 어긋남을 풀기 위해, getServerSideProps가 실제로 언제, 어떤 조건에서 실행되는지 처음부터 단계별로 따라가 보기로 했다.
페이지 요청마다 다시 실행된다
getServerSideProps는 Pages Router의 페이지 파일에서 export하는 함수다. 이 함수 안의 코드는 브라우저가 아니라 Next.js 서버에서 실행된다.
// pages/index.tsx
import type { 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()
return { props: { repo } }
}) satisfies GetServerSideProps<{ repo: Repo }>브라우저가 /repo를 요청하면 Next.js 서버는 getServerSideProps를 실행해 GitHub API에서 저장소 정보를 가져오고, 그 결과를 props로 페이지 컴포넌트에 전달해 HTML을 만든다. 잠시 뒤 같은 /repo로 요청이 한 번 더 들어오더라도, 직전 결과를 재사용하지 않고 getServerSideProps를 처음부터 다시 실행한다.
getStaticProps라면 빌드 시점에 딱 한 번 실행돼 결과를 정적 파일로 남기고, 이후 요청은 그 파일을 그대로 재사용한다. 하지만 getServerSideProps에는 재사용할 결과가 없으므로, 매 요청이 곧 매번의 실행이다.

링크로 이동해도 다시 실행된다
브라우저에서 next/link를 클릭하거나 next/router로 페이지를 이동하면 전체 문서를 새로고침하지 않는다. 대신 Next.js의 클라이언트 라우터가 현재 React 애플리케이션을 유지한 채, 이동할 페이지에 필요한 데이터를 서버에 요청한다. 앞서 “들어가며” 섹션에서 본 요청이 바로 이 요청이다. 이 과정에서도 서버는 해당 페이지의 getServerSideProps를 그대로 실행하고, 완성된 HTML 문서 대신 클라이언트 라우터가 페이지를 갱신하는 데 필요한 데이터를 JSON으로 반환한다.

요청의 경로에는 /_next/data와 빌드 ID가 붙는데, resolvedUrl은 여기서 /_next/data 접두사를 제거하고 원래 쿼리 값까지 포함해 정규화한 값이다. getServerSideProps 내부에서 이 값을 확인하면 사용자가 이동하려던 원래 페이지 URL(/repo)을 그대로 확인할 수 있다. 요청 URL을 기준으로 로직을 작성할 때는 /_next/data 같은 내부 경로를 직접 파싱하기보다 resolvedUrl을 쓰는 편이 적절하다.
요청이 클릭한 순간에야 나타나는 이유는 next/link의 prefetch 방식에 있다. Next.js 공식 문서는 뷰포트에 들어온 Link가 Static Generation 페이지라면 데이터까지 함께 prefetch 하지만, 서버 렌더링 라우트의 데이터는 클릭했을 때만 가져온다고 명시한다. getStaticProps를 쓰는 페이지는 빌드 시점에 이미 고정된 데이터가 있어 함께 prefetch 해도 서버 부하가 없다. 반면, getServerSideProps는 요청마다 서버 로직을 다시 실행해야 하므로 뷰포트에 걸린 링크마다 미리 실행하면 서버 부하만 늘어나기 때문으로 보인다.

context는 요청 시점에 확정된다
getServerSideProps가 매 요청마다 다시 실행돼야 하는 이유는 context 매개변수에 담긴 값들이 요청이 들어온 순간에만 확정되기 때문이다. 현재 로그인 여부는 빌드 시점은 물론 직전 요청 시점에도 알 수 없다. 이번 요청에 실린 쿠키를 확인해야만 판단할 수 있다.
// 요청 쿠키에서 인증 토큰 확인
export async function getServerSideProps({ req }) {
const token = req.cookies.authToken
if (!token) {
return { redirect: { destination: '/login', permanent: false } }
}
const user = await getUserByToken(token)
if (!user) {
return { redirect: { destination: '/login', permanent: false } }
}
return { props: { user: { id: user.id, name: user.name } } }
}같은 페이지, 같은 코드인데도 요청마다 다른 쿠키가 실려 오기 때문에 결과가 달라진다. 동적 라우트도 마찬가지다. /posts/123을 요청하면 params.id에 "123"이 담기는데, 이 값도 요청이 서버에 닿기 전까지는 존재하지 않는다. params, 쿠키, 쿼리스트링처럼 요청이 들어온 뒤에야 확인할 수 있는 정보를 써야 한다면, getServerSideProps는 그 요청에 맞는 값을 매번 다시 계산해야 한다.

정리하며
getServerSideProps는 페이지가 아니라 요청에 묶인 함수다. 브라우저가 처음 /repo에 접속하든, next/link로 다른 페이지에서 넘어오든, 요청이 서버에 닿을 때마다 새로 실행된다. “들어가며” 섹션에서 Network 탭에 떠 있던 /_next/data 요청도 마찬가지다. 매번 새로 도착하는 요청에서만 알 수 있는 정보를 쓰기 위해, 이 함수는 그 요청이 올 때마다 처음부터 다시 실행된다.