카테고리 없음

넥스트와 서버 컴포넌트

Ahyeon, Jung 2024. 5. 30. 14:59

농장주인 분들이 주로 사용하기 때문에, 나이대가 있을 것이라 생각되어 SSR을 위해 Next.js를 선택하였다. 저사양 기기에서도 빠른 렌더링을 하고 싶었다. SSR을 하자고 생각하다보니 더 나아가 서버 컴포넌트를 활용하여 효율적으로 렌더링하고 싶기도했다. 그러다가 알아보니 Next.js의 fetch가 캐싱 기능을 도와준다고 한다. 사실 tanstack-query가 너무 시장 지배적인거 아닌가 하는 생각이 있었기 때문에 api 라이브러리를 따로 쓰지 않고 fetch를 활용해보기로 했다. 사실 애초에 서버에서 api 요청을 끝내고 올 것이기 때문에 굳이 클라이언트에서 캐싱을 처리할 필요가 없다. 또한 CSS-in-JS 기반의 styled-components 또한, "use client"가 선언되면서 클라이언트 사이드에서 스타일링이 되기 때문에, 서버에서 페이지를 구성해서 전달해줄 내 목적에는 맞지 않아 tailwindCSS를 선택했다.


클라이언트 컴포넌트와 서버 컴포넌트는 React 18에서 도입된 개념으로 Next.js 등의 프레임워크에서 활용가능하다. 각각의 컴포넌트는 렌더링되는 위치와 용도에 따라 다르다.

클라이언트 컴포넌트(Client Component)

브라우저에서 렌더링되고, 사용자 인터렉션과 관련된 동작을 처리하는 컴포넌트

상태 관리, 이벤트 핸들링, 라이브러리 로딩 등에 적합

'use client'를 파일의 최상단에 입력하면, 하위 구성요소를 포함하여 해당 파일로 가져온 다른 모든 모듈이 클라이언트 번들의 일부로 간주

'use client';

import React, { useState } from 'react';

const ClientComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default ClientComponent;

 

서버 컴포넌트(Server Component)

서버에서 렌더링되고, 초기 데이터 페칭, SEO 최적화, 성능 최적화 등에 적합

서버 컴포넌트는 클라이언트로 전송되기 전에 서버에서 HTML을 생성하여 클라이언트에 전달

모든 컴포넌트는 서버 컴포넌트가 기본

클라이언트에서 라이브러리리 등을 다운로드하고 실행할 필요 없기 때문에 자바스크립트 번들이 감소

Event Listener, React Hooks, Client Component , DOM API사용이 불가능

import React from 'react';

const ServerComponent = async () => {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();

  return (
    <div>
      <h1>Server Rendered Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default ServerComponent;

 

서버 컴포넌트와 SSR

SSR은 서버에서 렌더링된 HTML을 가져오지만, 서버 컴포넌트는 서버에서 HTML을 생성하여 렌더링 할 트리 객체를 가져온다.

서버와 클라이언트 컴포넌트의 렌더링 과정

  1. 서버가 렌더링 요청을 받는다. 서버가 렌더링 과정을 수행해야 하므로 리액트 서버 컴포넌트를 사용하는 모든 페이지는 항상 서버에서 시작된다. 즉, 루트에 있는 컴포넌트는 항상 서버 컴포넌트다.
  2. 서버는 받은 요청에 따라 컴포넌트를 JSON으로 직렬화한다. 이때 서버에서 렌더링할 수 있는 것은 직렬화해서 내보내고, 클라이언트 컴포넌트로 표시된 부분은 해당 공간을 플레이스홀더 형식으로 비워두고 나타낸다. 브라우저는 이후에 이 결과물을 받아서 다시 역직렬화한 다음 렌더링을 수행한다. 
  3. 브라우저가 리액트 컴포넌트 트리를 구성한다. 브라우저가 서버 스트리밍으로 JSON 결과물을 받았다면 이 구문을 다시 파싱한 결과물을 바탕으로 트리를 재구성해 컴포넌트를 만들어 나간다. 클라이언트 컴포넌트를 받았다면 클라이언트에서 렌더링을 진행할 것이고, 서버에서 만들어진 결과물을 받았다면 이 정보를 기반으로 리액트 트리를 그대로 만들 것이다. 그리고 최종적으로 이 트리를 렌더링해 브라우저의 DOM에 커밋한다.

 

서버에서 렌더링된 부분은 즉시 표시되지만, 클라이언트 컴포넌트는 JavaScript가 로드되고 실행될 때까지 렌더링되지 않는다. 즉, 클라이언트 컴포넌트는 마운트될 때까지 빈자리로 남아있다.

Skeleton UI

클라이언트 컴포넌트가 마운트되기 전까지 사용자에게 빈 공간 대신 로딩 스피터나 뼈대 UI를 보여준다

React의 Suspense 컴포넌트

React 18 이후, Suspense 컴포넌트를 사용하여 클라이언트 컴포넌트가 마운트될 때까지 로딩 상태를 보여준다

next/dynamic

클라이언트 컴포넌트를 동적으로 로드할 때, loading 속성을 사용하여 로딩 상태를 표시한다.

import dynamic from 'next/dynamic';
import ServerComponent from '../components/ServerComponent';

const ClientComponent = dynamic(() => import('../components/ClientComponent'), {
  ssr: false,
  loading: () => <p>Loading...</p>,
});

const HomePage = () => {
  return (
    <div>
      <ServerComponent />
      <ClientComponent />
    </div>
  );
};

export default HomePage;

 


기존 Next.js에서 데이터 패칭

getServerSideProps를 이용한 데이터 패칭

getServerSideProps는 페이지가 요청될 때마다 서버에서 실행되어 데이터를 패칭

import React from 'react';

export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: {
      data,
    },
  };
}

const HomePage = ({ data }) => {
  return (
    <div>
      <h1>Server Rendered Data with Caching</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default HomePage;

getStaticProps를 이용한 데이터 패칭

빌드 타입에 데이터를 패칭하여 정적 페이지를 생성(SSG)

페이지 로딩 속도를 크게 향상

revalidate 옵션을 사용하여 ISR(Incremental Static Regeneration)을 통해 일정 시간마다 페이지 재생성 가능

import React from 'react';

export async function getStaticProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: {
      data,
    },
    revalidate: 60, // 60초마다 페이지를 재생성
  };
}

const HomePage = ({ data }) => {
  return (
    <div>
      <h1>Server Rendered Data with ISR</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default HomePage;

 

fetch를 사용하고 cache-control 헤더를 설정

export default async function handler(req, res) {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();

  res.setHeader('Cache-Control', 's-maxage=600, stale-while-revalidate');

  res.status(200).json(data);
}

 

App Router 에서 데이터 패칭

그러나 Next.js 13에서 도입된 새로운 앱 라우터에서는 getServerSideProps, getStaticProps 등의 데이터 패칭 방식이 제거되었다. 대신에 fetch 함수에 캐싱 옵션을 추가하여 캐시 동작을 지정할 수 있으며, 서버 컴포넌트 내에서 "use" 훅을 통해 간단하게 데이터 패칭을 처리할 수 있게 변경되었다.

fetch 캐시 옵션 설정하기

import React from 'react';

const fetchData = async () => {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 } // 60초마다 데이터 재검증
  });
  const data = await res.json();
  return data;
};

const ServerComponent = async () => {
  const data = await fetchData();

  return (
    <div>
      <h1>Server Rendered Data with Automatic Revalidation</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default ServerComponent;

 

use 훅 사용하기

import React from 'react';
import { use } from 'react';

const fetchData = async () => {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 } // 60초마다 데이터 재검증
  });
  const data = await res.json();
  return data;
};

const ServerComponent = () => {
  const data = use(fetchData);

  return (
    <div>
      <h1>Server Rendered Data with use Hook</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default ServerComponent;

Reference

[모리딥] app router, react server component (tistory.com)

React 18: 리액트 서버 컴포넌트 준비하기 | 카카오페이 기술 블로그 (kakaopay.com)

새로 등장한 ‘리액트 서버 컴포넌트’ 이해하기 | 요즘IT (wishket.com)