[React] Server Component, 리액트 서버 컴포넌트 무엇이고 어떻게 활용할까

제론·2023년 5월 25일
4

#React

목록 보기
11/11

리액트 서버 컴포넌트

리액트 서버 컴포넌트를 기본적으로 지원하는 Next13이 출시되면서 서버 컴포넌트에 대해 관심이 높아지고 있습니다. 서버 컴포넌트는 무엇이고 어떤 문제를 해결할 수 있는지 그리고 그 기능은 무엇이 있는지 알아보겠습니다.

리액트 서버 컴포넌트란 뭐지?

const App = () => {
    return (
        <Wrapper>
            <ComponentA />
            <ComponentB />
        </Wrapper>
    )
}

위와 같이 컴포넌트가 구성되어져 있고 각각의 컴포넌트에서는 API 호출을 한다고 가정해봅시다.

const Wrapper = ({children}) => {
  
  const [wrapperData, setWrapperData] = useState({});
  
  useEffect(() => {
    // API call to get data for Wrapper component to function
    getWrapperData().then(res => {
      setWrapperData(res.data);
    });
  }, []);
  
  // Only after API response is received, we start rendering
  // ComponentA and ComponentB (children props)
  return (
  	<>
      <h1>{wrapperData.name}</h1>
      <>
        {wrapperData.name && children}
      </>
    </>
  )
}

/*-------------------------------------------------- */

const ComponentA = () => {
  const [componentAData, setComponentAData] = useState({});
  
  useEffect(() => {
    getComponentAData().then(res => {
      setComponentAData(res.data);
    });
  }, []);
  
  return (
  	<>
      <h1>{componentAData.name}</h1>
    </>
  )
}

/*-------------------------------------------------- */

const ComponentB = () => {
  const [componentBData, setComponentBData] = useState({});
  
  useEffect(() => {
    getComponentBData().then(res => {
      setComponentBData(res.data);
    });
  }, []);
  
  return (
  	<>
      <h1>{componentBData.name}</h1>
    </>
  )
}

이렇게 각각의 컴포넌트에서 API 콜을 하고 받은 데이터를 렌더링 합니다.

개별 컴포넌트는 오직 자신에게 필요한 데이터 받습니다.

자신에게 필요하지 않은 데이터는 처리할 필요없죠.

이 방식이 좋아보이나요?!

뜯어보면 문제가 보입니다.

각각의 컴포넌트에서 호출하는 API 콜이 응답받기까지 시간을 다음과 같이 가정해보겠습니다.

  • <Wrppaer /> -> 응답받기까지 1초 걸림
  • <ComponentB /> -> 응답받기까지 2초 걸림
  • <ComponentA /> -> 응답받기까지 3초 걸림

문제가 보이나요?

  1. Wrapper가 1초 뒤에 화면에 보이고

  2. ComponentB가 2초 뒤에 보입니다.

  3. ComponentA가 3초 뒤에 화면에 보입니다. 하지만 ComponentA는 ComponentB를 밑으로 누르듯이 화면에 나타나게 됩니다. 이것은 사용자 경험을 저해합니다.

그리고 Wrapper 컴포넌트가 렌더링 되기 전까지 자식 컴포넌트인 A, B는 렌더링 되지 않습니다. 이것은 Waterfall 현상을 발생시킵니다.

cf) Waterfall: 여러개의 API 호출시 하나의 API가 처리완료(resolved, response) 되었을 때 다음 API가 실행되는 현상을 말합니다.

순차적인 API 호출은 Waterfall을 발생하게 합니다.

어떤 문제를 해결할 수 있지?

이 문제를 어떻게 해결할 수 있을까요?

최상단 루트 컴포넌트에서 API 호출을 하여 Props로 넘겨주는 거도 하나의 방법일 수 있습니다.

const App = () => {
    
    const data = fetchAllStuffs();
    
    return (
        <Wrapper data={data.wrapperData}>
            <ComponentA data={data.componentAData} />
            <ComponentB data={data.componentBData} />
        </Wrapper>
    )
}

위와 같이 말이죠.

하지만 이렇게 했을 경우 API가 컴포넌트에 커플링되어 버립니다.

즉 ComponentA를 지웠을 경우 API에서는 data.componentAData가 여젼히 존재하게 되죠.
불필요한 리소스도 불러오게 됩니다.

해결책은 서버컴포넌트입니다.

위 문제는 서버컴포넌트에 대한 접근으로 해결 가능합니다.
문제를 다시 돌아보자면

컴포넌트는 서버로 API 호출을 하고 다른 컴포넌트를 렌더링 하기 위해서는 응답이 도착할 때까지 기라려야합니다.

서버 컴포넌트는 어떻게 해결할까요? 바로 가시죠

컴포넌트가 서버에 있다고 가정해봅시다.

그렇다면 서버 데이터를 받기까지 시간이 걸릴까요? 그렇지 않습니다.

클라이언트에서 서버 데이터를 요청하고 받는거보다 훠얼씬 빠릅니다.

서버 컴포넌트는 서버에서 존재하고 렌더링합니다.

이말은 컴포넌트가 서버에 있는 것이고 서버 인프라에 접근할 수 있다는 말이 됩니다.

결국 서버 컴포넌트 그 자체가 서버가 되고 바로 DB에 접근할 수 있습니다.

하지만!

여기서 알아야 할 것은

서버 컴포넌트는 리액트 훅스(useState, useEffect), Web APIs, 이벤트 핸들러를 쓸 수 없습니다.

해당 기능들은 클라이언트에서 렌더링(브라우저)해야 쓸 수 있습니다. 따라서 서버에서 렌더링된다면 사용이 불가능하죠

그래서 어떻게 쓰면 좋지?

=> 결국 서버 컴포넌트는 실시간 상호작용이나 사용자 인터렉션이 필수적이지 않은 상황에서 쓰는게 적합합니다!

블로그 서비스라면

  • 블로그 글을 보여주는 부분 -> 서버 컴포넌트
// Note.js - Server Component

import db from 'db'; 
// (A1) We import from NoteEditor.js - a Client Component.
import NoteEditor from 'NoteEditor';

async function Note(props) {
  const {id, isEditing} = props;
  // (B) Can directly access server data sources during render, e.g. databases
  const note = await db.posts.get(id);
  
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
      {/* (A2) Dynamically render the editor only if necessary */}
      {isEditing 
        ? <NoteEditor note={note} />
        : null
      }
    </div>
  );
}
  • 블로그 글 작성이나 업데이트하는 부분 -> 클라이언트 컴포넌트
// NoteEditor.js - Client Component

'use client';

import { useState } from 'react';

export default function NoteEditor(props) {
  const note = props.note;
  const [title, setTitle] = useState(note.title);
  const [body, setBody] = useState(note.body);
  const updateTitle = event => {
    setTitle(event.target.value);
  };
  const updateBody = event => {
    setBody(event.target.value);
  };
  const submit = () => {
    // ...save note...
  };
  return (
    <form action="..." method="..." onSubmit={submit}>
      <input name="title" onChange={updateTitle} value={title} />
      <textarea name="body" onChange={updateBody}>{body}</textarea>
    </form>
  );
}

이렇게 활용할 수 있습니다!

서버 컴포넌트와 클라이언트 컴포넌트 사용시 주의사항

두 개를 동시에 사용해서 부모/자식 관계로 컴포넌트를 구성할 수 있을까요?!

안타깝게도 클라이언트 컴포넌트에서는 서버 컴포넌트를 불러올 수 없습니다.

  • 서버 컴포넌트 안에서 클라이언트 컴포넌트 import -> O

  • 클라이언트 컴포넌트 안에서 서버 컴포넌트 import -> X

  • 서버 컴포넌트 안에 있는 클라이언트 컴포넌트에서 자식 컴포넌트로 클라이언트 컴포넌트를 넣기 -> O

const ServerComponentA = () => {
    return (
        <ClientComponent>
            <ServerComponentB />
        </ClientComponent>
    )
}

서버 컴포넌트의 또 다른 장점

컴포넌트 번들 사이즈를 줄일 수 있다.

정적인 페이지에서 클라이언트에 필요한 패키지들을 다운받는다면 이것들을 빌드시 다 번들해야하고
클라이언트 파일이 커지게 됩니다.

서버 컴포넌트를 이용한다면 이러한 번들 사이즈를 줄일 수 있죠.

백엔드에 접근 가능하다.

API 호출 없이 직접 DB에 접근해 데이터를 가져올 수 있습니다.

심지어 fs 모듈을 통해 서버에 있는 파일 접근도 가능합니다.

이상으로 서버 컴포넌트에 대해 알아봤습니다.

Next.js 13에선 기본적으로 서버 컴포넌트를 사용하기 때문에
간편하게 쓸 수 있습니다.

reference

profile
Software Developer

1개의 댓글

comment-user-thumbnail
2023년 11월 29일

"서버 컴포넌트 안에 있는 클라이언트 컴포넌트에서 자식 컴포넌트로 클라이언트 컴포넌트를 넣기 -> O" 부분에서 아래 코드는 ClientComponent의 자식으로 ServerComponentB가 들어가있는데, 그러면 "서버 컴포넌트 안에 있는 클라이언트 컴포넌트에서 자식 컴포넌트로 서버 컴포넌트를 넣기"가 맞지 않을까요?

답글 달기