호진방 블로그
학습

서버 컴포넌트 이해하기

# 서버 컴포넌트란 무엇일까요? 그리고 SSR과의 차이점은 무엇일까요?

2024년 03월 04일

개요

최근 Next.js를 사용하여 이런저런 프로젝트를 진행하며 CSR과 SSR에 대한 이해도는 높아졌지만, 서버 컴포넌트와 클라이언트 컴포넌트에 대한 이해는 아직 부족한 상황이라고 생각했다. 특히 Next.js 블로그 프로젝트에서는 App Router를 사용하여 개발중인데, 이 과정에서 새로운 컴포넌트 개념을 더 깊이 이해할 필요성을 느꼈다 이번 글을 통해 서버 컴포넌트의 부족한 개념을 채우고, 이해해보는 시간을 갖고자 한다.


서버 컴포넌트란

서버 컴포넌트는 Next.js의 최신 기능 중 하나로, React18부터 도입된 개념이며, 서버 측에서 렌더링되는 React 컴포넌트를 의미한다. 이러한 컴포넌트는 서버에서 실행되기 때문에 클라이언트에서 불필요한 자바스크립트 코드가 전송되지 않으며, 이에 따라 초기 로딩 속도가 개선되고 SEO 측면에서 유리한 점이 많다.

서버 컴포넌트는 전통적인 React 컴포넌트와 다르게 클라이언트 브라우저에서 실행되지 않으며, 서버에서만 실행되는 특성을 가지고 있다. 그렇다면 전통적인 React 컴포넌트는 클라이언트에서만 렌더링되는것인가? 맞다 우리가 리액트에서 사용했던 모든 컴포넌트가 바로 클라이언트 컴포넌트라고 할 수 있고, 서버 컴포넌트와의 차이는 단순히 컴포넌트가 렌더링되는 장소가 서버냐 클라이언트냐의 차이라고 할 수 있다.


전통적인 React 컴포넌트의 문제점

서버 컴포넌트에 대해 알아보기 전에, 먼저 아래의 예시를 통해 전통적인 React 컴포넌트의 문제점을 살펴보자

function UserProfile({ userId }) {
  return (
    <UserDetails userId={userId}>
      <UserBalance userId={userId} />
      <UserHistory userId={userId} />
    </UserDetails>
  );
}

<UserProfile> 컴포넌트는 부모 컴포넌트인 <UserDetails> 아래에 두 개의 자식 컴포넌트 <UserBalance><UserHistory>를 포함하고 있다. 이들 컴포넌트는 모두 서버 API를 호출하여 데이터를 받아와야 한다. 그렇다면 개발자는 데이터를 받아오는 방법으로 두 가지를 선택할 수 있을것이다.

방법 1. 부모 컴포넌트에서 API를 호출하여 데이터를 받아오고 이를 자식 컴포넌트에 내려주기

방법 2. 각 컴포넌트에서 필요한 API를 개별적으로 호출하여 데이터를 받아오기


방법 1 : 부모 컴포넌트에서 데이터 호출

방법 2: 각 컴포넌트에서 개별적으로 데이터 호출

❗ 이와 같이 클라이언트 컴포넌트에서의 비동기 data fetching의 두 방법 모두 애플리케이션의 성능이 저하하며, 사용자 경험을 떨어뜨릴것이다


서버컴포넌트의 등장

이러한 문제점을 해결하기 위해 서버 컴포넌트가 등장했다. 서버 컴포넌트를 사용하면 컴포넌트를 서버에서 렌더링할 수 있는 것뿐만 아니라 데이터 또한 서버에서 가져올 수 있게 된다. 이 덕분에 클라이언트보다는 서버에서 데이터 요청을 처리하게 되어 API 요청에 걸리는 시간을 줄일 수 있고, 클라이언트에서 연속적으로 API를 호출할 필요가 없어져, 클라이언트와 서버 간의 로딩 시간을 줄일 수 있게 된다.

위 그림처럼 컴포넌트의 렌더링과 데이터 패칭 책임을 서버에 넘기고, 클라이언트는 서버에서 렌더링된 결과를 단순히 표시하기만 하면 된다.

서버 컴포넌트의 동작

Next13부터는 App 폴더가 Pages 폴더를 대체하여, App directory를 사용한다. App directory내부에서는 모든 컴포넌트가 기본적으로 서버컴포넌트로 동작하며 아래는 Next.js App directory에서의 서버 컴포넌트 동작 방식을 설명한다.

❗ 참고로 해당 예제에서는 최상위 컴포넌트, 즉 페이지 단위 컴포넌트에서는 최상위 use client 지시어를 사용하지 않는것을 가정한다. 클라이언트 컴포넌트 안의 자식 컴포넌트는 서버 컴포넌트가 될 수 없다.

+정정

'use client';
 
// ❌
import ServerComponent from './ServerComponent';
 
export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

// ✅
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';
 
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}
'use client';
 
export default function ClientComponent({children}) {
  return (
    <>
      {children}
    </>);
 

1. 요청 수신

2. 서버 컴포넌트 렌더링

❗ 이 때 컴포넌트 트리를 root부터 실행하며 직렬화된 json형태로 재구성하게 되는데, 직렬화 과정은 모든 서버 컴포넌트 실행되며, 클라이언트 컴포넌트일 경우는 “module reference” 라고 하는 새로운 타입을 적용하고, 해당 컴포넌트의 경로를 명시함으로써 트리를 구성한다.


3. HTML 전송

4. 추가 JavaScript 로딩


그렇다면 서버 컴포넌트 === SSR인것인가?

SSR이란

여기까지 설명을 들으면 서버 컴포넌트는 SSR 동작과 매우 유사해보인다. 사실 여기까지 글을 작성하면서 이거 그냥 SSR 동작아니야? 라고 생각하여 SSR과의 차이점에 대해 알아보았다.

종합해보면 SSR은 전체 페이지의 렌더링 방식을 의미하고, 서버 컴포넌트는 이러한 SSR 방식을 구현하기 위한 구체적인 구현 방법 중 하나로 이해하면 된다.


서버 컴포넌트 사용시 주의점

상태와 이벤트 핸들링의 제한

서버 컴포넌트는 말 그대로 서버에서 동작되기 때문의 클라이언트(브라우저의) 상태(state)나 이벤트 핸들링을 직접적으로 처리할 수 없기 때문에 서버 컴포넌트 내에서는 useStateuseEffect와 같은 React 훅을 사용할 수 없다. 이러한 기능은 클라이언트 컴포넌트에서 처리하도록 수정해야하고, 이벤트 핸들링 또한 브라우저에서 발생하는 동작이기 때문에, 클라이언트 컴포넌트에서만 수행될 수 있다.

클라이언트 전용 코드 포함 금지

서버 컴포넌트에는 브라우저 전용 API나 클라이언트 전용 라이브러리를 포함해서는 안된다. 예를 들어, window, document, localStorage와 같은 브라우저 API를 서버 컴포넌트에서 사용하면 오류가 발생한다. 서버 컴포넌트는 주로 UI를 렌더링하는 역할을 하며, UI 관련 로직은 클라이언트 컴포넌트에서 처리해야 한다.


me
@banhogu
안녕하세요 배움을 나누며 함께 전진하는 1년차 주니어 개발자 방호진입니다.