기본적으로 React와 react-router-dom으로만 구성된 경우, client component로만 구성된다는 것을 알고 시작하자.
먼저 `app/page.tsx' 내용을 모두 지우고 아래 내용을 넣어 `yarn dev`로 실행해보자.
// src/app/page.tsx
export default function Home() {
console.log("Hello Hello");
return (
<div className="p-8">
Next.js를 시작해보자
</div>
);
}
그러면 브라우저 콘솔창에는 콘솔이 나오지 않지만 터미널에서는 'Hello Hello'가 적혀있는 것을 확인할 수 있다.
즉 해당 페이지는 서버에서 렌더링된 페이지라는 것이다. 자세히 말하자면 브라우저가 아닌 서버를 돌리고 있는 `localhost:3000`에서 콘솔이 출력되는 것이다.
이번에는 아래 파일로 바꿔서 실행해보자.
// src/app/page.tsx
"use client";
export default function Home() {
console.log("Hello Hello");
return (
<div className="p-8">
Next.js를 시작해보자
</div>
);
}
그러면 브라우저 콘솔창에 'Hello Hello'가 출력되는 것을 확인할 수 있다.
차이점은 파일 첫줄에 `"use client"`가 적혀있냐 없냐 차이이다. 즉 `use client`를 추가하면 해당 컴포넌트는 client에서 렌더링이 이루어진다. 사용자와 상호작용이 필요한 페이지는 'use client'를 사용해 client 사이드에서 렌더링이 이루어지도록 해야한다. 따라서 다양한 react hook을 사용하려면 `use client`를 추가해야 한다. 빼고 실행해보면 에러를 띄우는 것을 확인할 수 있다.
1. 언제, 어떻게 Server/Client component를 사용해야 할까?
Next.js 홈페이지에서 위와 같이 제시하고 있다. 즉 시작할 때 이야기한 것처럼 사용자과 상호작용이 있는 경우 client component를, 데이터를 가져오는 등 그 외 경우에는 server component를 사용하도록 권장하고 있다.
Next.js의 기본 component는 server component이다. 따라서 `onClick`, `useState` 등이 필요한 컴포넌트는 따로 분리해야 한다. 따라서 하나의 `button` 태그에 `onClick` 이벤트를 주기 위해서 전체 컴포넌트를 client component로 바꾸기보다 해당 버튼만 client component로 작성해서 연결해주면 된다.
// src/app/page.tsx
import Button from '@/components/Button.tsx'
export default function Home() {
return (
<div className="p-8">
Next.js를 시작해보자
<section>
<h1>제목</h1>
<p>내용</p>
<ul>
<li>항목1</li>
<li>항목2</li>
<li>항목3</li>
</ul>
</section>
<Button/>
</div>
);
}
// src/components/Button.tsx
'use client'
const Button = () => {
return (
<button
onClick={() => {
alert("클릭!");
}}
>
클릭해보세요
</button>
);
};
export default Button;
2. 4가지 렌더링 방식 : CSR, SSG, SSR, ISR
그러면 server component와 client component의 렌더링 방식은 어떤 차이점이 있을까?
시작하기 전에 MPA(Multi-Page Application)와 SPA(Single-Page Application)에 대해 간단히 살펴보자.
MPA는 말 그대로 페이지 개수 만큼 html 파일을 만들어 사용하는 방법으로, 옛날에 사용되던 방식이다. 페이지 이동 및 렌더링 시 깜빡이는 현상이 있고, 컨텐츠 양에 따라 페이지별 편차가 심해져 결국 UX가 저하된다. 이러한 문제를 해결하기 위해 React, Angular, Vue와 같은 SPA가 등장했다.
SPA는 root라는 id를 가진 div만 다운로드해 JS bundle을 통해 UI가 완성된다. 이를 통해서 새로고침이나 깜빡거림이 적은 웹서비스 이용이 가능해져 UX가 크게 향상됐다. 하지만 JS bundle이 모두 불러와질 때까지 기다려야해 초기 로딩이 길다는 단점이 대두되었다. 이를 해결하기 위해 Next.js에서 사용하는 '코드 스플리팅' 방법이 제시되었다. 즉 당장 필요하지 않은 코드는 나중에 불러오는 방법이다.
CSR (Client Side Rendering)
브라우저에서 JS를 이용해 동적으로 페이지를 렌더링하는 방식이다.
렌더링 주체는 이름처럼 클라이언트이며, 사용자와의 상호작용이 빠르고 부드럽고 서버에 추가 요청을 보내지 않으므로 사용자 경험이 좋고 서버 부하가 적다는 장점이 있다.
하지만 앞서 말했듯이 첫 페이지 로딩 시간(Time To View, TTV)이 길 수 있고 JS가 로딩되고 실행될 때까지 페이지가 비어 있어 SEO(검색 엔진 최적화)에 불리하다는 단점이 있다.
SSG (Static Site Generation)
서버에서 페이지를 렌더링해 클라이언트에게 html을 전달하는 방식이다.
최초 빌드 시에만 생성되며 서버에서 미리 정적 페이지를 여러 개 렌더링해놓고 client가 요청하면 바로 제공해 TTV가 짧고 SEO에 유리하다는 장점이 있다.
하지만 정적인 데이터에만 사용할 수 있으며 사용자와의 상호작용이 서버와의 통신에 의존해 CSR보다 상호작용이 느릴 수 있고 서버 부하가 클 수 있다는 단점이 있다.
ISR (Incremental Static Regeneration)
SSG처럼 먼저 정적 페이지를 제공하고 설정한 주기에 페이지를 재생성하는 방식이다.
정적 페이지를 먼저 제공하므로 UX가 좋고, 설정 주기마다 페이지를 재생성하므로 페이지를 비교적 최신 상태로 유지할 수 있다는 장점이 있다.
하지만 동적인 컨텐츠를 다루기에 한계가 있으며 마이페이지 같은 데이터에 의존하는 화면을 그려주는 경우에는 사용할 수 없다는 단점이 있다.
SSR (Server Side Rendering)
SSG, ISR처럼 렌더링 주체가 서버이며, 클라이언트가 요청할 때 렌더링하는 방식이다.
빠른 로딩 속도와 높은 보안성을 제공하며, SEO에 좋다. 실시간 데이터를 사용하며 데이터에 의존한 페이지를 구성할 수 있다.
하지만 컨텐츠가 변경되면 전체 페이지를 다시 빌드해야 하므로 시간이 오래 거릴 수 있고 서버 과부하가 발생할 수 있다는 단점이 있다.
3. 코드로 구현해보기
간단한 데이터를 json-server에서 가져오는 상황을 가정해보자. CSR을 제외하면 `/app/page.tsx` 파일만 조금씩 변경된다.
그전에 type, component와 db.json을 저장해놓자.
./src/types/type.ts
export type Product = {
id: number;
handle: string;
availableForSale: boolean;
isNew: boolean;
title: string;
description: string;
descriptionHtml: string;
options: {
name: string;
values: string[];
}[];
price: {
amount: string;
currencyCode: string;
};
images: string;
seo: {
title: string;
description: string;
};
tags: string[];
rating: number;
};
./src/components/ProductList.tsx
import { Product } from "@/types/type"
const ProductList = ({ products }: { products: Product[] }) => {
return (
<div className="p-8 m-4">
{products.map((product) => (
<div className="flex border p-4 gap-4 rounded-md" key={product.id}>
<img
className="rounded-smr"
width={150}
height={150}
src={product.images}
alt={product.title}
/>
<div className="flex flex-col justify-between">
<div>
<h2 className="text-xl font-bold">{product.title}</h2>
<p className="text-sm">{product.description}</p>
<p className="mt-4 text-2xl">{product.price.amount}$</p>
</div>
</div>
</div>
))}
</div>
)
}
export default ProductList
./db.json
{
"products": [
{
"id": 1,
"handle": "fjallraven-foldsack-no-1-backpack",
"availableForSale": true,
"isNew": false,
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
"description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
"descriptionHtml": "<p>Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday</p>",
"options": [
{
"name": "Size",
"values": ["One Size"]
}
],
"price": {
"amount": "109.95",
"currencyCode": "USD"
},
"images": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
"seo": {
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
"description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday"
},
"rating": 4.2,
"tags": ["men's clothing", "backpack"]
},
{
"id": 2,
"handle": "mens-casual-premium-slim-fit-t-shirts",
"availableForSale": true,
"isNew": false,
"title": "Mens Casual Premium Slim Fit T-Shirts",
"description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
"descriptionHtml": "<p>Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.</p>",
"options": [
{
"name": "Size",
"values": ["S", "M", "L", "XL"]
}
],
"price": {
"amount": "22.3",
"currencyCode": "USD"
},
"images": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
"seo": {
"title": "Mens Casual Premium Slim Fit T-Shirts",
"description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing."
},
"rating": 4.2,
"tags": ["men's clothing", "t-shirt"]
}
],
"carts": []
}
SSG
서버에서 렌더링할 때에는 컴포넌트를 비동기로 만들 수 있다.
import ProductList from "@/components/ProductList";
import { Product } from "@/types/type";
export default async function Home() { // 비동기 컴포넌트 가능
// 따라서 직접 fetch 가능
const response = await fetch('http://localhost:5000/products');
const products: Product[] = await response.json();
console.log('rendered') // 터미널에 출력됨
return (
<div>
<h1>Products</h1>
<ProductList products={products} />
</div>
);
};
SSR
위 코드에서 fetch할 때 cache에 대한 정보를 넣어준다.
import ProductList from "@/components/ProductList";
import { Product } from "@/types/type";
export default async function Home() { // 비동기 컴포넌트 가능
// 따라서 직접 fetch 가능
const response = await fetch('http://localhost:5000/products',{
cache: "no-store", // default는 "force-cache"로 SSG일 때 사용
});
const products: Product[] = await response.json();
console.log('rendered') // 터미널에 출력됨
return (
<div>
<h1>Products</h1>
<ProductList products={products} />
</div>
);
};
ISR
재생성되는 주기에 대한 정보를 넣어준다.
import ProductList from "@/components/ProductList";
import { Product } from "@/types/type";
export default async function Home() { // 비동기 컴포넌트 가능
// 따라서 직접 fetch 가능
const response = await fetch('http://localhost:5000/products',{
next: {
revalidate: 3, // 단위: 초
}
});
const products: Product[] = await response.json();
console.log('rendered') // 터미널에 출력됨
return (
<div>
<h1>Products</h1>
<ProductList products={products} />
</div>
);
};
정확히는 불러온 데이터가 3초가 지나면 stale, 지난 데이터가 되고, 서버에서 내부적으로 새로운 데이터를 불러온다. 새로고침하기 전에는 기존의 cache된 데이터(stale data)를 보여주고, 새로고침 후에는 새로 불러온 컨텐츠를 적용한다.
CSR
React로만 작성하듯이, 작성하면 된다. `app/page.tsx`에서 아무것도 하지 않고 `ProductList.tsx`를 사용하되 `ProductList.tsx`에서 기존처럼 `useEffect`와 `useState`를 통해 데이터를 불러오고 저장하면 된다.
이때 한 가지 주의할 점은 만약 server component를 작성하고 client component로 변경한다면 컴포넌트에 async가 남아있으면 안된다.
// ./src/app/page.tsx
import ProductList from "@/components/ProductList3";
export default function Home() {
console.log('rendered')
return (
<div>
<h1>Products</h1>
<ProductList />
</div>
);
};
// ./src/components/ProductList.tsx
'use client';
import { Product } from '@/types/type';
import React, { useEffect, useState } from 'react'
const fetchData = async () => {
const response = await fetch('http://localhost:5000/products');
const products: Product[] = await response.json();
return products;
}
const ProductList = () => {
const [data, setData] = useState<Product[]>([]);
useEffect(()=>{
console.log('rendered')
fetchData().then(setData);
},[])
return (
<div className="p-8 m-4">
{data.map((product) => (
<div className="flex border p-4 gap-4 rounded-md" key={product.id}>
<img
className="rounded-smr"
width={150}
height={150}
src={product.images}
alt={product.title}
/>
<div className="flex flex-col justify-between">
<div>
<h2 className="text-xl font-bold">{product.title}</h2>
<p className="text-sm">{product.description}</p>
<p className="mt-4 text-2xl">{product.price.amount}$</p>
</div>
</div>
</div>
))}
</div>
)
}
export default ProductList
'Frontend > Today I Learned' 카테고리의 다른 글
[Next.js] Middleware로 잘못된 경로 접근 시 redirect하기 (2) | 2024.10.03 |
---|---|
[Next.js] TanStack Query로 count up/down 기능 구현하기 (2) | 2024.10.01 |
[TS] TypeScript 시작하기 - 2 (1) | 2024.09.25 |
[TS] TypeScript 시작하기 (4) | 2024.09.24 |
[CSS] position : fixed, sticky와 width:inherit (1) | 2024.09.02 |