코린이의 개발 일지

React Sever Component 살펴보기 본문

웹 (web)/프론트엔드

React Sever Component 살펴보기

폴라민 2023. 4. 29. 23:02
반응형

React Sever Component란?

서버 사이드 렌더링

  • 서버에서 사용자에게 보여줄 페이지를 모두 미리 구성한 뒤 페이지를 렌더링 하는 방식

 

RSC

  • 위의 그림처럼 서버 컴포넌트는 렌더링 과정에서 클라이언트 사이드 렌더링과 서버 사이드 렌더링을 동시에 할 수 있도록 해준다.

즉 컴포넌트 별로 서버에서 렌더링할지, client에서 렌더링할지가 결정된다.

 

RSC 동작 방식

  1. 사용자가 접속하면 브라우저가 프론트엔드 서버에 요청을 보낸다.
  2. 프론트엔드 서버는 렌더링을 시작한다.
    1. root 컴포넌트 부터 시작해서 서버 컴포넌트와 클라이언트 컴포넌트를 분리하여 렌더링을 한다.
    2. 클리이언트 컴포넌트는 일종의 ‘placeholders’로 채워 놓고, 서버 컴포넌트는 html 태그로 이루어진 트리구조로 렌더링한다.
    3. 서버는 완성된 트리를 JSON으로 직렬화한다.
  3. 브라우저는 JSON형태로 직렬화된 데이터를 받아서 이를 바탕으로 React 트리를 재구성한다.
    1. ‘placeholders’로 대체했던 클라이언트 컴포넌트는 렌더링해서 실제 클라이언트 컴포넌트 함수로 바꾼다.
  4. 브라우저에서 재구성된 React 트리를 렌더링한다.

JSON으로 직렬화된 형태

 

 

SSR이랑 RSC가 어떻게 다를까?

둘은 반대되는 개념도 아니고 같은 개념도 아니다.

 

SSR은 초기에 렌더링되는 모든 트리가 서버에서 구성되어 클라이언트로 보내진다. 이때 데이터 형태는 html 형태로 보내지고 클라이언트는 그 html을 받아서 hydration한다.

이후에 리렌더링 되는 부분들은 모두 클라이언트 사이드 렌더링이다.

 

RSC는 첫 접속시 렌더링 되는 과정은 SSR과 비슷하다. 서버에서 전체적인 트리구조가 구성되어 넘어오고 (클라이언트 컴포넌트는 placeholder로 대체) 브라우저는 이를 받아 트리구조 재구성(일종의 hydration)을 한 뒤, 렌더링한다.

차이점은 첫 접속 이후이다. RSC를 사용한 경우, 서버 컴포넌트는 리렌더링도 서버에서 동작한다. 브라우저는 서버 컴포넌트에 일체 관여하지 않는다.

 

따라서 Server Side Rendering과 React Server Component는 서로 보완적인 관계라고 볼 수 있다.

 

RSC 사용해보자

코드를 살펴보기 앞서 서버 컴포넌트의 특징에 대해 좀 알아야한다.

서버 컴포넌트는 서버에서 실행되고, 클라이언트 컴포넌트는 클라이언트에서 실행되기 때문에 각각이 할 수 있는 일에 많은 제한이 있다.

서버 컴포넌트에서는 리액트 개발할 때 흔히 쓰는 모든 훅(hook)을 사용할 수 없다.

  • useState
  • useEffect

등등 모두 사용 불가능하며 button과 같은 이벤트 발생 element도 사용할 수 없다.

 

  • 훅은 왜 사용 불가능할까? 

 

 

  • 이벤트 발생 element는 왜 사용 불가능할까?
    • 이벤트 핸들러는 브라우저에서 동작해야하기 때문에 직렬화한 JSON 형태에 포함시켜서 브라우저로 보내야한다. 그러나 함수는 JSON형태로 직렬화가 불가능하다. 따라서 사용 불가능

 

 

또한 클라이언트 컴포넌트에서 서버 컴포넌트를 가져올 수 없다.

다음과 같은 사용은 불가능하다.

// ClientComponent.client.jsx
// NOT OK:
import ServerComponent from './ServerComponent.server'
export default function ClientComponent() {
  return (
    <div>
      <ServerComponent />
    </div>
  )
}

 

 

따라서 서버 컴포넌트와 클라이언트 컴포넌트를 합성(Composition)하기 위해서는 서버 컴포넌트에서 각각을 호출해야 한다.

// ClientComponent.client.jsx
export default function ClientComponent({ children }) {
  return (
    <div>
      <h1>Hello from client land</h1>
      {children}
    </div>
  )
}

// ServerComponent.server.jsx
export default function ServerComponent() {
  return <span>Hello from server land</span>
}

// OuterServerComponent.server.jsx
import ClientComponent from './ClientComponent.client'
import ServerComponent from './ServerComponent.server'
export default function OuterServerComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )

 

 

그럼 이제 간단한 앱을 만들어보자.

아래 소개할 코드는 Next.js 13버전 app directory를 사용해서 구현한 todo list 이다.

 

투두리스트는 입력란, 투두 목록으로 나뉘어져있다. 새로운 투두를 입력하면 백엔드로 api 요청을 보내 저장하고, 새로운 투두 리스트를 업데이트하는 로직을 구현해보자

// ./app/page.tsx

import InputTodo from "@/components/InputTodo";
import TodoList from "@/components/TodoList";

export default function Home() {
  return (
    <div>
      <header>TodoList</header>
      <main>
        <InputTodo />
        {/* @ts-expect-error Server Component */}
        <TodoList />
      </main>
    </div>
  );
}

 

최초의 Root 컴포넌트 (app 디렉토리 내에 있는 페이지)들은 전부 서버 컴포넌트이다.

서버 컴포넌트에서 사용할 클라이언트 컴포넌트와 서버 컴포넌트를 가져온다.

 

InputTodo는 사용자의 입력을 받아서 백엔드에 데이터를 전송하는 클라이언트 컴포넌트이다.

TodoList는 투두 목록을 api 요청으로 받아와서 렌더링하는 서버 컴포넌트이다.

 

아래는 투두 목록을 렌더링하는 서버 컴포넌트이다.

// ./components/TodoList.tsx

import React from "react";
import { getTodos } from "@/http";
import { Todo } from "@/type";

export default async function TodoList() {
	const res = await fetch("<http://localhost:8080/api/todos>", {
    cache: "no-cache",
  });
  const data: Todo[] = await res.json();
  return (
    <div className="todo-list">
      {data.map((todo: Todo) => (
        <div className="todo" key={todo.id}>
          {todo.title}
        </div>
      ))}
    </div>
  );
}

 

서버컴포넌트에서 fetch 요청을 보낼때 기본적으로 cache가 되어 새로운 데이터를 refetch해오지 않기 때문에 따로 설정을 해줘야한다.

이때 설정에

fetch('https://...', { next: { revalidate: 10 } });

이와 같이 revalidation 설정을 해주면 react query처럼 캐시의 생명 주기를 설정해서, 일정한 주기로 refetching 해올 수 있다.

 

 

아래는 새로운 투두를 추가하는 클라이언트 컴포넌트 코드이다.

// ./components/InputTodo.tsx

"use client";

import React, { useState, KeyboardEvent } from "react";
import { postNewTodo } from "@/http";
import { useRouter } from "next/navigation";

export default function InputTodo() {
  const [value, setValue] = useState("");
  const router = useRouter();

  const postNewTodoMutate = async () => {
    await fetch(`http://localhost:8080/api/todos`, {
	    method: "POST",
	    headers: {
	      "Content-Type": "application/json",
	    },
	    body: JSON.stringify({ title: value, content: "" }),
	  });
    setValue("");
		// 해당 페이지에 속해있는 모든 서버 컴포넌트들을 데이터 refetch 후 
    // 리렌더링하도록 하는 메서드
    router.refresh();
  };

  const onKeyDown = (e: KeyboardEvent) => {
    if (e.nativeEvent.isComposing) return;
    const { key } = e;
    if (key === "Enter") {
      postNewTodoMutate();
    }
  };
  return (
    <input
      onChange={(e) => setValue(e.target.value)}
      placeholder="입력하세요"
      {...{ value, onKeyDown }}
    />
  );
}

 

 

router.refresh()에 대한 상세 내용은 아래 링크로 확인할 수 있다.

Functions: useRouter

 

Functions: useRouter | Next.js

Using App Router Features available in /app

nextjs.org

 

 

위의 로직은 actions 함수를 서버에서 돌리는 아래와 같은 방식으로 구현할 수도 있다.

// ./components/InputTodo.tsx

"use client";

import React, { useState, KeyboardEvent } from "react";
import { postNewTodoMutate } from "@/actions/addItem";

export default function InputTodo() {
  const [value, setValue] = useState("");

  const onKeyDown = (e: KeyboardEvent) => {
    if (e.nativeEvent.isComposing) return;
    const { key } = e;
    if (key === "Enter") {
      postNewTodoMutate(value);
      setValue("");
    }
  };
  return (
    <input
      onChange={(e) => setValue(e.target.value)}
      placeholder="입력하세요"
      {...{ value, onKeyDown }}
    />
  );
}
// ./actions/postNewTodoMutate.ts

"use server";

import { postNewTodo } from "@/http";
import { revalidatePath } from "next/cache";

export const postNewTodoMutate = async (value: string) => {
  // 이 함수는 서버에서 동작한다.
	// api 서버를 따로 두지 않을 경우, DB에 직접 저장 가져오기 등의 로직이 있어도 상관 없다.
	// 서버에서 동작하기 때문에 유저에게 코드가 노출되지 않음
  await fetch(`http://localhost:8080/api/todos`, {
	    method: "POST",
	    headers: {
	      "Content-Type": "application/json",
	    },
	    body: JSON.stringify({ title: value, content: "" }),
	  });
  revalidatePath("/todo");
};

 

 

이와같이 서버에서 동작하는 action을 server action이라하고 더 다양한 Server Actions은 아래에서 볼 수 있다.

Data Fetching: Server Actions

 

Data Fetching: Server Actions | Next.js

Using App Router Features available in /app

nextjs.org

 

 

서버 컴포넌트가 SSR을 대체할까?

  • 아니다. 다른 이유를 요약하면 다음 세가지 이유가 있다.
  1. 서버 컴포넌트 코드는 절대 클라이언트에게 전달되지 않는다. 많은 React를 사용한 SSR의 구현은 자바스크립트 번들을 통해 클라이언트로 컴포넌트 코드가 보내지게 된다. 이로 인해 상호작용이 지연될 수 있다.
  2. 서버 컴포넌트를 사용하면 트리의 어느 곳에서나 백엔드에 접근할 수 있다. Next.js를 사용한다면, 최상위 페이지에서만 가능한 getServerProps()를 통해 백엔드에 접근하는 것에 익숙할 것이다. 하지만, 임의 npm 컴포넌트는 이런 동작이 불가능하다.
  3. 트리 내부에서 클라이언트 측의 상태(state)를 유지하면서 서버 컴포넌트를 다시 가져올 수 있다. 이는 주요 전송 메커니즘이 HTML보다 훨씬 풍부하기 때문이다. 따라서, 내부 상태(e.g 검색 입력 텍스트, 포커스, 텍스트 선택)를 없애지 않고 서버에서 렌더링 한 부분(e.g 검색 결과 목록)을 다시 가져올 수 있게 한다.

 

 

RSC 이점

가장 큰 이점은 자바스크립트 번들 사이즈를 훨씬 줄일 수 있다는 점이다.

아래와 같이 서버에서 사용하는 서버 컴포넌트는 클라이언트 측으로 전달되지 않는다.

서버 컴포넌트에서만 사용하는 라이브러리도 번들에서 제외시킬 수 있다.

// NoteWithMarkdown.server.js - Server Component === zero bundle size

import marked from 'marked'; // zero bundle size
import sanitizeHtml from 'sanitize-html'; // zero bundle size

function NoteWithMarkdown({text}) {
  // 이전과 같다.
}

 

 

그외 다른 이점을 꼽자면 서버 컴포넌트의 역할과 클라이언트 컴포넌트의 관심사를 명확히 구분해서 컴포넌트 재사용성을 높일 수 있다.

→ 리액트 쿼리가 추구하는 바와 비슷하다. 서버 상태와 클라이언트 상태를 구분해서 관리하는 것

 

 

 

 

 

구현한 todo app 전체 코드는 아래 레포지토리에서 확인해 볼 수 있다.

https://github.com/leesunmin1231/practice_app_dir

 

GitHub - leesunmin1231/practice_app_dir: Next.js 13 app directory 학습

Next.js 13 app directory 학습. Contribute to leesunmin1231/practice_app_dir development by creating an account on GitHub.

github.com

 

 

 

Refs.

https://www.plasmic.app/blog/how-react-server-components-work

 

How React server components work: an in-depth guide

A deep dive exploration of React server components under the hood.

www.plasmic.app

https://beta.nextjs.org/docs/getting-started

 

Getting Started | Next.js

Get started with Next.js in the official documentation, and learn more about Next.js features!

beta.nextjs.org

https://ui.toast.com/weekly-pick/ko_20210119

 

React 서버 컴포넌트

이번 주, React 팀은 서버 주도(Server-Driven) 멘탈 모델로 모던 UX를 가능하게 하는 것을 목표로 하는 zero-bundle-size React Server Components를 시연했다. 서버 컴포넌트는 서버 사이드 렌더링(SSR)과는 상당히

ui.toast.com

https://betterprogramming.pub/will-react-server-components-replace-ssr-f2f772347109

 

Will React Server Components Replace SSR?

The birth of React Server Components and their effect on SSR

betterprogramming.pub

https://codingapple.com/unit/nextjs-server-actions/?id=68759 

 

Next.js의 Server actions 기능 - 코딩애플 온라인 강좌

(아직 정식기능이 아니라 테스트중 기능이므로 나중에 바뀔 수 있어서 글로 맛만 봅시다) DB에 데이터를 저장, 수정 등을 하고 싶으면 당연히 서버를 거쳐야합니다.  그래서 page.js에 <form>같은 것

codingapple.com

 

반응형
Comments