Next.js의 다양한 사전 렌더링 방식 중에 가장 기본적인 렌더링이 SSR 입니다.
브라우저에 요청이 들어올 때마다 매번 계속해서 새롭게 페이지를 사전 렌더링하는 방식입니다.
먼저 pages/index.tsx에 컴포넌트 바깥에 함수를 만들면서 시작합니다.
export const getServerSideProps = () => {};
이렇게 함수를 만들고 나면 이제부터는 index 페이지는 SSR 방식으로 사전 렌더링이 이루어지게 됩니다.
약속된 이름의 함수를 만들어서 내보내게 되면 해당 페이지는 SSR로 자동으로 동작하게 되는 것 입니다.
그러면 getServerSidreProps()는 어떤 함수이냐면 페이지를 요청해서 사전 렌더링을 하게 될 때 Page 컴포넌트보다 먼저 실행이 되어서 해당 페이지 컴포넌트에 필요한 데이터들을 백엔드 서버로부터 불러오는 함수입니다.
즉, 페이지 역할을 하는 컴포넌트보다 먼저 실행이 되어서 해당 컴포넌트에 필요한 데이터들을 미리 불러오는 역할을 하는 함수인 것입니다.
또한 getServerSideProps 함수는 return 문에 객체를 포함해야 하는데, 객체 타입의 props라는 프로퍼티가 들어있어야 합니다.
만약 함수 안에서 data = "hello" 데이터를 페이지에 보내고 싶다면
export const getServerSideProps = () => {
const data = "hello"
return {
props: {
data,
}
}
};
위와 같이 설정하면 됩니다.
이는 일종의 프레임워크의 문법이라고 생각하시면 됩니다.
그러면 페이지 컴포넌트에서는 React에서 받아 오듯이 {data}로 똑같이 받아 오면 됩니다.
export const getServerSideProps = () => {
const data = "hello";
return {
props: {
data,
},
};
};
export default function Home({ data }: any) {
console.log(data);
데이터를 console.log 결과를 확인해보면 잘 받아온 것을 확인할 수 있습니다.

getServerSideProps 함수는 사전 렌더링을 하는 과정에서 딱 한 번만 실행이 되기 때문에, 오직 서버 측에서만 실행이 되는 함수 입니다.
따라서 함수 내부에 console.log로 어떤 문구를 출력하려고 해도 console창에는 뜨지 않게 됩니다.
따라서 window 메서드나 alert메서드 또한 window는 자바스크립트의 브라우저를 나타내기에 getServerSideProps 함수 내부에서 시키면 오류가 발생해 사용할 수 없다는 것 또한 알아가야 합니다.
추가적으로 Home 페이지 내부에서도 console.log(data)는 브라우저에서만 실행이 되는 것이 아닌 백엔드 서버에서 실행이 됩니다. 따라서 총 2번이 되는 것입니다.
터미널 창에서 확인해보면 hello가 있는 것을 확인할 수 있습니다.

이는 Home 페이지 안에서도 window나 location또한 함부러 사용할 수 없다는 것을 의미하는 것입니다.
Next를 사용할 때는 이런점을 주의해야 합니다.
만약 window를 출력하거나 location을 실행시키기 위해서는 useEffect를 실행시키면 됩니다.
export default function Home({ data }: any) {
console.log(data);
useEffect(() => {
console.log(window);
}, []);

Next.js에는 기본적으로 다양한 내장 타입들이 제공 됩니다. 그래서 대부분의 타입 정의들은 Next.js에서 제공하는 것들만 이용해서도 거의 다 가능합니다.
inferGetServerSidePropsType는 getServerSideProps함수에서의 props를 자동으로 타입을 추론해주는 기능을 하는 타입입니다.
export default function Home(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
이제 SSR 방식을 프로젝트의 모든 페이지에 다 적용해 보면서 데이터 패칭까지 함께 진행해 보려고 합니다.
백엔드 서버를 가동시킨 후에 기능 구현을 시작하면 되겠습니다.
인덱스 페이지에서는 추천 도서 데이터와 모든 도서 데이터를 불러오려고 합니다.
기존의 index.tsx에서 데이터를 불러오려는 함수를 만들면 복잡하고 가독성이 떨어지기 때문에 이럴 때에는 src에 lib 폴더를 만들어준 후 따로 정리하면 가독성이 좋고 편합니다.
src/lib/fetch-books.ts 파일을 만들어 준 후
export default async function fetchBooks() {
}
url을 변수로 설정해주고
const url = 'http://localhost:12345/book'
오류가 날 수 있기에 try 문으로 감싸서
const response는 await으로 fetch 메서드를 호출하고 인수로는 요청 경로인 url을 넣어주도록 합니다.
그러고 나서 조건문으로 fetch문이 실패할 수 있기에 response.ok가 만약 false, undefined이면 thorw new Error를 호출해서 예외를 던지도록 만들어주고
걸리지 않았더라면 await response.json()으로 json형식으로 받아 응답값을 json 포맷으로 변환해서 반환해주면 됩니다.
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error()
}
return await response.json()
}
try문까지 완료가 되었다면, 그 다음은 catch문을 만들어줘야 합니다. catch문에서는 에러를 cosole.log로 에러 메세지를 출력을 한 다음에 일단 빈 배열이라도 반환해주도록 설정해주면 됩니다.
catch (err) {
console.log(err);
return [];
}
다음에는 마지막으로 FetchBooks라는 함수의 반환값 타입까지 정의를 하면 됩니다.
FetchBooks 함수는 서버로부터 모든 ㅁ덧의 데이터를 불러와서 반환을 해주는 함수 입니다.
비동기로 반환을 해주고 있기 때문에 먼저 비동기 결과를 의미하는 Promise 객체에 제네릭으로 정의를 해주면 됩니다.
import { BookData } from "@/types";
export default async function fetchBooks(): Promise<BookData[]> {
const url = "http://localhost:12345/book";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.log(err);
return [];
}
}
이제 이 함수를 index.tsx에 getServerSideProps 함수 안에서 방금 만든 함수를 변수로 await을 사용해서 호출해주면 됩니다.
이때 함수 내에서 await을 사용하려면 getServerSideProps에 async 키워드를 붙여줘야 합니다.
그런 다음에 함수의 변수를 props로까지 전달하도록 설정해주면 됩니다.
그러면 페이지 컴포넌트에서는 getServerSideProps로부터 전달받은 값을 똑같이 props로 받아서 컴포넌트 안에서 console.log를 통해 제대로 전달이 되는지 확인해보면 됩니다.
export const getServerSideProps = async () => {
const allBooks = await fetchBooks();
return {
props: {
allBooks,
},
};
};
export default function Home({ allBooks }: InferGetServerSidePropsType<typeof getServerSideProps>) {
console.log(allBooks);

이제
export default function Home({ allBooks }: InferGetServerSidePropsType<typeof getServerSideProps>) {
useEffect(() => {
console.log(window);
}, []);
return (
<div className={style.container}>
<section>
<h3>추천하는 도서</h3>
{books.map((book) => ( // -> allBooks
<BookItem key={book.id} {...book} />
))}
</section>
<section>
<h3>등록된 모든 도서</h3>
{books.map((book) => ( // -> allBooks
<BookItem key={book.id} {...book} />
))}
</section>
</div>
);
}
books대신에 allBooks를 넣어주면 됩니다.
똑같이 lib 폴더 안에 fetch-random-books.ts 를 만들고 위와 똑같이 만들어주면 됩니다.
import { BookData } from "@/types";
export default async function fetchRandomBooks(): Promise<BookData[]> {
const url = "http://localhost:12345/book/random";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.log(err);
return [];
}
}
}
후 다시 index.tsx로 돌아와 getServerSideProps 함수 내에서 새로운 변수로 fetchRandomBooks함수를 받아주고 위와 똑같이 변경까지 해주면 됩니다.
export const getServerSideProps = async () => {
const allBooks = await fetchBooks();
const recoBooks = await fetchRandomBooks();
return {
props: {
allBooks,
recoBooks,
},
};
};
export default function Home({ allBooks, recoBooks }: InferGetServerSidePropsType<typeof getServerSideProps>) {
console.log(recoBooks);
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>
);
}
export const getServerSideProps = async () => {
const allBooks = await fetchBooks();
const recoBooks = await fetchRandomBooks();
현재 await인 allBooks와 recoBook는 allBooks의 함수가 끝나기까지 기다렸다가 끝나면 recoBooks의 함수를 실행시키는 이러한 직렬적인 방식으로 동작하고 있습니다.
이것을 병렬적으로 업데이트하기 위해서
const 배열로 all books와 reco books를 받아오는데 await Promise.all() 메서드로 받아옵니다.
Promise.all()은 인수로 전달한 배열 안에 들어있는 모든 비동기 함수들을 동시에 실행시켜주는 메서드 입니다.
그래서 Promise.all()안에 인수로 fetchBooks(), fetchRandomBooks()를 넣어주시면 됩니다.
이제는 검색바에서 api 요청으로 받아온 데이터를 검색하는 것을 구현해보고자 합니다.
먼저 search/index.tsx로 들어가주시고
똑같이 getServerSideProps 함수를 내본내며 SSR, 서버사이드 함수가 실행시키도록 합니다.
먼저 검색을 시킨 값을 나타내려면 쿼리스티링 함수를 getServerSideProps 함수에서 읽어야 합니다.
이떄에는 getServerSideProps 함수에 전달되는 매개변수를 이용하면 됩니다.
매개변수의 타입은 Next.js에서 자체 제공하는 getServerSidePropsContext로 정의 해주면 됩니다.
이때 매개 변수는 모든 정보가 다 들어 있습니다.
그렇기에 console.log로 확인해보면 서버측에 출력이 잘되고 있는 것을 확인할 수 있고

query에 query 스트링도 잘 전달이 되어 있는 걸 볼 수 있습니다.
그래서 이 값을 꺼내서 사용하면 됩니다.
q를 변수로 context.query.q로 꺼내오면 됩니다.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const q = context.query.q
return {
props: {},
};
};
그 다음 백엔드에서 검색 api를 호출하는 새로운 함수 하나를 더 만들어야 합니다
하지만 이번에는 그럴 필요가 없습니다.
왜냐하면 앞서 만들어둔 FetchBooks 함수를 확장해서 사용하면 되기 때문입니다.
확장 방법은 fetchBooks 함수가 q를 매개변수로 설정하고 string 타입으로 받도록 설정한 다음에 q뒤에 ?를 붙여서 선택적 프로퍼티로 설정을 해주고
그러고 나서 함수 내부에서 if문으로 만약에 q가 있다면 검색 결과를 불러오는 api를 호출하면 되는 것입니다.
먼저 const로 설정됬던 url을 let으로 변경시켜주고
let url = "http://localhost:12345/book";
조건문 내부에서 irl += '/search?q=${}'으로 설정해주면 됩니다.
그러면 q에 아무 값도 전달하지 않으면 모든 도서를 불러오는 api가 그대로 호출이 되며,
q가 있다면 검색 결과를 불러오는 api로 호출을 할 수 있습니다.
따라서 search/index.tsx로 돌아와서
books변수를 fetchBooks() 함수로 호출해주며 q를 인수로 전달해주면 됩니다.
또한 q가 stringArray, undefined가 있을 수 있으니 as string으로 타입을 반환해서 전달까지 해주면 됩니다.
import { ReactNode } from "react";
import SearchableLayout from "../components/searchable-layout";
import BookItem from "../components/book-item";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import fetchBooks from "@/lib/fetch-books";
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>;
};
pages/book/id.tsx 파일을 들어간 후 위와 같이 SSR 방식을 만들어 줍니다.
export const getServerSideProps = () => {
return {
props: {},
};
};
이제 함수 안에 url 파라미터인 특정 아이디를 기준으로 특정 도서의 데이터를 불러와야 합니다.
그럴려면 url 파라미터를 함수 안에 불러와야 합니다.
이런 경우는 서치 페이지에서 쿼리 스트링을 불러올 때랑 동일하게 context라는 매개변수를 이용해주면 됩니다. GetServerSidePropsContext로 타입 변수를 지정해주고
export const getServerSideProps = (context : GetServerSidePropsContext) => {
return {
props: {},
};
};
url 파라미터는 context의 params라는 프로퍼티로 불러와주시면 됩니다.
const id = context.params.id
이때 타입 에러가 발생하고 있습니다. 이런거는 !단언을 통해서 params가 값이 있을 거다 undefined가 아니다라고 정의해주면 됩니다.
이렇게 사용할 수 있는 이유는 [id].tsx는 무조건 url 파라미터 하나 있어야만 접근할 수 있는 페이지이기 때문입니다.
const id = context.params!.id
console.log로 확인해보면 서버측 터미널에서 확인할 수 있습니다.

서버에서 불러오는 api를 호출해서 데이터를 불러오면 됩니다. lib/fetch-one-book.ts 만들어서 export default
export default async function fetchOneBook() {
}
매개변수로 아이디로 넘버 타입으로 이렇게 받아오도록 설정을 해 준 다음에 url 변수로 url주소를 입력 해줍니다.
나머지는 위와 같이 해줍니다.
import { BookData } from "@/types";
export default async function fetchOneBook(id: number): Promise<BookData | null> {
const url = `http://localhost:12345/book/${id}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.log(err);
return null;
}
}
[id].tsx에서 돌아와 나머지도 똑같이 해주면 됩니다.
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import style from "./[id].module.css";
import fetchOneBook from "@/lib/fetch-one-book";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const id = context.params!.id;
const book = await fetchOneBook(Number(id));
return {
props: {
book,
},
};
};
export default function Page({ book }: InferGetServerSidePropsType<typeof getServerSideProps>) {
if (!book) return "문제가 발생 했습니다. 다시 시도해주세요";
const { id, title, subTitle, description, author, publisher, coverImgUrl } = book;
return (
<div className={style.container}>
<div className={style.cover_img_container} style={{ backgroundImage: `url('${coverImgUrl}')` }}>
<img src={coverImgUrl} />
</div>
<div className={style.title}>{title}</div>
<div className={style.subTitle}>{subTitle}</div>
<div className={style.author}>
{author} | {publisher}
</div>
<div className={style.description}>{description}</div>
</div>
);
}
그러면 여태까지의 모든 페이지들이 SSR 렌더링 방식으로 만든 것을 확인할 수 있습니다.