카테고리 없음

2-2. MPA부터 SSR까지

개발_자 2024. 10. 15. 01:44

MPA

전통적인 서버 사이드 렌더링 방식인 MPA로부터 프론트엔드 웹개발이 시작됨

페이지 이동 시 및 렌더링 시 깜빡거리는 현상이 있고 컨텐츠의 양에 따라서 페이지 별 편차가 심해짐. 결국 UX가 저하됨

이러한 문제 때문에 React, Angular, Vue 등 SPA(Single Page Application)이 등장

SPA

브라우저에서 동작하는 Javascript를 이용해 동적으로 페이지, 컴포넌트 등을 렌더링 하는 방식

'Client의 사이드에서 렌더링을 한다'라는 개념은 기존 프론트엔드 개발자들에게 획기적 방법으로 소개

--

최초 서버로부터는 텅 빈, root라는 id를 가진 div만 다운로드 ⇒ Javascript Boundle을 통해 UI가 완성

더 이상 새로고침이나 깜빡거림 없이 웹 서비스 이용이 가능하여 UX가 크게 향상

그러나 다음과 같은 단점이 새롭게 대두됨

  • 늦는 초기 로딩 속도
    1. 텅 빈 div만 불러오기 때문에 Javascript의 평가, 실행이 될 때까지 하얀 화면이 유저에게 노출됨
    2. 이를 보완하기 위해 Code Spilitting(Lazy-Loading) 방법 제시
    3. 하나로 번들된 코드를 여러 코드로 나눠 당장 필요한 코드가 아니면 나중에 불러옴
    4. 그러나 태생적으로 완벽하게 해결되지는 못함

4가지 주요 렌더링 기법

  • CRS(Client Side Rendering)
  • 특징
    • 순수 React 사용했을 때 100%
    • 브라우저에게 JavaScript를 이용해 동적으로 페이지를 렌더링하는 방식
    • 렌더링의 주체: 클라이언트
  • 장점
    • (최초 한번 로드가 끝나면) 사용자와의 상호작용이 빠르고 부드럽다.
    • 서버에게 추가적인 요청을 보낼 필요가 없기 때문에, 사용자 경험이 좋다
    • 서버 부하가 적음
  • 단점
    • 첫 페이지 로딩 시간(Time To View)이 길 수 있다.
    • JavaScript가 로딩 되고 실행될 때까지 페이지가 비어있어 검색 엔진 최적화(SEO)에 불리함
import React from 'react';
import ReactDOM from 'react-dom';

function App() {
	return <h1>Hello, Client Side Rendering!</h1>;
}
// index.js
ReactDOM.render(<App />, document.getElementById('root'));
  • SSG(Static Site Generation)

 

  • 특징
    • 서버에서 페이지를 렌더링 하여 클라이언트에게 HTML을 전달하는 방식
    • 최초 빌드 시에만 생성이 됨
    • 사전에 미리 정적 페이지를 만듦 → 클라이언트가 홈페이지 요청을 하면, 서버에서 이미 만들어져 있는 사이트 제공 → 클라이언트는 표기만 함
  • 장점
    • 첫 페이지 로딩 시간이 매우 짧아(TTV) 사용자가 빠르게 페이지를 봄. SEO에 유리
    • CDN(Content Delivery Network) 캐싱 가능
  • 단점
    • 정적인 데이터에만 사용 가능
    • 사용자와의 상호작용이 서버와의 통신에 의존하므로, 클라이언트 사이드 렌더링(CSR)보다 상호작용이 느릴 수 있음. 또한 서버 부하가 클 수 있음
    • 마이페이지 처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가
  • ISR(Incremental Static Regeneration)

 
  • 특징
    • SSG처럼 정적 페이지 제공
    • 설정한 주기만큼 페이지를 계속 생성
      ex) 주기가 10분이라면, 10분마다 데이터베이스 또는 외부 영향 때문에 변경된 사항 반영
    • 정적 페이지를 먼저 보여주고, 필요에 따라 서버에서 페이지를 재생성하는 방식
  • 장점
    • 정적 페이지를 먼저 제공하므로 사용자 경험이 좋으며, 콘텐츠가 변경되었을 때 서버에서 페이지를 재생성하므로 최신 상태를 (그나마) 유지함
    • CDN 캐싱 가능
  • 단점
    • 동적인 콘텐츠를 다루기에 한계가 있을 수 있음(실시간 페이지 아님)
    • 마이페이지 처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가
import React from 'react';

function HomePage({ data }) {
	return <div>{data}</div>;
}

export async function getStaticProps() {
	const res = await fetch('https://...');
	
	// 외부 API 호출
	const data = await res.json();
	return {
		props: { data },
		revalidate: 60, // 1초 후에 페이지 재생성
		};
	}
	
	export default HomePage;
  • SSR(Server Side Rendering)

  • 특징
    • 렌더링 주체 서버
    • 클라이언트의 요청 시 렌더링
      • C → S: 이 페이지 줘!
      • S → C: (데이터베이스 읽고 등등 한 후) html 파일 제공
  • 장점
    • 빠른 로딩 속도(TTV)와 높은 보안성 제공
    • SEO 최적화 좋음
    • 실시간 데이터 사용
    • 마이페이지 처럼 데이터에 의존한 페이지 구성 가능
    • CDN 캐싱 불가
  • 단점
    • 사이트의 콘텐츠가 변경되면 전체 사이트를 다시 빌드해야 하는데, 이 과정의 시간이 오래 걸릴 수 있음 → 서버 과부하
    • 요청할 때 마다 페이지를 만들어야 함
import React from 'react';

function HomePage({ data }) {
	return <div>{data}</div>; 
}

export async function getServerSideProps() {
	const res = await fetch('https://...'); 
	// 외부 API 호출 
	const data = await res.json();
	
	return { props: { data } };
}

export default HomePage;

(종합) 비교

  CSR  SSR  SSG  ISR     
빌드 짧다   짧다   길다   길다      
SEO               나쁨   좋음   좋음   좋음  
페이지 요청에 따른 응답 시간 보통   길다   짧다   짧다  
최신 정보인가?  맞음   맞음   아님   아닐 수 있음
 

Hydration

  1. CSR
    • React에서 CSR로만 컴포넌트 렌더링을 할 때 TTV가 오래 걸림 → 모든 React 소스파일을 다운로드 받아야 화면을 볼 수 있기 때문
    • 최초 서버에선 index.html 파일만 제공하지만, 이후 React 소스파일을 바탕으로 한 JS파일이 모두 다운로드 돼야만(즉, Hydration이 돼야만) 최종 소스코드를 볼 수 있음
      하지만 CSR의 과정에서 Hydration 과정을 Hydration으로 볼 것이냐 하는 이견이 있을 수 있음
  2. SSR
    • 서버에서는 사용자의 요청이 있을 때마다 페이지를 새로 그려서 사용자에게 제공
    • 두 과정으로 나눠 제공
      1. pre-rebdering : 사용자와 상호작용하는 부분을 제외한 껍데기만을 먼저 브라우저에게 제공 → TTV 빠름
      2. hydration : 이 과정이 일어나기 전까지는 껍데기만 있는 html 파일이기 때문에 사용자가 아무리 버튼을 click해도 아무 동작 일어나지 않음. 인터렉션에 필요한 모든 파일을 다운로드 받는 과정 즉, hydration과정이 끝나야 인터렉션 가능. 이 간극! TTI를 줄이는 것이 관건이라 할 수 있음
  3. SSG, ISR도 SSR과 마찬가지로 hydration과정 존재

Pre-Rendering (Static) - Static Site Generation (SSG)

  1. fetch에 아무 옵션 주지 않기
  2. fetch에 fore-cache 옵션 주기
import ProductList from '@/components/ProductList'; 
import { Product } from '@/types';

const HomePage = async () => { 
	const response = await fetch('http://localhost:4000/products', {
		cache: "force-cache"
	});
	
	const products: Product[] = await response.json();
	return ( 
		<div>
			 <h1>Products</h1>
			 <ProductList products={products} />
		</div>
	);
}; 

export default HomePage;
  • 아무리 새로고침 해도 동일한 페이지만 출력됨

Incremental Static Regeneration (ISR)

  1. fetch에 옵션주기
import ProductList from '@/components/ProductList'; 
import { Product } from '@/types';

const ProductsPage = async () => { 
	const response = await fetch('http://localhost:4000/products',
	 next: { 
		 revalidate: 5, 
		}
	 );
	 
	const products: Product[] = await response.json();
	return (
		<div> 
			<h1>Products</h1>
			<ProductList products={products} />
		</div>
		); 
	}; 
	
	export default ProductsPage;
  1. page.tsx 컴포넌트에 revalidate 추가하기
import ProductList from '@/components/ProductList'; 
import { Product } from '@/types'; 

export const revalidate = 5;

const HomePage = async () => { 
	const response = await fetch('http://localhost:4000/products');
	const products: Product[] = await response.json();
	
	return ( 
		<div> 
			<h1>Products</h1> 
			<ProductList products={products} /> 
		</div> 
	); 
}; 

export default HomePage;
  • 주어진 시간에 한 번씩 갱신

Pre-Rendring (Dynamic) -Server Side Rendering (SSR)

  1. fetch에 no-store 옵션 주기
import ProductList from '@/components/ProductList'; 
import { Product } from '@/types'; 

const ProductsPage = async () => { 
	const response = await fetch('http://localhost:4000/products', {
		cache: "no-store"
	});
	
	const products: Product[] = await response.json();
	
	return ( 
		<div> 
			<h1>Products</h1> 
			<ProductList products={products} /> 
		</div> 
	); 
}; 

export default ProductsPage;
  1. page.tsx 컴포넌트에 dynamic 추가하기
import ProductList from '@/components/ProductList'; 
import { Product } from '@/types'; 

export const dynamic = "force-dynamic"; 

const ProductsPage = async () => { 
	const response = await fetch('http://localhost:4000/products');
	const products: Product[] = await response.json(); 
	
	return ( 
		<div> 
			<h1>Products</h1> 
			<ProductList products={products} /> 
		</div> 
	); 
}; 

export default ProductsPage;
  • 요청이 있을 때마다 지속해서 갱신.
  • hydration이 완료되기 전까지의 시간 즉, TTI(Time To Interactive)가 관건

Client Side Rendering (CSR) & use client

  1. "use client" 옵션 주기
"use client" 

import ProductList from '@/components/ProductList'; 
import { Product } from '@/types'; 

const HomePage = async () => { 

	const [products, setProducts] = useState([]); 
	
	useEffect(() => {
		 fetchData();
	},[])
	
	const fetchData = async () => { 
		const response = await fetch('http://localhost:4000/products');
		const products: Product[] = await response.json();
		
		setProducts(products);
	}
	
	return (
		<div> 
			<h1>Products</h1> 
			<ProductList products={products} /> 
		</div> 
	); 
}; 

export default HomePage;
  • 요청이 있을 때 마다 지속해서 갱신
  • client side rendering이기 때문에, loading에 관련된 state 제어를 통해 사용자에게 알려 줄 수 있음