Hydration : 이미 렌더링된 HTML과 리액트와 연결하는 것을 의미한다.
즉, 프리렌더링은 웹 페이지가 렌더링되기 이전의 렌더링하는 것을 의미하며, 크게 정적 생성과 서버사이드 렌더링으로 나뉜다.
프리렌더링을 하면 좋은 점은 초기 로딩이 빨라지며, 검색엔진 최적화(SEO)가 된다.
Next.js에서는 기본적으로 정적 생성을 한다. 이 때 데이터를 가져와서 사용하고 싶을 때는 getStaticProps를 사용해서 사용하면 된다.
export async function getStaticProps() {
const res = await axios.get('/products');
const products = res.data.results;
return {
props: {
products
}
}
}
export default function Hom({ products }) {
return (
...
)
}
// [id].js
export async function getStaticPaths() {
const res = await axios.get('.products/');
const products = res.data.results;
const paths = rpoducts.map((product) => ({
params: { id: String(product.id) },
}));
return {
paths,
fallback: true,
}
}
export async function getStaticProps(context) {
const productId = context.params['id'];
let product;
try {
const res = await axios.get(`/products/${productId}`);
product = res.data;
} catch {
return {
notFound: true,
};
}
return {
props: {
product,
}
}
}
export default function Product({ product }) {
...
if (!product) return (
<div className={styles.loading}>
<Spinner />
</div>
);
...
}
export async function getServerSideProps(context) {
const q = context.query['q'];
const res = await axios.get(`/products/?q=${q}`);
const products = res.data.results ?? [];
return {
props: {
products,
q,
},
}
}
export default function Search({ q, products }) {
...
}
...
export default function Product({ product, sizeReviews: initialSizeReviews }) {
const [sizeReviews, setrSizeReviews] = useState(initialSizeReviews);
const [formValue, setFormValue] = useState({
size: 'M',
sex: 'male',
height: 173,
fit: 'good',
});
async function handleSubmit(e) {
e.preventDefault();
const sizeReview = {
...formValue,
productId: product.id,
};
const res = await axios.post('/size_reveiws/', sizeReview);
const newSizeReview = res.data;
setSizeReveiws((prevSizeReviews) => [
newSizeReview,
...prevSizeReviews,
]);
};
async function handleInputChange(e) {
const { name, value } = e.target;
handleChange(name, value);
}
async function handleChange(name, value) {
setFormValue({
...formValue,
[name]: value,
});
}
...
}
프리렌더링을 하면 검색엔진 최적화(SEO)에 도움이 되고, 페이지의 로딩 속도가 빠르다는 장점이 있다. 정적 생성과 서버사이드 렌더링 두 가지 선택이 있는데, 이 두가지를 언제 어떻게 사용하면 좋을까?
홈페이지에서는 상품 데이터가 자주 업데이트되지 않는다고 가정했기 때문에, 정적 생성을 했었다. getStaticProps() 함수를 구현하고 데이터를 불러와서 정적 생성을 하도록 구현했다. 만약에 관리자가 상품을 자주 업데이트하거나, 항상 최신의 상품을 보여줘야 한다면 서버사이드 렌더링을 선택했을 것이다.
검색 페이지에서는 주소에 있는 쿼리스트링 값에 따라서 다른 검색 결과를 보여줘야 하기 때문에, 서버사이드 렌더링을 했었다. getServerSideProps() 함수를 구현해서 쿼리스트링에 따라 다른 데이터를 가지고 서버사이드 렌더링을 하도록 구현했다.
상품 상세 페이지에서는 항상 최신의 리뷰 데이터를 보여주기 위해서, 서버사이드 렌더링을 했었다. getServerSideProps() 함수를 구현해서 아이디 값에 따라 다른 데이터를 가지고 서버사이드 렌더링을 하도록 구현했다. request가 들어올 때마다 매번 리뷰 데이터를 불러와서 렌더링하기 때문에, 항상 최신의 리뷰 데이터로 프리렌더링할 수 있다.
설정 페이지에서는 딱히 데이터를 사용하지 않았기 때문에, Next.js에서 기본적으로 정적 생성을 하고 있었다.
그 외에 특별한 이유가 없다면 되도록 정적 생성을 하는 걸 권장한다.
웹 브라우저가 페이지를 로딩하기 이전에 렌더링하는 걸 의미한다. 크게 정적 생성(Static Generation)과 서버사이드 렌더링(Server-side Rendering)으로 나뉜다. Next.js에서는 기본적으로 모든 페이지를 정적 생성한다.
프로젝트를 빌드하는 시점에 미리 HTML을 렌더링하는 걸 의미한다.
정적 생성할 때 필요한 데이터를 받아와서 렌더링하고 싶다면 getStaticProps() 함수를 구현하고 export하면 된다. 객체의 props 프로퍼티로 넘겨줄 Props 객체를 지정하고, 이것을 페이지 컴포넌트에서 사용할 수 있다.
export async function getStaticProps() {
const res = await axios('/products/');
const products = res.data;
return {
props: {
products,
},
};
}
export default function Home({ products }) {
return (
<ProductList products={products} />
);
}
[id].js와 같이 다이나믹 라우팅을 하는 페이지를 정적 생성을 할 때에는 어떤 페이지를 정적 생성할지 지정해줘야 한다. getStaticPaths() 라는 함수를 구현하고 export해서 정해줄 수 있다.
getStaticPaths() 함수에서는 리턴 값으로 객체를 리턴하는데, paths라는 배열에서 각 페이지에 해당하는 정보를 넘겨줄 수 있다. 예를 들어 id 값이 '1'인 페이지를 정적 생성하려면 { params: { id: '1' } }과 같이 쓸 수 있다.
fallback이라는 속성을 사용해서 정적 생성되지 않은 페이지를 처리해 줄 것인지 지정할 수 있다. fallback: true라고하면 생성되지 않은 페이지를 접속했을 때 getStaticProps() 함수를 실행해 페이지를 만들어서 보여준다.
export async function getStaticPaths() {
return {
paths: [
{ params: { id: '1' }},
{ params: { id: '2' }},
],
fallback: true,
};
}
getStaticProps() 함수에서는 context 파라미터를 사용해서 필요한 Params(context.params) 값이나 쿼리스트링(context.query) 값을 참조할 수 있다.
그리고 fallback: true라고 지정했다면, 필요한 데이터가 존재하지 않을 수 있기 때문에 적절한 예외처리를 해야 한다. { notFound: true }를 리턴하면 데이터를 찾을 수 없을 때 404 페이지로 이동시킬 수 있다.
export async function getStaticProps(context) {
const productId = context.params['id'];
let product;
try {
const res = await axios(`/products/${productId}`);
product = res.data;
} catch {
return {
notFound: true,
};
}
return {
props: {
product,
},
};
}
만약 fallback: true를 설정했다면 getStaticProps()를 실행하는 동안 보여줄 로딩 페이지를 구현해야 한다. 페이지 컴포넌트에서 필요한 데이터가 존재하지 않을 때를 처리해 주면 된다.
export default function Product({ product }) {
if (!product) {
return <>로딩 중 ...</>
}
return <>상품 이름: {product.name}</>;
}
Next.js 서버에 request가 도착할 때마다 페이지를 렌더링해서 보내주는 방식이다. getServerSideProps() 함수를 구현하고 export하면 된다. 이때 리턴 값으로는 객체를 리턴한다. 정적 생성때와 마찬가지로 props 프로퍼티로 Props 객체를 넘겨주면 페이지 컴포넌트에서 받아서 사용할 수 있다.
export async function getServerSideProps() {
const res = await axios('/products/');
const products = res.data;
return {
props: {
products,
},
};
}
export default function Home({ products }) {
return (
<ProductList products={products} />
);
}
Next.js 13.4 이후 버전부터는 새로운 방식의 라우팅을 지원하기 시작했다. App Router라는 것이다. 이전까지 배웠던 것은 Pages Router라고 부른다.
App Router의 가장 큰 차이는 /pages 폴더가 아닌 /app 폴더에 페이지 컴포넌트들을 추가한다는 것이다. 그리고 이 페이지 컴포넌트들은 기본적으로 리액트 서버 컴포넌트(React Server Compoenent)이다. 기존에 사용하던 리액트 컴포넌트와는 조금 다른 컴포넌트이다. 간단히 설명해서 서버에서만 렌더링되는 컴포넌트이다. 그 밖에도 라우팅의 여러 기능들이 달라졌다. 공통된 레이아웃을 적용하는 방식이나, 메타데이터를 사용하는 방식, 그리고 데이터를 받아오는 방식 등이 달라졌다.
리액트 서버 컴포넌트는 서버에서만 렌더링하는 컴포넌트이다. 기존에 사용하던 컴포넌트 방식은 서버 컴포넌트와 구분하기 위해서 리액트 클라이언트 컴포넌트라고 부르기도 한다.
리액트 서버 컴포넌트와 리액트 클라이언트 컴포넌트의 문법에서 가장 큰 차이는 데이터를 가져오는 방식이다. 클라이언트에서 request를 보내서 데이터를 받아오거나, Next.js에서 프리렌더링을 한다면 데이터를 Props로 내려받았다. 리액트 서버 컴포넌트를 사용하면 컴포넌트를 async/await 함수로 만들 수 있고, 함수 최상위(top-level)에서 await로 데이터를 가져올 수 있다.
async/await를 사용해서 컴포넌트 함수를 작성하기 때문에 훨씬 직과적인 문법으로 컴포넌트를 개발할 수 있다. 서버에서 모든 데이터를 가져온 다음 렌더링까지 해서 보내주기 때문에 서버와 클라이언트가 여러번 request를 주고받을 때보다 빠르게 페이지를 보여줄 수 있다. 게다가 서버 컴포넌트 렌더링에 필요한 자바스크립트는 서버에서만 실행하기 때문에 클라이언트가 다운로드해야 할 자바스크립트 코드의 양도 줄어든다.
async function getData() {
const res = await fetch('https://api.example.com/...');
return res.json();
}
export default async function Page() {
const data = await getData();
return <main> ... </main>;
}
Introducing Next.js App Router - Vercel on Youtube
React Labs: What We've Been Working On - March 2023 - React
Server Components - Next.js
Introducing Zero - Bundle-Size React Server Components