Next.js의 다양한 렌더링 방식 🎐

Sheryl Yun·2023년 5월 20일
0

목차

  • 서버 사이드 렌더링 (SSR)
  • 클라이언트 사이드 렌더링 (CSR)
  • 특정 컴포넌트를 클라이언트에서 렌더링하는 다양한 방법
  • 정적 사이트 생성 (SSG)

서버 사이드 렌더링 (SSR)

  • Next.js에서는 어떤 페이지를 빌드 시점에 정적으로 생성하고 실행 시점에 동적으로 제공할 것인지 지정 가능
  • SSR은 원래 웹 페이지를 제공하는 가장 흔한 방법
    • 예: PHP, 루비, 파이썬 등

SSR 과정

  1. HTML 페이지를 웹 브라우저로 전송하기 전에 서버에서 전부 렌더링
    • 각 요청에 따라 서버에서 HTML 페이지를 동적으로 렌더링
  2. 해당 페이지의 모든 자바스크립트 코드 로드
  3. 동적으로 페이지 내용 렌더링
    • 서버에서 렌더링한 페이지에 스크립트 코드를 넣어 웹 페이지를 동적으로 처리
      = 하이드레이션(hydration)

리액트 하이드레이션
참고 링크: https://react.dev/reference/react-dom/client/hydrateRoot

  • React 18 버전에서 hydrate()는 더 이상 쓰지 않음
    => hydrateRoot()로 바뀜
  • CSR(CRA)에서의 createRoot()
  • 형태: hydrateRoot(domNode, reactNode)
    • 실제 사용: hydrateRoot(document.getElementById('root'), <App />)
  • 반환값: render()와 unmount()를 포함한 객체
    • render() 사용하기: root.render(<App />);
      • 하이드레이션된 리액트 root 내부에서 리액트 컴포넌트 갱신
    • unmount() 사용하기: root.unmount();
      • 트리에 있는 모든 컴포넌트를 언마운트하고, root DOM 노드로부터 리액트 '분리(detach)'

SSR 예시

블로그에서 어떤 사람이 작성한 모든 글을 한 페이지에 렌더링하는 경우

  1. 사용자가 페이지에 접근
  2. 서버가 페이지를 (서버에서) 렌더링
  3. 결과물인 HTML 페이지를 브라우저로 전송
    • 받자마자 화면에서 바로 볼 수 있음
  4. 이후 브라우저가 페이지에서 요청한 모든 스크립트를 다운로드
  5. DOM에 각 스크립트 코드를 하이드레이션
    • 이후 페이지에서 상호작용 가능
  • 하이드레이션 과정으로 SSR에서도 페이지 새로고침 없이 웹 페이지와 상호 작용 가능
    => (CSR의 특징인) SPA처럼 작동하는 SSR 웹앱 만들기 가능
    • SSR과 CSR의 장점을 모두 취함

장점

  1. 안전성 증가
  • 페이지를 서버에서 렌더링
    = 쿠키 관리, API 호출, 데이터 검증 작업을 '서버'에서 처리
    => 중요한 데이터를 클라이언트에 노출할 필요가 없음 = 안전성 증가
  1. 웹 사이트 제공 범위 확대
  • 렌더링을 서버에서 담당
    • 클라이언트가 자바스크립트를 사용하지 못하는 환경이거나 오래된 브라우저에서도 웹 페이지 제공 가능
  1. 검색엔진최적화(SEO) 유리
  • 서버에서 렌더링에 필요한 모든 데이터를 넣어 HTML 생성
    • 웹 크롤러 등의 검색 엔진 웹 문서 수집기가 필요한 데이터를 모두 수집
      => 웹앱의 SEO 점수 상승

단점

  1. 자원 소모, 서버 부하 증가
  • SSR을 사용하면 '서버'가 필요
  • CSR이나 SSG는 클라우드 서비스에 배포하는 serverless 방식을 사용 가능하지만 SSR은 서버를 사용해야 함
    = 웹앱이 더 많은 자원을 소모하고 부하를 보이며 유지 보수 비용이 증가
  1. 페이지 이동 시 요청 처리 시간이 더 길어짐
  • 페이지 렌더링 때마다 매번 데이터 접근 및 외부 API 호출 발생
    => 해결: Next.js에서 이 성능을 향상 시킬 수 있는 기능 제공

getServerSideProps로 동적 렌더링

  • Next.js는 기본적으로 빌드 시점에 정적으로 페이지를 생성
  • 페이지에서 외부 API를 호출하거나 DB에 접근하는 등 동적 작업을 할 경우
    • Next.js가 제공하는 예약 함수(getServerSideProps)를 활용

예시

  1. 초기에는 문자열만 표시하는 정적 페이지
function IndexPage() {
	return <div>여기는 index 페이지입니다.</div>;
}

export default IndexPage;
  1. 요청 때마다 사용자 환영 문구를 표시하는 페이지로 변경
  • 사용자 별로 서로 다른 페이지를 볼 수 있는 것 (= 동적 렌더링)
export async function getServerSideProps() {
	const userRequest = await fetch('https://example.com/api/user');
    const userData = await userRequest.json();
    
    return {
    	props: {
        	user: userData,
        },
    };	
}

function IndexPage(props) {
	return <div>안녕하세요, {props.user.name}님!</div>;
}

export default IndexPage;

getServerSideProps 예약 함수 사용
서버 API를 미리 호출하여 동적 렌더링에 필요한 값(userData) 호출

  1. getServerSideProps라는 비동기(async) 함수를 페이지 내에서 export
    • 빌드 과정에서 Next.js가 getServerSideProps를 export하는 모든 페이지를 찾아서 서버가 페이지 요청 처리 시 이 함수를 호출하도록 함
      => getServerSideProps 함수 코드는 항상 서버에서만 실행
    • getServerSideProps 함수 안에서 fetch API가 실행되도록 하는 것은 별도의 polyfill 없이 Next.js가 알아서 처리
  2. 반환 값: props 프로퍼티를 갖는 객체
    • 이 props가 컴포넌트 함수에 인자로 전달됨
      • 서버와 클라이언트(컴포넌트) 모두가 props에 접근 및 사용 가능

주의점

  • Next.js는 기본적으로 페이지를 서버에서 렌더링 (=> Node.js 사용)
    • 브라우저 전용 객체인 window, document나 브라우저 전용 API를 사용할 수 없음
      => 브라우저 전용인 것을 사용하려면 사용되는 컴포넌트를 반드시 브라우저에서 렌더링하도록 명시적으로 지정해야 함
      • 다양한 방법이 있음 (CSR 다음에 소개)

클라이언트 사이드 렌더링 (CSR)

  • 표준 리액트 앱(CRA)이 사용하는 방식

CSR 과정

  1. 자바스크립트 번들을 서버에서 전송 받은 후 클라이언트에서 렌더링

    • 이때 받는 HTML은 스크립트(<script>)와 스타일(<link>)을 포함된 기본 HTML 마크업
      • body에 <div id="root"></div>
  2. 빌드 과정 동안 컴파일한 JS 파일과 CSS 파일을 HTML 페이지에서 불러오고 <div id="root"></div> 안에 불러온 내용을 렌더링

    • 이 과정 동안 웹 브라우저 화면이 비어있게 됨
      • 초기 로딩이 느리게 느껴짐

장점

  1. 네이티브 앱처럼 부드러운 화면 전환
  • 전체 자바스크립트 번들을 다운로드
    = 웹 애플리케이션이 렌더링할 모든 페이지를 이미 브라우저가 다운로드한 상태
  • 다른 페이지로 이동할 때 서버에서 새로운 컨텐츠를 다운로드하지 않고 해당 컨텐츠로 갈아끼움
    => 컨텐츠 변경을 위해 페이지 새로고침 필요 없음 (= 스무스한 화면 전환)
  1. 페이지 전환 간에 효과 추가 가능
  • 화면을 새로 고침할 필요 없이 다른 페이지로 이동 가능
    => 화면 전환 간에 멋진 효과를 쉽게 넣을 수 있음
  1. 지연 로딩 (lazy loading)
  • CSR은 기본적으로 페이지 표현에 최소로 필요한 HTML 마크업만 렌더링
  • 이후 버튼 클릭 등으로 (예를 들어) 모달을 띄우면 모달 관련한 마크업을 그때 리액트가 동적으로 생성
    • 그 전에는 HTML에 모달 관련 마크업 없음 (=> 지연 로딩 가능)
  1. 서버 부하 감소
  • 전체 렌더링 과정을 모두 클라이언트에서 진행
    • 서버는 간단한 HTML만 내려보내므로 할 일이 거의 없음
  • Firebase와 같은 serverless 환경에서 웹앱을 제공하는 것도 가능

단점

  1. HTML에 등록된 JS 코드와 CSS 파일을 클라이언트에서 받아서 적용
    • 초기 화면 로딩에 수 초가 걸릴 수 있음
      • 이 과정에서 사용자는 빈 페이지를 보고 있어야 함
  2. 웹앱의 SEO에 불리
    • 크롤러 봇이 수집해갈 페이지 정보가 없음
      • 초기 HTML의 root div 안에 아무 것도 없어서
    • 구글 봇의 경우 자바스크립트 번들이 전송될 때까지 기다리지만
      • 또한 이 기다리는 시간 때문에 웹 사이트 성능 점수가 낮아짐

특정 컴포넌트를 무조건 브라우저에서 렌더링하는 다양한 방법

  • Next.js는 기본적으로 페이지의 리액트 컴포넌트를 서버에서 렌더링 또는 빌드 시점에 미리 렌더링
  • Node.js 런타임이 window, document 등의 브라우저 전용 APIcanvas와 같은 HTML 요소를 제공하지 않으므로
    • 서버 렌더링 시 브라우저 전용 객체나 API에 접근하면 렌더링이 실패

=> 특정 컴포넌트를 브라우저(클라이언트)에서만 렌더링하도록 하는 방식으로 해결 가능

1. useEffect 활용

  • 함수형 컴포넌트에서 DOM 조작이나 데이터 불러오기와 같은 렌더링 이후의 side effec를 구현
  • 컴포넌트가 마운트된 이후 useEffect가 실행
    • 이 가정에서 컴포넌트의 특정 작업을 반드시 클라이언트(브라우저)에서 실행하도록 강제할 수 있음

예시

Highlight.js 라이브러리 적용

  • 코드 구문에 강조 표시하여 웹 페이지의 코드 부분을 더 읽기 쉽게 해주는 라이브러리
import Head from 'next/head';
import hljs from 'highlight.js';
import javascript from 'highlight.js/lib/languages/javascript';

function Highlight({ code }) {
	hljs.registerLanguage('javascript', javascript);
    hljs.initHighlighting();
    
    return (
    	<>
        	<Head>
            	<link rel='stylesheet' href='/highlight.css' />      
            </Head>
            <pre>
            	<code className='js'>{code}</code>
            </pre>
        </>
    );
}

export default Highlight;
  • Highlight.js 라이브러리는 동작 과정에서 document라는 브라우저 전역 변수 사용
    • Node.js에서 제공하지 않고 브라우저에서만 제공
      => 리액트에서는 문제 없이 작동하지만 Next.js에서는 문제가 생김

useEffect로 해결하기

hljs 호출 부분을 useEffect로 감싸준다.

  • 컴포넌트가 마운트된 이후 hljs 코드가 '클라이언트'에서 실행되도록 처리
import { useEffect } from 'react'; // 추가
import Head from 'next/head';
import hljs from 'highlight.js';
import javascript from 'highlight.js/lib/languages/javascript';

function Highlight({ code }) {

    // useEffect로 hljs 로직을 감싸주기
	useEffect(() => { 
    	hljs.registerLanguage('javascript', javascript);
    	hljs.initHighlighting();
    }, []);
    
    return (
    	<>
        	<Head>
            	<link rel='stylesheet' href='/highlight.css' />      
            </Head>
            <pre>
            	<code className='js'>{code}</code>
            </pre>
        </>
    );
}

export default Highlight;

useEffect와 useState를 함께 활용하기

  • 컴포넌트를 정확히 클라이언트에서만 렌더링하도록 지정
import { useState, useEffect } from 'react';
import Highlight from '../components/Highlight'; // // 위 Highlight 컴포넌트를 import

function UseEffectPage() {
	const [isClient, setIsClient] = useState(false); // useState 추가
    
    // 렌더링 잉후 useState 상태 변경
    useEffect(() => {
    	setIsClient(true);
    }, []);
    
    return (
    	<div>
            // useState 상태에 따라 Highlight 컴포넌트 함수 실행
        	{isClient && (
            	<Highlight code={"console.log('하이')"} language='js' /> 
            )}
        </div>
    );
}

export default UseEffectPage;

이제 Highlight 컴포넌트는 브라우저(클라이언트)에서만 렌더링된다.

2. typeof window 활용

  • 원래 process.browser 라는 boolean 값을 활용해서 클라이언트 여부를 식별했지만 Vercel에서 곧 지원 중단 예정
  • 대신 같은 기능을 typeof window가 대신함
// 예전 코드
function IndexPage() {
	const side = process.browser ? 'client' : 'server';
    
    return <div>현재 {side} 사이드 렌더링 중입니다</div>
}

export default IndexPage;

처음에는 '현재 server 사이드 렌더링 중입니다'를 표시하다가
리액트 하이드레이션이 끝나면 바로
'현재 client 사이드 렌더링 중입니다'로 바뀜

좀 더 정확한 의미를 갖는 typeof window 사용

// 지금 코드
function IndexPage() {
	const side = typeof window === 'undefined' ? 'server' : 'client';
    
    return <div>현재 {side} 사이드 렌더링 중입니다</div>
}

export default IndexPage;

window 객체가 정의되지 않으면(undefined이면) 서버 사이드,
window 객체가 존재하면(undefined가 아니면) 클라이언트 사이드

3. 동적 컴포넌트 로딩 (동적 import)

dynamic 모듈

  • Next.js에서 제공하는 기능 중 하나 (리액트에는 없음)
  • 브라우저에서 코드를 실행하는 경우에만 컴포넌트를 렌더링
import dynamic from 'next/dynamic'; // next/dynamic에서 import

// Highlight 컴포넌트를 dynamic 모듈을 활용하여 가져옴
// 형태: dynamic(callback, options)

const Highlight = dynamic(
	() => import('../components/Highlight'),
    { ssr: false },
);

import styles from '../styles/Home.module.css';

function DynamicPage() {
	return (
    	<div className={styles.main}>
        	// 동적으로 가져온 Highlight 컴포넌트
        	<Highlight code={"console.log('바이')"} language='js' />
        </div>
    );
}
  • ssr: false 옵션
    • 클라이언트에서만 코드를 실행(= 컴포넌트를 import)하는 옵션
  • 동적 import로 컴포넌트를 불러오면 클라이언트에서만 렌더링
    = 리액트 하이드레이션까지 끝나야 해당 컴포넌트와 상호 작용 가능

CSR이 SSR보다 더 좋은 경우

검색 엔진에 노출될 필요가 없는 웹 페이지를 만들 때

정적 사이트 생성 (SSG)

SSG 과정

  • 페이지를 빌드 시점에 미리 렌더링
  • 매번 빌드할 때마다 내용이 거의 변하지 않을 때
  • 빌드 과정에서 정적 페이지로 미리 렌더링
    • HTML 마크업 형태로 제공 (=> SSR과 비슷)
  • 정적 페이지이지만 리액트 하이드레이션 덕분에 사용자와 웹 페이지 간 상호 작용 가능

장점

  1. 캐싱 용이
  • 정적 페이지는 단순 HTML 파일
    • CDN으로 파일을 제공하거나 캐싱하기 쉬움
  1. 서버 부하 x
  • 정적 자원이므로 서버에서 제공 시 별도의 연산이 필요 없음
  1. 웹 사이트 전반적인 성능 상승
  • 빌드 시점에 HTML 페이지를 미리 렌더링
    • 서버는 정적 파일을 보내기만 하고, 클라이언트는 파일을 받아서 표시만 함
  • 변하는 데이터가 없으므로 서버 측에 데이터 요청도 없음
    • API 요청에 따르는 지연 x
  1. 안전성 증가
  • 필요한 모든 정보가 미리 렌더링되어 있음
    • API를 호출하거나 DB에 접근 및 보호가 필요한 민감한 데이터를 다루지 않음

단점

  • 웹 페이지를 한번 만들고 나면 다음 배포 전까지 내용이 변하지 않음
    • 예: 블로그에 새로운 글을 올렸는데 올린 글의 제목에서 오타 발견했을 때
      • 오타 한 글자를 수정하기 위해 필요한 데이터를 가져오고 정적 페이지를 다시 생성하는 과정을 다시 반복
      • 해결: ISR 병행 (아래 소개)

활용

  • 검색 엔진을 신경 쓸 필요가 없는 페이지일 때
  • 클라이언트에 정적 페이지를 전달하면서 필요한 데이터도 한번에 브라우저로 전송해야 할 때
    • 예: 관리자(어드민) 페이지, 비공개 프로필 페이지

증분 정적 재생성 (ISR)

Incremental Static Regeneration의 약자

  • SSG의 문제점
    • 빌드 시점에 페이지가 미리 렌더링되어 정적 자원처럼 제공됨
      => 아주 작은 부분의 수정이 있어도 데이터를 다시 가져와서 정적 페이지를 재생성해야 함
  • 해결: 증분 정적 재생성(ISR)
    • Next.js에서 제공하는 독특한 렌더링 방식
    • 정적 페이지를 업데이트하고 다시 렌더링할 주기를 지정 (throttle 느낌..?)

예시

동적 컨텐츠를 제공하지만 해당 데이터를 가져오는 데 아주 오랜 시간이 걸리는 웹 페이지
=> SSR 또는 CSR을 사용하면 끔찍한 사용자 경험 초래

  • 해결
    • SSG(정적 사이트 생성)와 ISR(증분 정적 재생성)을 함께 사용
      • SSG(정적 사이트 생성)와 SSR(서버 사이드 렌더링)을 섞어 쓰는 것과 비슷

예: 엄청나게 많은 데이터를 가져와야 하는 복잡한 대시보드

  • 데이터를 불러오기 위한 API 호출에 수 초가 소요됨
  • 호출되는 데이터가 자주 변하는 데이터가 아니라면 SSG와 ISR을 함께 사용하여 특정 시간(예: 10분) 동안 데이터를 캐싱

해결: getStaticProps 함수의 revalidate

  • 반환할 때 props와 같은 레벨에 revalidate 속성을 주고 캐싱할 시간 지정
import fetch from 'isomorphic-unfetch';
import Dashboard from './components/Dashboard';

export async function getStaticProps() {
	const userReq = await fetch('/api/user');
    const userData = await userReq.json();
    
    const dashboardReq = await fetch('/api/dashboard');
    const dashboardData = await dashboardReq.json();
    
    return {
    	props: {
        	user: userData,
            dashboard: dashboardData,
        },
        revalidate: 600 // 시간 지정 (주의: second 단위 / 1분은 60 => 600은 10분)
    };
}

function IndexPage(props) {
	return (
    	<div>
        	<Dashboard user={props.user} dashboard={props.dashboard} />
        </div>
    );
}

export default IndexPage;
  • Next.js가 빌드 과정에서 getStaticProps 함수를 호출해서 데이터를 호출하고
    • 정적 페이지를 렌더링한 후에 페이지 저장
  • 이후 revalidate로 지정한 시간까지 도중에 데이터 변경 요청이 있더라도 이 함수를 호출하지 않음 (= 페이지가 변경되지 않음)
  • revalidate 시간이 지나면 이후에 들어오는 데이터 변경 요청은 처리
    • 새로운 데이터를 받아와서 새로운 정적 페이지를 만든 후, 이전에 저장해둔 정적 페이지를 새로운 페이지로 덮어씀
    • 다음 revalidate 시점까지는 다시 동일한 페이지에 대한 모든 요청에 대해 저장된 새로운 정적 페이지를 제공
  • 10분이 지난 후에 시간이 지났더라도 새로운 요청이 없으면 새 정적 페이지를 빌드하지 않음 (요청이 있을 때만 동작)

정리

  • Next.js의 4가지 렌더링 전략 (SSR, CSR, SSG, ISR)
  • 각 방식의 장단점과 활용 예시
  • 컴포넌트를 클라이언트에서만 렌더링하는 방법 3가지

다양한 렌더링 방법을 선택할 수 있다는 점은 Next.js를 사용하는 중요한 이유이기도 하다.

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글