Frontend/Next.js

[Next.js/Page Router] 2. SSR (Server Side Rendering)

ayeongjin 2025. 2. 2. 21:09

SSR (Server-Side Rendering)


 

1. 기존 리액트 앱의 동작 방식

  1. 불러온 데이터를 보관할 State 생성
  2. 데이터 페칭 함수 생성
  3. 컴포넌트 마운트 시점에 fetchData 호출
  4. 데이터 로딩중일때의 예외처리
💡 문제점
클라이언트에서 데이터를 요청하고 로딩하는 데 오랜 시간이 걸림초기 접속 시, 화면이 렌더링되기 전에 필요한 데이터를 받아와야 하기 때문에 UX가 저하됨

 

2. SSR (Server-Side Rendering)이란?

  • 서버에서 미리 HTML을 생성하여 클라이언트에 전달하는 방식
  • 요청이 들어올 때마다 서버에서 렌더링을 수행하고, 완성된 HTML을 반환함
  • SEO 최적화 및 초기 로딩 속도 개선에 유리함

 

3. SSR의 동작 방식

  1. 클라이언트가 페이지를 요청함
  2. 서버에서 데이터 요청 및 페이지 렌더링을 수행함
  3. 완성된 HTML을 클라이언트에 전달함
  4. 클라이언트에서 React가 Hydration(재활성화)하여 인터랙티브한 요소를 추가함

 

4. React App과 Next App 비교

 


 

5. Next.js에서 SSR 사용하기

Next.js에서는 getServerSideProps 함수를 사용하여 SSR을 구현할 수 있음.

 

예제: SSR을 이용한 도서 목록 페이지

 

📌 getServerSideProps 함수 활용

  • 넥스트에서 약속된 이름의 함수로 만들어서 내보내면 해당 페이지는 SSR로 동작하도록 자동으로 설정된다.
  • 컴포넌트보다 먼저 실행되어서, 컴포넌트에 필요한 데이터를 불러오는 함수
// index.tsx
import SearchableLayout from "@/components/searchable-layout";
import style from "./index.module.css";
import { ReactNode } from "react";
import books from "@/mock/books.json";
import BookItem from "@/components/book-item";
import { InferGetServerSidePropsType } from "next";
import fetchBooks from "@/lib/fetch-books";
import fetchRandomBooks from "@/lib/fetch-random-books";

export const getServerSideProps = async () => {
  // 데이터 병렬 호출 (Promise.all을 사용하여 비동기 요청을 동시에 실행)
  const [allBooks, recoBooks] = await Promise.all([
    fetchBooks(),
    fetchRandomBooks(),
  ]);

  return {
    props: {
      allBooks,
      recoBooks,
    },
  };
};

// InferGetServerSidePropsType : getServerSideProps함수의 반환값 타입을 자동으로 추론해주는 타입
export default function Home({
  allBooks,
  recoBooks,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        {recoBooks.map((book) => (
          <BookItem key={book.id} {...book} />
        ))}
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        {allBooks.map((book) => (
          <BookItem key={book.id} {...book} />
        ))}
      </section>
    </div>
  );
}

Home.getLayout = (page: ReactNode) => {
  return <SearchableLayout>{page}</SearchableLayout>;
};

❌ 데이터 직렬 호출

fetchBooks() 완료 후 → fetchRandomBooks() 실행

  • 각 요청이 순차적으로 처리되어 전체 로딩 시간이 증가함
fetchBooks() -> fetchRandomBooks() -> 렌더링

✅ 데이터 병렬 호출

  • Promise.all()을 사용하여 두 개의 요청을 동시에 실행함
    • Promise.all : 인수로 전달한 배열 안에 있는 모든 비동기 함수를 동시에 실행시키는 메서드
  • 전체 데이터 로딩 속도가 단축됨
fetchBooks() + fetchRandomBooks() (동시 실행) -> 렌더링

 

📌 fetchBooks 함수 (API 호출)

// fetch-books.ts

import { BookData } from "@/types";

// 비동기로 반환하기 때문에 비동기 결과를 의미하는 promise의 제네릭으로 bookdata타입 불러와서 정의
export default async function fetchBooks(q?: string): Promise<BookData[]> {
  let url = `http://localhost:12345/book`;
  if (q) {
    url += `/search?q=${q}`; // 쿼리스트링을 사용하여 검색 기능 구현
  }

  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error();
    }
    return await response.json();
  } catch (err) {
    console.error(err);
    return [];
  }
}

 

📌 검색 기능을 포함한 SSR 구현 (쿼리스트링 활용)

// search/index.tsx

import SearchableLayout from "@/components/searchable-layout";
import { ReactNode } from "react";
import BookItem from "@/components/book-item";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import fetchBooks from "@/lib/fetch-books";

// 쿼리스트링으로 전달된 검색어 읽어오기
// context: 현재 브라우저로부터 받은 모든 요청이 들어있음
export const getServerSideProps = async (
  context: GetServerSidePropsContext
) => {
  const q = context.query.q; // 쿼리스트링을 이용하여 검색어를 받아옴
  const books = await fetchBooks(q as string);

  return {
    props: { books },
  };
};

export default function Page({
  books,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <div>
      {books.map((book) => (
        <BookItem key={book.id} {...book} />
      ))}
    </div>
  );
}

Page.getLayout = (page: ReactNode) => {
  return <SearchableLayout>{page}</SearchableLayout>;
};

 

 


6. SSR의 장점과 단점

장점

  1. SEO 최적화: 검색 엔진이 HTML을 읽을 수 있어 검색 노출이 유리함
  2. 빠른 초기 렌더링: 서버에서 미리 렌더링된 HTML을 제공하여 사용자 경험 개선
  3. 데이터 최신성 유지: 요청 시마다 최신 데이터를 받아옴

단점

  1. 서버 부하 증가: 모든 요청에서 서버가 렌더링을 수행해야 하므로 성능 이슈 발생 가능
  2. 캐싱 어려움: 동적인 페이지 특성상 캐싱이 쉽지 않음
  3. 긴 응답 시간: 서버에서 렌더링하는 과정이 필요하여 응답 속도가 느려질 수 있음

 


 

7. 배운점

  • SSR은 SEO 및 초기 로딩 속도 개선에 강점이 있는 렌더링 방식
  • Next.js에서 getServerSideProps를 활용하여 손쉽게 SSR을 구현 가능
  • 데이터 요청 시 병렬 호출 (Promise.all) 을 활용하여 성능 최적화 필요
  • 서버 부하와 캐싱 문제를 고려하여 CSR, SSG와 혼합하여 사용하는 것이 중요함