Static Generation without data
기본적으로 Next.js의 아무런 fetching이 필요없는 페이지는 Static Generation을 사용하는 pre-render기법을 사용합니다.
예시를 들면
function About() {
return <div>About</div>
}
export default About;
이러한 경우에는 Next.js는 하나의 HTML파일을 빌드타임떄 생성합니다.
Static Generation with data
어떠한 페이지는 당연히 pre-rendering되기 위해 외부의 fetching되는 데이터가 필요합니다. 이 경우에는 두가지 시나리오가 존재합니다.
이 중 하나 또는 둘다 적용될 수 있습니다.
< SSG및 SSR적용 방안 >
그렇다면 다시 CNA프로젝트로 돌아가서 SSG와 SSR을 어떻게 적용하는지 직접 코딩하며 알아보자.
SSG방식의 경우 데이터 없이 또는 데이터와 함꼐 실행하는 2가지 방법으로 다시 세분화 된다.
먼저 일단 CSR방식으로 useEffect를 사용하여 컴포넌트가 마운트 되었을 때 가져오고, 가져온 데이터를 화면에 뿌려주는 방식으로 작성해보자.
// pages/index.tsx
import type { NextPage } from "next";
import styles from "../styles/Home.module.css";
import Link from "next/link";
const Home: NextPage = () => {
return (
<div className={styles.App}>
<Link href="about">
<a className={styles.link_about}>about 페이지로 이동</a>
</Link>
</div>
);
};
export default Home;
// pages/about/index.tsx
import React, { useEffect, useState, useCallback } from "react";
import type { NextPage } from "next";
import axios from "axios";
import styles from "../../styles/Home.module.css";
type listContent = {
userId: number;
id: number;
title: string;
body: string;
};
const About: NextPage = () => {
const [list, setList] = useState<listContent[]>([]);
const getList = useCallback(async () => {
const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const data = res.data;
setList(data);
}, []);
useEffect(() => {
(async () => {
getList();
})();
}, [getList]);
return (
<ul className={styles.About}>
<h1>여기는 About 페이지입니다!</h1>
{list.length &&
list.slice(0, 10).map((item) => <li key={item.id}>{item.title}</li>)}
</ul>
);
};
export default About;
// styles/Home.module.css
.About {
list-style: none;
border: 1px solid black;
margin: 20px;
padding-bottom: 20px;
background-color: bisque;
}
.About li {
margin: 10px;
}
이렇게 한다음 localhost:3000/about으로 들어가서 network창을 확인해 보면
< 1-1 >> 'getStaticProps' >
하지만 만약 해당 문서에 출력되는 10개의 타이틀이 항상 고정된 형식과 내용이라고 가정해보자. 그렇다면 요청에 따라 그때마다 HTML문서를 생성할 필요없이 첫 요청에 하나의 정적 HTML문서를 생성 후 그 이후의 요청엔 계속 동일한 문서를 반환하면 될 것이다. 이 떄 화면에 뿌려줄 데이터 역시 미리 서버에서 처리하여 완성된 정적 HTML문서를 반환한다면 SEO적용이 용이할 것이다. 다음과 같이 코드를 수정해보자.
// pages/about/index.tsx
// @ts-nocheck
import React, { useEffect, useState, useCallback } from "react";
import type { NextPage } from "next";
import axios from "axios";
import styles from "../../styles/Home.module.css";
type listContent = {
userId: number;
id: number;
title: string;
body: string;
};
const About: NextPage = ({ list }) => {
return (
<ul className={styles.About}>
<h1>여기는 About 페이지입니다!</h1>
{list.length &&
list.slice(0, 10).map((item) => <li key={item.id}>{item.title}</li>)}
</ul>
);
};
export default About;
export const getStaticProps = async () => {
const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const data: listContent[] = res.data;
console.log(data[1]);
return {
props: {
list: data,
},
};
};
getStaticProps라는 비동기 함수를 선언하고 그 안에서 axios요청을 한 뒤 console.log를 통해 출력을 하나 하고 return으로 props를 반환하고 있다. 그리고 About컴포넌트에서는 반환된 props에서 list를 구조분해 할당으로 받아오고 있다. 결과는 아래와 같다.
서버로부터 우리가 보고있는 화면과 동일한 마크업 구조의 HTML문서가 정상적으로 내려오고 있는 것을 확인할 수 있다. 그런데 크롬 개발자도구 콘솔창에는 우리가 getStaticProps에서 적어준 출력사항이 따로 나오지 않는다!. 사용하고 있는 IDE ( vscode )로 돌아가서 서버 콘솔창을 보면 그곳에 출력되고 있는 것을 확인할 수 있다. 즉 SSG역시 서버 사이드에서 1차적인 작업이 이루어지고 있다는 것을 의미한다. 이처럼 SSG를 특정 데이터와 같이 해야하는 경우엔 getStaticProps함수를 비동기로 정의하여 사용할 수 있다.
개발모드에서는 SSG로 작성하더라도 매 요청마다 페이지를 재생성한다. 개발의 용이성을 위해 그렇게 설정해둔 것 같다. 따라서 npm run buid후에 npm start로 실행하게 되면 첫 요청때 해당 페이지를 pre-rendering하여 정적 문서로 생성해두고 그 다음 요청때는 생성된 문서를 반환한다.
< 1-2 >> 'getServerSideProps' >
방금 제작한 About컴포넌트의 각 리스트들을 Link로 다시 감싸주고, 해당 Link를 클릭 시 번호에 해당하는 포스트로 이동하게끔 수정하였다. 링크 이동 후 보여지는 페이지는 포스트 작성자, 내용, 제목 등 매 요청 시 마다 달라지는 내용을 렌더링한다. 이 같은 경우엔 SSR로 매 요청 시 각기 다른 HTML문서를 생성하여 반환하도록 하는 방식이 적절하다. 이를 코드로 구현해보자!
// pages/about/index.tsx
// @ts-nocheck
import React, { useEffect, useState, useCallback } from "react";
import type { NextPage } from "next";
import axios from "axios";
import styles from "../../styles/Home.module.css";
import Link from "next/link";
type listContent = {
userId: number;
id: number;
title: string;
body: string;
};
const About: NextPage = ({ list }) => {
return (
<ul className={styles.About}>
<h1>여기는 About 페이지입니다!</h1>
{list.length &&
list.slice(0, 10).map((item) => (
<Link
href={{
pathname: `./detail/${item.id}`,
}}
passHref
key={item.id}
>
<li key={item.id}>{item.title}</li>
</Link>
))}
</ul>
);
};
export default About;
export const getStaticProps = async () => {
const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const data: listContent[] = res.data;
console.log(data[1]);
return {
props: {
list: data,
},
};
};
// pages/detail/[id].tsx
// @ts-nocheck
import React, { useEffect, useState } from "react";
import axios from "axios";
import type { NextPage } from "next";
import styles from "../../styles/Home.module.css";
type listContent = {
userId: number;
id: number;
title: string;
body: string;
};
const Detail: NextPage = ({ item }) => {
return (
<div className={styles.Detail}>
<h1>{item.title}</h1>
<p>{item.body}</p>
<p>{item.id}번째 게시글</p>
</div>
);
};
export default Detail;
export const getServerSideProps = async (ctx) => {
const id = ctx.params.id;
const res = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
const data: listContent = res.data;
console.log(data);
return {
props: {
item: data,
},
};
};
pages 폵더에 detail이라는 디렉토리를 만들고, 그 밑에 [id].js에 해당하는 내용을 기입했다. 이는 Next의 dynamic routing 방식이다. 여기서는 SSR에 대해 좀 더 초점을 맞춘다. Detail컴포넌트 내용을 보면 알 수 있듯이 매번 클릭된 포스트에 따라 다른 내용이 출력될 것이다. 따라서 SSR방식을 채택했고 이를 위해 getServerSideProps라는 비동기 함수를 다시 export하고 있다. 대체로 아까 보았던 getStaticProps와 유사한 형태를 띄고 있는데, ctx라는 인자를 받고 있는 것을 볼 수 있다.
localhost:3000/detail/6으로 들어가면 이런 화면을 볼 수 있을 것이다.
중간 정리
일단 Pure React.js app에서는 CSR방식이기 때문에 < React components are initialized -> js에의해 interactive 해 진다. >
그 다음 Next.js를 이용한 Pre-rendering은 < Pre-rendered HTML -> js에 의해 interactive 해 진다 > 이는 SSR + CSR방식으로 js에의해 interative하기 전까진 <Link / >와 같은 것은 동작하지 않는다.
우리가 예제를 통해 알아본 것을 정리해보자
우선 우리는 아무것도 적용하지 않은 pure한 Next.js를 통해 게시글을 만들었다. 여기서는 Next는 기본적으로 SSG를 지원하기 때문에 비동기적으로 받아온 데이터를 제외하곤 rendering된 상테에서 우리에게 보여진다 또한 CSR에 의해서 나머지 부분이 렌더링 되면서 동시에 interactive해진다.
이는 SEO최적화에 어려움을 겪은 바 있다. 그렇기 때문에 바뀌지 않을 비동기적으로 받아올 수 있는 내용을 getStaticProps를 통해 미리 SSG를 통해 HTML을 만들어 놓았다. 그랬더니 SSG를 통해 처음부터 내용이 깔끔하게 나옴으로서 SEO최적화가 가능했다.
또한 다음에는 각각의 li에 Link태그를 겹쳐 각각의 포스트로 갈 수 있게 했다.
< 1-3 >> 'getStaticPaths'
SSR방식의 getServerSideProps같은 경우는 다이나믹(dynamic)라우트 예시를 들어 설명했다. 하지만 다이나믹 라우트의 경우에도 SSG를 적용하여 각 제품군의 상세 페이지를 미리 정적 생성하고 싶을 수 있다. 그럴때 사용하는 비동기 함수가 getStaticPaths이다.
pages폴더에 detail-static디렉토리를 만들고 deynamic path로 [id].tsx 파일을 하나 만들었다.
// src/detail-static/[id].tsx
// @ts-nocheck
import React from "react";
import axios from "axios";
type listContent = {
userId: number;
id: number;
title: string;
body: string;
};
const DetailStatic = ({ item }) => {
return (
<div>
{item && (
<div className="Detail">
<h1 style={{ color: "#fff" }}>with Static Generation</h1>
<h1>{item.title}</h1>
<p>{item.body}</p>
<p>{item.id}번째 게시글</p>
</div>
)}
</div>
);
};
export default DetailStatic;
export const getStaticPaths = async () => {
return {
paths: [
{ params: { id: "1" } },
{ params: { id: "2" } },
{ params: { id: "3" } },
],
fallback: true,
};
};
export const getStaticProps = async (ctx) => {
const id = ctx.params.id;
const res = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
const data: listContent = res.data;
console.log(data);
return {
props: {
item: data,
},
};
};
먼저 getStaticprops로 getServerSideProps와 동일한 방식으로 데이터를 가져온다. 하지만 static이기 때문에 매 요청마다 렌더링되지 않고 가져온 데이터로 서버에 정적 HTML문서를 생성할 것이다.
그리고 getStaticPaths는 다이나믹 경로를 지정해주었다. 위에서는 3개의 id만 지정해주었다. 해당 함수는 fallback옵션을 설정해주는데 이 값이 false인 경우엔 지정되지 않은 경우 ( 4 - 10번 까지의 포스트 )에 대한 요청엔 404에러를 출력한다. 우리는 이 값을 true로 지정하여 지정되지 않는 경로에 대한 요청도 대응하여 정적생성을 하도록 설정한 것이다.
getStaticProps, getServerSideProps...??
React Component개념으로만 접근하면 위와 같은 getServerSideProps또는 getStaticProps가 어떻게 어떻게 저 위치에서 서버와 통신을 하는지 의아할 수 있다.
이를 위해 우리는 SSR이라는 용어에 주목할 필요가 있다. 서버 사이드 렌더링, 즉 서버에서 무언가 렌더링을 위한 로직을 처리한다는 뜻이다. 그런데 "어 근데 따로 node.js에서 express와 같은 서버를 직접 띄워주지 않았는데요?" 하는 의문이 생길 수 있다. 그렇기 때문에 다음과 같이 이해할 수 있다.
서버 사이드 렌더링은 결국 서버의 역할이 필요하다. 즉 Next는 자체적으로 서버를 가지고 있다. 해당 서버는 아마도 node.js환경에서 구동되는 서버일 확률이 높다. (내부적으로 express를 쓰는지 아니면 다른 웹 서버 프레임워크를 쓰는지 모르겠다..) SSR을 하기위해 필수조건은 아니지만 나름 주요한 요건 중 하나가 Isomorphic javascript환경인데, 이는 프론트엔드와 백엔드, 즉 클라이언트 단과 서버 단의 프로그래밍언어가 자바스크립트로 동일함을 의미한다.
이러한 환경이 나름 중요한 이유 중에 하나는 결국 서버가 프론트엔드 단에서 쓰이는 형식과 유사하게 컴포넌트를 구성하여 뷰(View)단을 짜서 전달해 주어야 하는데, 둘의 언어가 일치한다면 이와 같은 통신이 비교적 원활하기 때문이다. 물론 무조건 < 프론트엔드.js + 백엔드.js >구성으로 SSR을 지원할 수 있는 것은 아니긴 하나 이 조합보다 다소 복잡한 설정과 러닝커브가 요구될 것이다. ]
즉 Next에서 pages폴더의 영역은 프론트엔드와 백엔드, 즉 서버와 공유하고 있는 공간이라고 생각하면 될 것 같다. 즉 해당 폴더의 파일에서 export하는 getServerSideProps와 getStaticProps등의 함수는 Next의 서버로 전달되어 주어진 임무를 수행하고 이를 다시 컴포넌트 단에 결과를 반환해주는 것이다.
'Web > NextJs' 카테고리의 다른 글
Next.js - Built-in CSS Support (0) | 2022.02.23 |
---|---|
Next.js - Data Fetching ( getStaticProps, getStaticPaths ) (0) | 2022.02.18 |
Next.js - Data Fetching ( getServerSideProps ) (0) | 2022.02.18 |
Next.js - Getting Started - 1 (0) | 2022.02.17 |
Next.js ( CSR && SSR ) (0) | 2022.02.17 |