// SSR 방식

import { ReactNode, useEffect } from "react";
import SearchableLayout from "@/components/SearchableLayout";
import { InferGetServerSidePropsType } from "next";
import BookItem from "@/components/BookItem";
import { GetServerSidePropsContext } from "next";
import fetchBooks from "@/library/fetchBooks";
import { BookData } from "@/types";

 export const getServerSideProps = async (
   context: GetServerSidePropsContext
 ) => {
   const q = context.query.q;
   const books = await fetchBooks(q as string);

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

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

     </div>
   );
 }

 

// SSG 방식

import { ReactNode, useState, useEffect } from "react";
import SearchableLayout from "@/components/SearchableLayout";
import BookItem from "@/components/BookItem";
import fetchBooks from "@/library/fetchBooks";
import { BookData } from "@/types";
import { useRouter } from "next/router";


export default function SearchPage() {
  const [books, setBooks] = useState<BookData[]>([]);

  const router = useRouter();
  const q = router.query.q;

  const fetchSearchResult = async () => {
    const data = await fetchBooks(q as string);
    setBooks(data);
  };

  useEffect(() => {
    if(q) {
      fetchSearchResult()
    }
  }, [q]);

  return (
    <div>
      {books.map((book) => (
        <BookItem key={book.id} {...book} />
      ))}
    </div>
  );
}

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

 

 

GetServerSidePropsContext

  • Next.js에서 서버 측 렌더링(SSR) 시 사용되는 컨텍스트 객체로, 요청과 관련된 정보를 제공
  • 서버 측에서만 사용 가능

특징

  • getServerSideProps 함수의 첫 번째 매개변수로 제공됨
  • URL의 쿼리 파라미터, 동적 경로 매개변수, 요청/응답 객체 등 서버 측 정보를 포함
  • 주로 SSR에서 데이터를 페칭하거나 URL에 따라 조건부 렌더링을 구현할 때 사용

 

 

 

useRouter()

  • React Hook으로, Next.js의 클라이언트 측에서 라우팅 정보를 가져오고 라우팅을 제어하기 위해 사용
  • 클라이언트 측에서만 사용 가능

특징

  • 브라우저 환경에서 실행되며, 현재 경로, 쿼리 파라미터, 라우팅 관련 메서드 등을 제공
  • 주로 페이지 컴포넌트나 클라이언트 사이드 렌더링(CSR)에서 사용

 

 

 

 

 

 

 

Next.js에서 SSR은 대표적인 장점이다.

그러나 페이지 요청이 있을 때 매번 정적 HTML을 생성/렌더링 하고, JS 번들을 생성하고, 필요하다면 백엔드 서버 데이터를 불러온다.

 

이 때 호출하는 데이터가 오래 걸리거나 크기가 크다면, 그만큼 대기 시간이 발생해 유저 경험을 저해할 수 있다.

시시각각 변하는 데이터가 아니고서는 매번 다시 렌더링하는 것은 비효율적이다.

 

 

이 때 SSG(Static Site Generation)을 적용해줄 수 있다.

  SSR (Server-Side Rendering) SSG (Static Site Generation)
HTML 생성 시점 해당 페이지 요청 시 빌드 시점
서버 부하 요청할 때마다 리소스 사용 서버 부하 없음
(CDN(Content Delivery Network에서 정적 파일 제공)
사용 함수 getServerSideProps getStaticProps
타입 InferGetServerSidePropsType
<typeof getServerSideProps>
InferGetStaticPropsType
<typeof getStaticProps>
데이터 항상 최신 빌드 시점 기준 데이터
사용자 맞춤 대시보드, 검색 결과 페이지 정적 콘텐츠, 자주 변하지 않는 데이터, 블로그, 회사 소개 페이지

 

 

 

 

 

사용 방법은 아래와 같다.

// SSR

export const getServerSideProps = async () => {
  const [allData, randomData] = await Promise.all([
    fetchData(),
    fetchRandomData(),
  ]);

  return {
    props: {
      allData,
      randomData,
    },
  };
};


export default function ExamplePage({
  allData,
  randomData,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  
  return (
  //후략
// SSG

export const getStaticProps = async () => {
  const [allData, randomData] = await Promise.all([
    fetchData(),
    fetchRandomData(),
  ]);

  return {
    props: {
      allData,
      randomData,
    },
  };
};


export default function ExamplePage({
  allData,
  randomData,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  
  return (
  //후략

 

 

 

이 상태 그대로 개발 모드에서 확인한다면 SSG가 적용되지 않는다.

SSG는 빌드 시점에 정적 페이지를 생성하기 때문이다.

 

 

프로젝트 빌드 후 다시 프로덕션 모드로 확인해보자.

npm run build // 빌드
npm run start // 프로덕션 모드

 

이 과정을 따르면 해당 ExamplePage는 SSG가 적용되어 요청한 백엔드 데이터를 미리 불러와 적용시킨 정적 페이지로 동작하게 된다.

 

 

 

 

빌드 후 로그 살펴보기

빌드 후 로그 끝부분에 다음과 같은 내용이 포함되어 있다.

각 페이지가 SSR, SSG 중 어떤 방식으로 처리되는지 보여준다.

 

○, ●, ƒ 이 세가지 기호에 주목하자.

 

 

● (SSG) : 조금 전 살펴본 SSG 방식이다.

ƒ (Dynamic) : SSR. 페이지가 요청될 때마다 다이나믹하게 렌더링 된다.

 

 

그렇다면 Static은 뭘까?

SSR, SSG 중 어떤 방식도 명시하지 않은 페이지의 경우, 자동으로 빌드 시 정적 페이지로 생성한다.

 

 

 

그럼 SSG가 Next.js의 디폴트라는 뜻인가?

조금 다르다.

SSG는 해당 페이지의 정적 페이지에 API 요청, 데이터베이스 쿼리, 파일 읽기 등 필요한 모든 처리를 포함시킨다.

말하자면 A 데이터를 뿌려주는 페이지라면, A 데이터가 포함된 페이지가 보여진다는 것이다.

 

그러나 Static의 경우, 데이터 호출 처리 없이 단순한 HTML 정적 페이지를 생성한다.

 

 

 

 

 

따라서 각 페이지마다 자신이 필요한 기능 및 사용자 경험에 맞추어 SSR, SSG, Static 중 선택하고 적용해야 한다.

 

 

 

 

 

 

 

Next.js 강의를 들으며 페이지 라우팅을 학습하던 도중, <Link> 태그와 useRouter() 사용법을 배웠다.

 

이 때 의문이 들었다.

 

 

 

아래처럼 쓰면 결국 똑같지 않나?
왜 두가지 방식이 존재할까?

//_app.tsx

import "@/styles/globals.css";
import type { AppProps } from "next/app";
import Link from "next/link";
import { useRouter } from "next/router";

export default function App({ Component, pageProps }: AppProps) {
  
  const router = useRouter()
  
  const onClickButton = () => {
    router.push("/home")
  };


  return (
    <>
      <header>
        <Link href={"/home"}>	// 이 태그도 홈으로 이동이고,
          홈
        </Link>
       
        <div>
          <button onClick={onClickButton}>홈</button> // 이 버튼도 동일하게 홈으로 이동한다.
        </div>
      </header>
      
      <Component {...pageProps} />
    </>
  );
}

 

 

 

우선 a, Link 태그의 차이점을 살펴보자.

 

<a>

  • 브라우저가 완전히 새로운 HTML 문서를 가져온다.
  • 기존 페이지의 상태(State)를 초기화하고, 새로운 페이지를 렌더링한다.
  • 추가적인 서버 요청이 있기 때문에 성능 저하가 발생할 수 있다.
  • 외부 라우팅에 사용한다.

 

<Link>

  • Next.js에서 제공하는 컴포넌트로, 클라이언트 측에서 즉시 라우팅한다.
  • 자바스크립트 상태(State)가 유지된다.
  • 클라이언트 측에서 새로고침 없이, 필요한 데이터만 로드하여 속도가 빠르다.
  • 내부 라우팅에 사용한다.
  • Pre-Fetching이 일어난다.
    • Pre-Fetching: 현재 페이지 외에 이동할 가능성이 있는 페이지를 사전에 미리 불러오는 것. (페이지 이동속도 개선)

 

 

 


 

 

 

 

위 특징을 읽어보면 컴포넌트 간 이동을 하는 경우에는 Link 태그가 더 많은 장점을 가진 것으로 보인다.

그렇다면 맨 처음 질문처럼, useRouter()와는 어떤 차이가 있는지 알아보자.

 

 

useRouter()

  • 프로그래밍적 페이지 전환이 필요할 때 사용한다.
    • 프로그래밍적 페이지 전환?
      • 사용자가 직접 링크를 클릭하지 않아도 로직에 따라 페이지가 이동되는 경우.
      • 로그인 후 리다이렉트, 특정 조건이나 이벤트에 따라 페이지 전환이 되는 경우
  • push, replace, back 등 옵션을 통해 브라우저 히스토리를 조작할 수 있다.
  • 동적 URL 생성 등 더 유연한 라우팅이 가능하다.
  • 기본적으로 Pre-Fetching이 일어나지 않는다. (별도 처리로 가능)

 

 

 

따라서, 컴포넌트 간 페이지 전환은  Link를 사용하되, 어떤 로직이나 조건이 필요한 경우는 useRouter가 적합하다.

 

 

 

 

 


 

 

 

 

 

 

위에 잠시 언급된 Pre-Fetching에 대해 알아보자.

 

Pre-Fetching이란?

 

우선 Next.js가 페이지를 렌더링하는 방식은 아래와 같다.

  1. 프로젝트를 빌드하면 서버에서 페이지 별로 정적 HTML파일JavaScript 번들을 생성한다.
  2. 요청된 URL과 일치하는 페이지 컴포넌트의 HTML을 렌더링하고, 클라이언트에 보낸다.
  3. 이후 필요한 기능을 Javascript 번들로 전달하고 Hydration하여 동적 기능을 추가한다.
  4. 다른 페이지로 전환하는 경우, 해당 페이지에 필요한 HTML을 서버에서 추가로 전달한다.
  5. 이동한 페이지에 해당하는 JavaScript 번들을 받아와 Hydration을 진행한다.
  6. 반복...

 

 

 

그러나 페이지 전환이 잦을 경우, 이 과정이 짧은 시간동안 반복되면 서버 요청이 많아지고, 효율적이지 않다.

 

 

 

가령, 아래와 같은 페이지가 있다.

home/
├── menu/      
├── search/    
├── option/    
└── error/

 

  • menu와 search 페이지로의 이동 가능성이 높다.
  • option으로의 이동은 적다.
  • error은 특정 조건을 만족시켜야만 이동하는데, 이동 가능성이 높다. (useRouter() 사용)

 

그렇다면 자주 방문하게 되는 페이지를 미리 받아와두면 더욱 빠른 페이지 전환이 되겠구나!

 

그것이 Pre-Fetching이다.

  • 브라우저가 미리 데이터를 가져와 캐싱하거나, 리소스(스크립트, 이미지 등)를 다운로드해 사용자의 다음 행동에 대비한다.
  • 보통 다음과 같이 사용된다.
    • 최적화된 빌드 파일(e.g., bundle.js)을 대상으로 동작.
    • 특정 경로나 리소스를 미리 요청하고 캐시에 저장.
    • 사용자가 해당 리소스가 필요한 시점에 즉시 제공.

 

 

 

특히, Link 태그는 디폴트로 Pre-Fetching 기능이 적용되어있다.

따라서 자주 이동하는 페이지는 Pre-Fetching을 적용시키고, 그렇지 않는 페이지는 이를 방지함으로써 효율을 증대시킬 수 있다.

 

 

 

그럼 위에서 제시한 예시 구조는 아래처럼 생각할 수 있다.

home/
├── menu/      // Pre-fetching 적용
├── search/    // Pre-fetching 적용
├── option/    // Pre-fetching 해제
└── error/     // Pre-fetching 적용
import "@/styles/globals.css";
import { useEffect } from "react";
import type { AppProps } from "next/app";
import Link from "next/link";
import { useRouter } from "next/router";

export default function App({ Component, pageProps }: AppProps) {

  const router = useRouter();
  
  const onClickButton = () => {	// error 페이지로 이동하는 조건
    router.push("/error");
  };

  useEffect(() => {	// prefetch 강제 적용
    router.prefetch("/error"); 
  }, []);

  return (
    <>
      <header>
        <Link href={"/menu"}>
          메뉴
        </Link>
        <Link href={"/search"}>
          검색
        </Link>
        <Link href={"/option"} prefetch={false}> // prefetch 해제
          옵션
        </Link>
        <div>
          <button onClick={onClickButton}>
          	error페이지로 이동
          </button>
        </div>
      </header>
      
      <Component {...pageProps} />
    </>
  );
}

 

 

 

 

 


 

 

 

 

 

참고로 Pre-Fetching 관련 수정은 개발 환경에서 곧바로 확인할 수 없다.

npm run dev

 

개발 환경에서는 저장하더라도 리소스가 번들로 합쳐지지 않고, 각 파일이 별도로 요청되기 때문이다.

또한 브라우저가 Pre-Fetching한 파일이 변경될 가능성이 높고, 이에 따라 캐싱과 Pre-Fetching의 효율이 모두 떨어진다.

 

 

그러므로 꼭 빌드를 하고 Prod 환경에서 Pre-Fetching 여부를 확인해보자:)

개발자 도구 네트워크 탭에서 쉽게 확인할 수 있다.

// ctrl + D 로 서버 중지
npm run build
npm run start

 

 

 

 

Next.js의 최대 장점은 SSR(Server-Side-Rendering)을 통한 빠른 렌더링이 된다는 점이다.

 

 

그래서 필자는 Next.js를 쓰면 전부 Server Component이고, 빠르게 동작할 것이라고 짐작했다.

 

 

그러나 Server Component와 Client Component의 기능과 목적이 각각 다르고, 경우에 따라 선택해야 한다는 것을 알았다.

 

왜일까?

 

 

우선 각각의 기능을 살펴보자.

 

Client Component란?

 

  • 브라우저에서 클라이언트 측에서 실행되는 컴포넌트.
  • React의 기존 방식처럼 상태 관리, 이벤트 핸들링브라우저 상호작용에 필요한 기능을 제공.
  • 컴포넌트 최상단에 "use client" 지시자를 추가해야 클라이언트 컴포넌트로 동작한다.
'use client'

 

Server Client란?

  • 서버에서 렌더링되는 컴포넌트.
  • 클라이언트에 최적화된 HTML을 전달하여 초기 로드 시간을 단축하고 SEO 성능을 향상시킨다.
  • 클라이언트에 불필요한 JavaScript를 줄이고 서버에서 데이터를 처리할 수 있다.
  • React 훅(useState, useEffect, useReducer 등)을 사용할 수 없다.

 

 

그게 무슨 말인가...

말하자면 정적 컨텐츠는 Server, 동적 컨텐츠는 Client에 작성하는 것이다.

 

 

 

기본적으로 Server Component와 Client Component가 분리된 이유는 다음과 같다.

  • 성능 최적화
  • 사용자 경험 개선
  • 개발 생산성 증대

 

 

이렇게 쓰고 보니, Server Component로 작성할 경우가 많지 않아보인다.

예를 들면 어떤 경우에 써야할까?

특징 Client Component Server Component
렌더링 위치 브라우저 서버
Hook 사용 가능 불가능
브라우저 API 접근 가능 (window, localStorage, document 등) 불가능
데이터 Fetch 클라이언트에서 처리 서버에서 처리
SEO 클라이언트 렌더링 후 콘텐츠 제공 서버에서 정적 HTML 제공 (SEO에 더욱 유리함)
JS 번들 크기 클라이언트로 전달 JS 번들 전달 없이 정적 HTML만 전달
예시 상태 관리, 브라우저 전용 기능, 반응형 UI, 버튼 클릭 및 모달 팝업, 드롭다운, form 등 정적 페이지, 최초 로드가 중요한 페이지, 데이터 Fetching 및 이후 정적 렌더링할 페이지, SEO 최적화 등

 

 

따라서 어느 한쪽을 무조건 사용한다기 보다는, 혼합하여 사용하는 것이 권장된다.

 

// Server Component
export default function Page() {
  const data = await fetch("https://api.example.com/data").then((res) =>
    res.json()
  );

  return (
    <div>
      <h1>Server Component</h1>
      <p>Fetched Data: {data.title}</p>
      <ClientComponent />
    </div>
  );
}

// Client Component
"use client";

import { useState } from "react";

function ClientComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </div>
  );
}

 

 

 

+ Recent posts