Next.js는 기본적으로 SSR(Server Side Rendering) 방식을 사용하는데, SSG 방식으로 사전 렌더링을 미리 수행해두면 페이지 로딩 속도가 더욱 빨라질 수 있다. 이 과정을 index 페이지에 적용해서 기존의 SSR과 SSG 설정 방법과 그 차이점을 자세히 살펴보자.
SSG를 적용하려면, Next.js에서 제공하는 getStaticProps
함수를 사용하면 된다. getServerSideProps
를 사용해 SSR
방식으로 데이터를 서버에서 받아왔던 것과 동일한 방식으로, getStaticProps
함수를 페이지 컴포넌트 외부에 작성한 후 export
하여 내보내기만 하면 해당 페이지는 SSG 방식으로 동작한다.
참고로 이 getStaticProps
라는 함수는 getServerSideProps
함수와 동일하게 사전 렌더링 과정에서 필요한 데이터를 불러온 다음에 그렇게 불러온 데이터를 props
로써 컴포넌트에게 전달하는 역할을 해줄 수 있다.
이 함수는 빌드 타임에 딱 한 번만 실행되며, 그 시점에 데이터를 미리 가져와 사전 렌더링한 페이지를 생성한다. 이때 getStaticProps
함수의 반환 값은 반드시 props 객체
를 포함해야 하며, 이 props는 컴포넌트의 props
로 전달된다. 예를 들어, 아래와 같이 작성해볼 수 있다.
export const getStaticProps = async () => {
const [allBooks, recoBooks] = await Promise.all([
fetchBooks(),
fetchRandomBooks()
]);
return {
props: {
allBooks,
recoBooks
},
};
};
이렇게 설정된 SSG 방식은, SSR 방식과 달리 빌드 후에도 페이지가 다시 생성되지 않으며, 매우 빠른 속도로 정적 HTML 파일을 제공해 준다. Next.js는 페이지를 빌드 타임에 미리 생성하므로 사용자가 접속할 때마다 빠른 속도로 응답할 수 있는 장점이 있다.
getStaticProps
함수에서 props를 받아오면 이를 컴포넌트의 props로 사용할 수 있다. InferGetStaticPropsType
을 사용하여 getStaticProps
함수의 반환값 타입을 자동으로 추론하도록 설정할 수 있다.
import { InferGetStaticPropsType } from "next";
export default function Home({
allBooks,
recoBooks
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div>
<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>
);
}
이렇게 InferGetStaticPropsType<typeof getStaticProps>
을 통해 props의 타입을 자동으로 추론해 설정할 수 있으며, 타입 오류를 방지할 수 있다.
SSG 방식이 진짜 빌드 타임에 딱 한 번만 실행되는지 확인하려면, getStaticProps
함수 내에 console.log("인덱스 페이지")
를 추가하여 빌드 과정에서만 실행되는지 확인할 수 있다.
export const getStaticProps = async () => {
console.log("인덱스 페이지");
return {
props: {
data: "some data",
},
};
};
이렇게 설정한 후 빌드는 npm run build
명령어로 수행한다.
이 설정은 빌드 타임
에만 실행되어야 하기 때문에 빌드 후 실행 시 서버 콘솔에 한 번만 출력되어야 정상이다. 그런데 개발 모드(npm run dev)
로 실행 중이라면 상황이 달라지게 된다. 개발 모드에서는 페이지의 빠른 수정과 즉각적인 반영을 위해 Next가 자동으로 새로고침과 SSR 방식의 사전 렌더링을 사용한다. 이로 인해 브라우저에서 새로고침할 때마다 getStaticProps가 마치 SSR 방식처럼 요청을 받을 때마다 다시 실행되어 "인덱스 페이지"
메시지가 계속해서 서버 콘솔에 출력된다.
이것은 개발 모드
에서만 발생하는 현상으로, 실제로 SSG가 제대로 작동하는지 확인하려면 프로덕션 모드
로 실행해야 한다.
1. 프로덕션 빌드 수행: npm run build
명령어로 빌드 과정을 완료한다. 이 과정에서 console.log("인덱스 페이지")
메시지가 서버 콘솔에 출력되며 SSG 설정이 적용된 페이지가 빌드 타임에 한 번 사전 렌더링되어 HTML 파일로 생성된다.
프로젝트를 build 해보면 이렇게 collecting page data라는 메세지가 나오면서 정적페이지 생성을 위한 데이터를 수집하고 그리고 그 이후에 Generating static pages라는 메세지가 나오면서 우리가 SSG로 동작하도록 설정한 페이지들이 생성되고 있다고 나온다. 이 과정에서 "인덱스 페이지"
라는 메세지가 출력이 되면서 우리가 앞서
설정한 getStaticProps
함수가 빌드 타임에 잘 실행이 되었다는 것도 확인할 수 있다. 왜냐면 저 메세지는 getStaticProps
이 함수가 실행이 되어서 출력이 된 것이기 때문이다.
그리고 이어서 빌드 결과를 더 확인을 해보자면,
이런식으로 페이지별로 build결과가 출력이 되는데 SSG방식으로 동작하도록 설정해 놓은 index페이지의 경우에는 색상이 채워진 흰색 동그라미 기호가 붙은 걸 볼 수 있고, 그 외에 페이지에는 (f) 이렇게 function기호가 붙어있는 걸 볼 수 있는데 이때 이 페이지별 기호의 차이는 build 결과의 최하단에 나온 메세지를 보면 알 수 있다.
기호별 의미를 살펴보자면,
흰색 동그라미는, 이 페이지가 SSG 방식으로 동작한다는 것이고, 더 상세히는 prerendered as static html
이라고 해서 html로 사전 렌더링된 페이지다라고 써져있다. 그리고, 추가로 (use getStaticProps)
라고 해서 우리가 설정 했었던 것처럼 이 페이지는 getStaticProps를 사용해서 데이터를 불러오는 static페이지다라는 것까지 출력이 된다.
function기호의 의미는 dynamic페이지라는 뜻이다. 동적인 페이지라는 뜻. 더 상세히는 server-rendered on demand
라고 해서 on demand가 주문형이라는 뜻인데 브라우저로부터 요청을 받을 때마다 dynamic하게 이 페이지가 계속 사전 렌더링이 된다는 뜻이다. 그렇기 때문에 이런 dynmaic페이지의 기호에는 SSR방식으로 작동하도록 설정해 놓은 /book
페이지, /search
페이지에 적용이 된 걸 볼 수 있다.
그리고 이때 추가로, /book페이지 이외에도 /api/hello
나, /api/time
이런 API Routes
들에도 dynamic기호 SSR기호가 붙어있는 걸 볼 수 있는데 이렇게 되는 이유는, Nextjs가 기본적으로 모든 API Routes들을 dynamic하게 SSR방식으로 작동하도록 설정을 해두기 때문이다.
빈 동그라미 기호는, Static이라고 써져있고 그 뒤에는 prerendered as static content
라고 되어있는데 이건 흰색동그라미 SSG와 동일한 정적 페이지인데 해당 페이지의 getStaticProps를 설정해 두지 않았기 때문에 그냥 기본 값으로서 설정이 된 SSG페이지 즉, static페이지라는 것이다.
그래서 이미지의 페이지별 결과를 보면,
404페이지
나 test페이지
처럼 우리가 페이지로써 만들어 놓긴 했는데 아무런 설정도 따로 해주지 않은 Next에서는 이렇게 아무것도 적용되지 않은 페이지들을 기본 값으로 정적인 페이지로 빌드 타임에 미리 사전 렌더링 하도록 설정해 준다. 그래서 그냥 기본 값은 SSG와 동일하게 동작한다 라고 이해하면 된다.
그래서 여기서 알 수 있는 추가적인 사실 한 가지는, 우리가 어떤 페이지에 SSG를 설정하는 getStaticProps
도 없고 SSR을 설정하는 getServerSideProps
도 없다면 이런식으로 기본적으로는 SSG 페이지로 설정이 된다 라고 이해하면 된다. 그래서 이 방식은 default 사전 렌더링 방식
이라고 할 수 있다.
2. 프로덕션 모드 실행: 빌드가 완료된 Next 앱을 프로덕션 모드로 실행하려면 npm run start
명령어를 사용한다. 이제 이 페이지는 SSG방식으로 동작하기 때문에 굉장히 빠른 속도로 화면이 렌더링 되는 걸 알 수 있다. 그리고 브라우저에서 페이지를 새로고침하더라도 "인덱스 페이지"
메시지가 더 이상 출력되지 않음을 확인할 수 있다. 이는 페이지가 빌드 타임에 한 번만 생성되고, 이후에는 추가적인 사전 렌더링이 이루어지지 않음을 보여준다.
이렇게 Next.js의 SSG 방식은 빌드 타임에 한 번만 렌더링하여 페이지 로딩 속도를 높이고, 개발 환경에서는 코드 수정 사항을 즉각 반영하는 유연한 구조를 제공한다.
일부 페이지, 예를 들어 검색 결과를 표시하는 /search
페이지에서 쿼리 스트링 값에 따라 동적으로 데이터를 불러와야 하는 경우는 SSG 방식으로 설정할 때 주의가 필요하다. SSG는 빌드 타임에 정적 파일을 생성하기 때문에, URL의 쿼리 스트링에 따라 데이터를 불러오는 동작은 클라이언트 측에서 직접 구현해야 한다.
import SearchableLayout from "@/components/searchable-layout";
import { ReactNode, useEffect, useState } from "react";
import BookItem from "@/components/book-item";
import fetchBooks from "@/lib/fetch-books";
import { useRouter } from "next/router";
import { BookData } from "@/types";
import { GetStaticPropsContext } from "next";
export const getStaticProps = async (context: GetStaticPropsContext) => {
const q = context.query.q;
const books = await fetchBooks(q as string);
return {
props: {
books
}
};
};
export default function Page() {
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>
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>
}
getStaticProps
함수에서는 쿼리 스트링 값을 빌드 타임에 알 수 없기 때문에, context매개변수로 부터 querystring을 가져오고 있는 const q = router.query.q;
이 부분에서 query부분에('GetStaticPropsContext' 형식에 'query' 속성이 없습니다.)
라는 오류가 발생한다. 이 오류는 지금 이 context
객체에 querystring을 보관하는 query
라는 프로퍼티가 존재하지 않는다는 건데 이유는, querystring
이라는 건, localhost:3000/search?q=사과
이런식으로 사용자가 직접 입력한 검색어라던가 아니면, list가 있다면 list의 정렬 기준이라던가, 그런 것들이 전달되는 공간이기 때문에 querystring에 어떠한 값이 들어올지는 빌드 타임에 getStaticProps함수 안에서 알아낼 방법은 없다.
그래서 우리가 이전에 React앱에서 했었던 방식대로 data fetching을 진행해 줘야 한다.
해당 페이지는 기본적으로 SSG 방식으로 설정되지만, 데이터는 컴포넌트가 클라이언트에서 마운트된 후 useEffect를 사용하여 클라이언트 측에서 직접 페칭해야 한다. 이를 통해 SSR과 SSG의 장점을 혼합하여 효율적인 데이터 로딩을 구현할 수 있다.
export default function Page() {
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>
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>
}
아예 getStaticProps
함수를 삭제하고 대신에 그냥 컴포넌트 내부에서 useRouter
훅을 호출해서 const router = useRouter()
router.query.q
이렇게 querystring을 꺼내온 다음에 useEffect
를 컴포넌트 내부에서 호출해준 다음에 이 useEffect를 이용해서 검색어인 q
가 변경이 되었을때 현재 검색어가 있다면, 이 if조건문 안에서 검색어를 불러오는 로직을 작동하도록 만들어주고 state에 보관해주면 된다.
이렇게 해주면 이 search페이지는 이제 getStaticProps와 getServerSideProps함수가 모두 없어졌기 때문에 기본적으로는 SSG방식으로 동작하게 된다. 그렇지만 querystring으로 전달되는 검색어를 빌드 타임에는 알 수 없기 때문에 사전 렌더링 과정에서는 결국 이 페이지의 레이아웃 정도만 렌더링 하게 된다. 그리고나서 이 컴포넌트가 마운트된 이후에 브라우저측에서 이 컴포넌트가 다시 실행이 되면서 직접 querystring으로 데이터를 불러와서 검색 결과 데이터를 클라이언트 측에서 렌더링하게 되는 그런 방식으로 동작하게 될 것이다.
그래서 개발모드를 종료한 다음에 프로젝트를 다시 build 해주면
search페이지의 기호가 이렇게 빈 동그라미로 설정이 되면서 이제는 SSG방식으로 동작하도록 설정이 된 걸 볼 수 있고,
그럼 이어서 npm run start
로 앱을 프로덕션 모드로 가동시킨 다음에 개발자 도구의 네트워크앱에 들어가서 새로고침을 눌러보면 Next의 서버측에서 보내주게 되는 파일들을 볼 수 있는데
그 중에 가장 위쪽에 있는 가장 처음으로 보내주게 되는 이 사전 렌더링 결과의 html파일을 보면 검색 결과 데이터는 제외하고 나머지 부분만 렌더링 해서 브라우저에게 보내준다. 그래서 이런 검색 결과들은 클라이언트 측에서 직접useEffect
를 통해서 백엔드 서버에게 직접 데이터를 요청하는 기존의 React앱의 방식대로 동작한다.
SSR과 SSG 설정 방법: getServerSideProps
를 사용하면 SSR
, getStaticProps
를 사용하면 SSG
방식으로 동작.
getStaticProps의 특성: 빌드 타임에 딱 한 번 실행되며, 이후에는 정적 파일을 클라이언트에 빠르게 제공.
검색 페이지와 같이 쿼리 스트링을 사용하는 경우: SSG로 설정할 때 쿼리 스트링 기반의 동적 데이터는 클라이언트에서 useEffect
를 통해 직접 불러오는 방식으로 구현.
타입 정의: InferGetStaticPropsType
을 사용해 props 타입을 자동 추론하여 타입 오류 방지.
Next.js의 SSG 방식은 특히 정적 페이지 생성에 강점이 있으며, 페이지 로딩 속도를 높여 사용자 경험을 개선할 수 있다. SSG와 SSR, 그리고 클라이언트 측 데이터를 조합한 이 방식을 통해 다양한 페이지에 적합한 렌더링 방식을 선택하여 성능 최적화와 데이터 동기화를 동시에 달성할 수 있다.
index페이지처럼 빌드 타임에 사전 렌더링 할 때 데이터를 불러오게 하고싶다면getStaticProps
함수를 사용하면 되고,
search페이지처럼 querystring을 사용하기 때문에 빌드 타임에는 데이터를 미리 불러올 수 없는 페이지가 있다면, 데이터를 그냥 React 앱에서 처럼 client side측에서 직접 fetching해서 불러오도록 설정하는 것도 가능하다.