Archive.sakamoto
RenderingNext.js

getStaticProps vs getServerSideProps

두 데이터 패칭 방식이 언제 HTML을 만들고, 왜 Prefetch 효과가 다른지 비교해보자!

Sakamoto·

1. 핵심 개념

둘의 차이는 단순히 "SSG냐 SSR이냐"가 아니다.

진짜 차이는 "HTML이 언제 만들어지고, 어떻게 캐싱되고, Prefetch가 가능한가" 다.

비유하자면, getStaticProps는 식당에서 미리 만들어둔 도시락이고, getServerSideProps주문받고 나서 요리하는 것이다. 도시락은 즉시 줄 수 있지만 누구에게나 같은 메뉴고, 주문 요리는 맞춤형이지만 기다려야 한다.

구분getStaticPropsgetServerSideProps
HTML 생성 시점빌드 타임요청마다
캐싱CDN 레벨거의 불가
Prefetch 효과매우 큼거의 없음
서버 비용거의 0트래픽만큼 증가
사용자별 데이터불가가능
SEO최상좋음

2. 왜 이렇게 설계하는가 ⭐

Pages Router를 처음 설계할 때, 모든 페이지를 SSR로 처리하면 단순하지만 트래픽이 몰릴수록 서버 비용이 폭증하고 응답 속도도 불안정해지는 문제가 있었다.

반대로 모든 페이지를 정적으로 만들면 빠르지만 로그인 상태, 실시간 데이터처럼 사람마다 다른 화면을 만들 수 없다.

그래서 두 방식으로 분리했다.

  • 공통 데이터 페이지 → 빌드 타임에 미리 만들어 CDN에 올려두기 (getStaticProps)
  • 개인화 데이터 페이지 → 요청 시점에 서버에서 생성하기 (getServerSideProps)

이 설계의 핵심 가치는 "필요한 곳에만 서버를 쓰는 것" 이다.
모든 페이지를 SSR로 돌리는 건 자원 낭비고, 모든 페이지를 정적으로 만드는 건 기능 제약이다.

 

💡
면접 tip
"둘의 차이가 뭔가요?" 질문에 "빌드 타임 vs 런타임"으로만 답하면 약하다.
"Prefetch 가능 여부와 CDN 캐싱 여부까지 다르기 때문에 성능과 서버 비용에서 체감 차이가 크다"고 답해야 깊이가 보인다.

3. 사용 원리 (코드 실행 흐름)

getStaticProps

// 이 코드는 빌드 타임에 딱 한 번 실행된다
export const getStaticProps = async () => {
  // [입력] 빌드 시점 — 요청 context 없음, 외부 API 호출
  const data = await fetchBlogPosts();
 
  // [처리] 서버에서 데이터 가공 → props로 직렬화
  return {
    props: { data },
    revalidate: 60, // ISR: 60초마다 백그라운드 재생성
  };
 
  // [출력] 정적 HTML 파일로 저장 → CDN에 올라감
  // 이후 요청은 서버를 아예 거치지 않음
};

getServerSideProps

// 이 코드는 유저가 URL에 접근할 때마다 실행된다
export const getServerSideProps = async (context) => {
  // [입력] 요청마다 context (쿠키, 쿼리, 헤더 등 포함)
  const { req, query } = context;
  const session = await getSession(req);
 
  // [처리] 요청 시점 서버에서 실행 → 사용자별 데이터 조회 가능
  const data = await fetchUserData(session.userId);
 
  return {
    props: { data },
    // [출력] 매번 HTML 새로 생성 → CDN 저장 안 됨
  };
};

4. 데이터 흐름

getStaticProps 흐름

  1. next build 실행 → getStaticProps 단 한 번 실행
  2. /posts/1 API 호출 → 데이터 수신
  3. posts/1/index.html 정적 파일 생성 → CDN 업로드
  4. 유저가 /posts/1 요청 → 서버 없이 CDN에서 HTML 즉시 응답
  5. revalidate: 60 설정 시 → 60초 후 백그라운드에서 재생성 (ISR)

getServerSideProps 흐름

  1. 유저가 /mypage 요청
  2. 브라우저 → Next.js 서버 도달
  3. getServerSideProps 실행 → req.cookies에서 세션 추출
  4. /api/user?id=123 호출 → 개인 데이터 수신
  5. HTML 생성 → 브라우저로 전달
  6. 다음 유저 요청 시 → 1번부터 다시 반복

5. 주요 옵션 (실무 기준)

항목getStaticPropsgetServerSideProps
revalidate✅ ISR 설정 가능❌ 없음
context.req❌ 접근 불가✅ 쿠키/헤더 접근
context.query❌ (getStaticPaths 필요)✅ URL 쿼리 직접 접근
notFound: true
redirect
Prefetch✅ 완벽 지원❌ 거의 없음
CDN 캐싱✅ 최강❌ 제한적

6. 실무 사용 패턴

패턴 1: 블로그 상세 페이지 — getStaticProps + ISR

// app: pages/posts/[id].tsx
 
// 상황: 콘텐츠는 자주 안 바뀌지만 SEO + Prefetch가 중요할 때
export const getStaticProps = async ({ params }) => {
  // [입력] 빌드 타임에 params.id로 특정 포스트 조회
  const post = await fetchPost(params.id);
 
  return {
    props: { post },
    revalidate: 3600, // 1시간마다 백그라운드 재생성
  };
};
 
export const getStaticPaths = async () => {
  const posts = await fetchAllPostIds();
  return {
    paths: posts.map((id) => ({ params: { id } })),
    fallback: 'blocking', // 빌드 후 새 포스트는 첫 요청 시 생성
  };
};
🔧
실무 point
fallback: 'blocking'을 쓰면 빌드 타임에 없던 페이지도 첫 요청 시 서버에서 생성 후 캐싱된다.
콘텐츠가 계속 추가되는 블로그나 상품 페이지에 적합하다.

패턴 2: 마이페이지 — getServerSideProps + 세션 체크

// pages/mypage.tsx
 
// 상황: 로그인한 사용자만 접근, 사람마다 다른 데이터
export const getServerSideProps = async (context) => {
  // [입력] 요청의 쿠키에서 세션 추출
  const session = await getSession(context.req);
 
  // [처리] 세션 없으면 로그인 페이지로 redirect
  if (!session) {
    return { redirect: { destination: '/login', permanent: false } };
  }
 
  const userData = await fetchUserData(session.userId);
  return { props: { userData } };
 
  // [출력] 사용자별 HTML — CDN 캐싱 안 됨
};

패턴 3: 성능 최적화 — getStaticProps + ISR 조합 (권장 패턴)

// 상황: 데이터 최신성도 필요하고, Prefetch 성능도 포기하기 싫을 때
export const getStaticProps = async () => {
  return {
    props: { ... },
    revalidate: 30, // SSG + ISR → 성능, 비용, 최신성 동시 해결
  };
};
🔧
실무 point
getServerSideProps를 쓰기 전에 "정말 요청마다 달라야 하는가?" 먼저 물어봐야 한다.
최신성이 문제라면 대부분 ISR로 해결 가능하고, 서버 비용을 크게 줄일 수 있다.

7. 자주 하는 실수 / 주의사항

잘못된 방법: 최신성이 필요하다는 이유로 모든 페이지에 getServerSideProps 사용

올바른 방법: revalidate 옵션으로 ISR 적용

🔎 이유: getServerSideProps는 요청마다 서버를 거치기 때문에 트래픽이 몰리면 서버 비용이 선형으로 증가한다. 데이터가 1분 단위로만 바뀌어도 된다면 revalidate: 60으로 ISR을 적용하면 성능과 비용을 모두 잡을 수 있다.


잘못된 방법: getStaticProps에서 context.req로 쿠키 접근 시도

export const getStaticProps = async (context) => {
  const session = getSession(context.req); // ❌ context.req가 없음
};

올바른 방법: 쿠키/세션이 필요하면 getServerSideProps 사용

🔎 이유: getStaticProps는 빌드 타임에 실행되므로 요청 객체(req)가 존재하지 않는다. 런타임 요청 정보가 필요한 로직은 반드시 getServerSideProps에서 처리해야 한다.


잘못된 방법: getServerSideProps에서 revalidate 옵션 사용

export const getServerSideProps = async () => {
  return {
    props: { ... },
    revalidate: 60, // ❌ 무시됨
  };
};

올바른 방법: revalidategetStaticProps에서만 동작

🔎 이유: revalidate는 정적 파일을 백그라운드에서 재생성하는 ISR 옵션이다. getServerSideProps는 정적 파일을 만들지 않기 때문에 이 옵션 자체가 의미 없고 무시된다.


8. 한 줄 요약

getStaticProps는 빌드 타임에 HTML을 만들어 CDN에 올려 Prefetch까지 가능한 구조,
getServerSideProps는 요청마다 서버에서 HTML을 생성해 사용자별 데이터를 처리하는 구조.
데이터가 공통이면 Static, 사람마다 달라야 하면 ServerSide — 그 전에 ISR로 해결 가능한지 먼저 검토한다.