Original article: React Server Components – How and Why You Should Use Them in Your Code

2020년 말, React 팀은 "제로-번들-사이즈 React 서버 컴포넌트" 개념을 도입했습니다. 그 이후로 React 개발자 커뮤니티는 이 미래 지향적인 접근 방식을 실험하고 적용하는 방법을 학습해 왔습니다.

React는 UI를 구축하는 방식에 대한 우리의 생각을 바꾸었습니다. 그리고 React 서버 컴포넌트를 사용하는 새로운 모델은 훨씬 더 구조화되고 편리하며, 유지 관리하기 쉽고 더 나은 사용자 경험을 제공합니다.

Next.js의 최신 릴리즈는 “서버 컴포넌트로 생각하기” 방향으로 나아갔습니다. 그리고 React 개발자로서 우리는 애플리케이션을 구축하는데 이 기술을 최대한 활용하기 위하여 새로운 사고 모델에 적응해야 합니다.

이 튜토리얼에서는 RSC(React Server Component)에 대해 배울 것입니다. React 서버 컴포넌트가 무엇인지, 어떻게 작동하는지, 그리고 더 중요하게는 이것이 어떤 문제를 해결하는지 중점적으로 알아볼 것입니다.

또한 왜 RSC가 필요한지 이해할 수 있도록 많은 예시를 보여줄 것입니다. 마지막으로 React 서버 컴포넌트와 비슷해 보이지만 다른 기능인 서버 사이드 렌더링(SSR) 간의 차이점을 배울 것입니다.

React를 처음 접하는 경우, React 서버 컴포넌트에 대해 학습하기 전에, 컴포넌트 아키텍처, 상태(state), 속성(props)을 이용한 데이터 전달, 가상 돔(Virtual DOM) 트리에 대한 기본 지식이 필요합니다.

또한 이 글을 읽은 다음 freeCodeCamp에서 전체 로드맵을 참고하여 React 기초를 확고히 다질 수도 있습니다.

준비가 되었나요? 시작해봅시다.

만약 비디오 콘텐츠를 통해 학습하고 싶다면, 이 글을 비디오 튜토리얼로도 볼 수 있습니다. 🙂

클라이언트 사이드 UI 라이브러리로서의 React

React는 처음부터 클라이언트 사이드 UI 라이브러리였습니다. 웹 및 모바일 개발자가 컴포넌트 기반 아키텍처를 활용하여 애플리케이션을 개발하는 데 도움이 되는 JavaScript 기반의 오픈 소스 라이브러리입니다.
React 철학은 우리가 디자인 전체를 더 작고 독립적인 부분인 컴포넌트라고 불리는 작은 조각으로 나누는 것을 제안합니다.

image-169

하나의 컴포넌트가 여러 개의 하위 컴포넌트로 분해되는 것을 보여주는 이미지입니다.

컴포넌트는 state라고 불리는 자체 비공개 데이터와 서로 다른 컴포넌트 간에 데이터를 전달하는 방법인  props를 가질 수 있습니다. 컴포넌트들을 컴포넌트 계층 구조로 나누고, 상태를 정의하고, 상태를 변경하여 발생되는 효과(effect)를 관리하고, 데이터 흐름(flow)을 결정합니다.

image-170

state와 props의 작동 방식을 보여주는 이미지

전통적으로 이러한 모든 컴포넌트들은 자바스크립트의 함수입니다(여기서는 함수형 컴포넌트에 대해서만 이야기하고, 과거에 사용했던 클래스형 컴포넌트는 따로 이야기 하지 않겠습니다). 앱이 브라우저에서 로드될 때, 컴포넌트 코드를 다운로드하고 이를 사용하여 앱이 작동하도록 만듭니다.

여전히 ‘컴포넌트’라는 용어를 사용할 것입니다. 하지만 이 문서는 React 서버 컴포넌트의 개념을 소개하므로, 이러한 전통적인 컴포넌트를 클라이언트 컴포넌트 라고 부르겠습니다(클라이언트/브라우저에서 다운로드되고 React가 마법을 부려 렌더링하는 컴포넌트).

React 앱의 일반적인 문제

React 클라이언트 컴포넌트는 훌륭하며 특정 사례를 해결하는 데 효과적입니다. 하지만 React 앱을 구축할 때, 패턴을 조금 다르게 고려해 볼 필요가 있습니다. 이는 다음과 같은 사항을 고려해야 하기 때문입니다.

  • 사용자 경험(User Experience): 우리는 사용자와 고객을 위한 소프트웨어 제품을 만듭니다. 앱이 성공하길 원한다면, 애플리케이션의 사용자 경험이 중요합니다.
  • 유지 보수(Maintainability): 프로젝트 코드는 여러 개발팀을 거쳐 수년간 원활하게 유지 보수 되어야 합니다.
  • 성능 비용(Performance Cost): 애플리케이션이 느려져서는 안 되며, 우리의 디자인(설계) 접근 방식이 느리게 만들어서도 안됩니다.

이제 일상적으로 마주할 수 있는 몇 가지 예를 살펴보겠습니다. 또한 React를 사용하여 일상적인 웹 개발에서 각 핵심 사항을 어떻게 구현하고 설계할 수 있는지 이해할 수 있을 것입니다.

레이아웃 이동 문제

매우 흔한 UX 문제 중 하나는 컴포넌트가 렌더링될 때 레이아웃이 갑자기 이동되는 현상입니다. 아래의 코드 스니펫을 살펴봅시다.

<CourseWraper>
  <CourseList />
  <Testimonials />
</CourseWraper>

다음은 익숙한 JSX 코드로, CourseWrapper 컴포넌트와 CourseListTestimonials 두 개의 하위 컴포넌트가 있습니다. CourseListTestimonials 두 컴포넌트 모두 데이터를 가져오기 위해 네트워크 호출(API 호출)을 수행한다고 가정해 봅시다.

다음은 CourseList 컴포넌트 입니다.

function CourseList() {

    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다.
    const info = fetchCourseList();

    return(
      <> </>)
}

그리고 Testimonial 컴포넌트 입니다.

function Testimonials() {

    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다.
    const info = fetchTestimonials();

    return(
      <> </>
    )
}

이러한 컴포넌트들이 네트워크 호출을 수행하는 경우, 응답이 돌아오는 순서에 대한 보장이 없습니다. 이는 네트워크 속도, 지연 시간 및 여러 다른 요인에 따라 달라집니다.
Testimonials 컴포넌트의 네트워크 호출이 CourseList 컴포넌트보다 먼저 완료되는 상황에서, Testimonials 컴포넌트가 먼저 렌더링되고 그 다음에 CourseList 컴포넌트가 렌더링됩니다. 이로 인해 Testimonials 컴포넌트가 적합한 위치로 이동됩니다. 아래에서 제가 하는 말이 무엇인지 확인할 수 있습니다.

layoutshift-1

레이아웃 이동의 UX 문제를 보여주는 슬로우 모션

로딩 인디케이터나 깜빡임 효과를 통해서 사용자에게 잠시 후를 기대하도록 알려주어 UX를 좀 더 향상시킬 수 있습니다(그러나 언제가 될지는 확신하지 못합니다).

네트워크 워터폴(waterfall) 문제

또 다른 전형적인 사용자 경험 문제에 대해 이야기해 봅시다. 지난 예제와 유사한 React 컴포넌트를 상상해 보세요.

function Course() {
  return (
    <CourseWraper>
      <CourseList />
      <Testimonials />   
    </CourseWraper>
  )
}

두 개의 하위 컴포넌트를 가진 CourseWrapper 컴포넌트

여기서 조금 수정을 해보겠습니다. CourseListTestimonials 컴포넌트와 함께 이제 CourseWrapper도 네트워크 호출을 수행합니다.

function CourseWrapper() {

    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다.
    const info = fetchWrapperInfo();
    
    return(
      <> </>
    )
}

CourseWrapper 컴포넌트 - 네트워크 호출 수행

따라서 부모 컴포넌트는 데이터를 가져오기 위해 네트워크 호출을 수행하고, 그 하위 컴포넌트들 또한 네트워크 호출을 수행합니다.

흥미로운 점은, 부모 컴포넌트는 네트워크 호출이 완료될 때까지 렌더링되지 않는다는 것입니다. 따라서 자식 컴포넌트들의 렌더링도 홀딩 합니다.

현재 작업을 시작하기 위해 이전 것의 응답이 완료되기를 기다리는 현상은 워터폴(Waterfall)라고 알려져 있습니다. 이 경우에는 네트워크 워터폴 및 컴포넌트 렌더링 워터폴 문제 모두 발생합니다.

이제 여러분은 각 컴포넌트에서의 모든 네트워크 호출을 제거하고 각각의 컴포넌트가 응답을 기다리지 않도록 부모 컴포넌트에서 단일 호출 하도록 네트워크 호출 로직을 끌어 올릴 수 있다고 생각할 수 있습니다. 이것은 현명한 생각이지만 유지 보수 측면에서 문제가 발생할 수 있습니다. 다음 섹션에서 이에 대해 더 자세히 알아보겠습니다.

유지 보수 관련 문제

서버 측 상호 작용과 관련된 몇 가지 사용자 경험 문제를 살펴보았으므로, 이제 유지 보수 관련 문제를 고려해 봅시다.

모든 컴포넌트가 네트워크 호출을 수행하지 않는다고 가정해 봅시다. 단일 API 호출인 fetchAllDetails()를 사용하여 (부모 컴포넌트를 포함하여) 모든 컴포넌트의 세부 정보를 한 번에 가져옵니다.

그런 다음 필요한 정보를 각 컴포넌트에 props로 전달합니다. 이것은 위에서 본 "워터폴" 문제보다 더 나은 접근 방법이겠지요?

function Course() {
	
    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다. 
    const info = fetchAllDetails();
    
    return(
    	<CourseWrapper
        	ino={info.wrapperInfo} >
            <CourseList
        		ino={info.listInfo} />
            <Testimonials
        		ino={info.testimonials} />
        </CourseWrapper>     
    )
 }

Course 컴포넌트 - 최상위 API 호출 수행

그러나 이것은 일부 유지 보수 문제를 야기할 수 있습니다.

어느 화창한 날, 제품이 Testimonials 기능을 삭제하기로 결정했다고 가정해 봅시다. 우리는 위의 코드에서 Testimonials 컴포넌트를 간단히 삭제할 수 있습니다. 그런데, fetchAllDetails() 호출을 통해 가져온 데이터를 정리하는 것을 잊을 수 있습니다. 사용되지 않고 불필요한 상태로 남아 있을 수 있습니다.

이러한 문제를 완화하기 위해, 가능한 사용자 경험 문제를 설명하면서 이전 섹션에서 이미 논의한 방식으로 코드를 변경할 수 있을 것입니다. 따라서 우리는 더 나은 해결책을 찾아야 합니다. 하지만 그에 앞서, 성능 비용이라는 또 하나의 고려 사항에 대해 먼저 이야기해 보겠습니다.

성능 비용

우리가 논의할 마지막 문제 영역은 성능 비용입니다.

image-171

인터넷에서 발견한 재미있는 밈 - 자바스크립트가 클라이언트에게 주는 무거움을 묘사

전통적으로, React 컴포넌트는 클라이언트 사이드 자바스크립트 함수입니다. 이는 React 애플리케이션의 구성 요소입니다. 클라이언트에서 애플리케이션을 로드할 때, 컴포넌트가 클라이언트에 다운로드되고 React가 그것들을 렌더링하는데 필요한 작업을 수행합니다.

그러나 이로 인해 두 가지 중요한 문제가 발생합니다.

먼저, 사용자가 요청을 보낼 때, 앱은 HTML과 연결된 JavaScript, CSS 및 이미지와 같은 다른 에셋을 다운로드합니다.

클라이언트 사이드(브라우저에서)에서 React는 마법을 부리기 시작하고 HTML 구조를 하이드레이트(hydrate) 시킵니다. 이것은 HTML을 구문 분석하고, 이벤트 리스너를 DOM에 연결하며 스토어에서 데이터를 가져옵니다. 따라서 사이트는 완전히 작동하는 React 앱이 됩니다.

그러나 중요한 점은 클라이언트에서 많은 일이 일어나고 있다는 것입니다. 결국 이 코드를 모두 클라이언트에서 다운로드하게 됩니다.

image-182

브라우저에 다운로드된 스크립트의 양

대부분의 경우, 프로젝트에 종속성이 있는 외부 라이브러리(Node 모듈)가 필요합니다. 이러한 모든 종속성은 클라이언트 사이드에서 다운로드되어 더욱 무거워집니다.

이제 문제점을 이해했으므로, React 서버 컴포넌트가 제공하는 기능과 이러한 문제를 해결하는 방법에 대해서 확실히 이해할 수 있을 것입니다.

그러나 이에 대해 이야기하기 전에, 클라이언트와 서버에 대해 조금 더 알아보겠습니다.

클라이언트-서버 모델

이 글에서 클라이언트와 서버 라는 용어를 여러 번 사용했습니다. 이제 이들에 대한 공식적인 정의를 내리고 높은 수준에서 그들의 관계를 설명해보겠습니다.

image-175

클라이언트와 서버의 관계를 보여주는 다이어그램

  • 클라이언트: 애플리케이션의 클라이언트는 최종 사용자 측에서 작업을 실행하는 시스템입니다. 클라이언트의 예로는 PC, 랩탑, 모바일, 브라우저 등이 있습니다.
  • 서버: 이름에서 알 수 있듯이, 서버는 클라이언트에 서비스를 제공합니다. 빠른 데이터 액세스를 위해 데이터 저장소나 데이터베이스와 공존할 수 있습니다.
  • 요청: 요청은 클라이언트가 서버로부터 서비스를 요청하기 위해 사용하는 통신 모드입니다.
  • 응답: 응답은 또한 서버가 서비스(데이터/정보)를 클라이언트에게 다시 보내기 위해 사용하는 통신 모드입니다.

React 클라이언트 컴포넌트

위에서 언급한대로, 전통적으로 React 컴포넌트는 클라이언트 사이드에서 실행됩니다. 서버와 상호 작용할 때, 요청을 보내고 응답이 돌아올 때까지 기다립니다. 응답을 받으면 클라이언트는 다음 작업을 트리거합니다.

요청한 서비스가 성공적으로 완료되면 클라이언트 컴포넌트는 UI에 맞게 작동하고 성공 메시지를 표시합니다. 오류가 발생한 경우 클라이언트 컴포넌트는 사용자에게 오류를 보고합니다.

image-176

클라이언트 서버 모델에서의 React 클라이언트 컴포넌트

네트워크 워터폴를 발생시킬 때 클라이언트 컴포넌트의 응답이 지연되어 사용자 경험이 저하됩니다. 그렇다면 이를 어떻게 완화할 수 있을까요?

React 서버 컴포넌트(RSCs)는 어떻게 도움을 줄까요?

React 컴포넌트를 서버로 옮겨보는 것은 어떨까요? 그리고 아마도 데이터 저장소와 동일한 곳에 위치시키고... 그런데 이것이 실제로 가능한 일일까요?
네! 이제 React Server Components에 대해 알아보겠습니다. 이러한 새로운 컴포넌트는 서버에 있으므로 데이터를 더 빨리 가져올 수 있습니다. 네트워크를 통해 왕복하지 않고도 파일 시스템 및 데이터 저장소와 같은 서버 인프라에 액세스할 수 있습니다.

image-177

클라이언트 서버 모델에서의 React 서버 컴포넌트

이제 서버 컴포넌트 측면에서 생각해야 하기 때문에, React 개발자에게는 완전한 패러다임 전환입니다.

RSC를 사용하면, 데이터 가져오는 로직을 서버로 이동하여(네트워크 호출 없이 데이터를 가져오도록) 서버 자체에서 준비할 수 있습니다. 클라이언트로 돌아오는 데이터는 모든 데이터와 함게 잘 구성된 컴포넌트 입니다. 얼마나 놀라운 일인가요?

이는 React 서버 컴포넌트를 사용하면 다음과 같은 코드를 작성할 수 있음을 의미합니다.

import { dbConnect } from '@/services/mongo'

import { addCourseToDB } from './actions/add-course'

import CourseList from './components/CourseList'

export default async function Home() {

  // MongoDB 연결
  await dbConnect();
  
  // db의 모델을 이용하여 모든 course를 얻는다.
  const allCourses = await courses.find();
  
  // 서버 쪽에서 콘솔이 찍힌다.
  console.log({allCourses})

  return (
    <main>
      <div>
        <CourseList allCourses={allCourses} />  
      </div>
    </main>
  )
}

React 서버 컴포넌트의 예

저것 보세요! 몇 가지 변경 사항을 즉시 알아챌 수 있습니다.

  • 이 컴포넌트는 비동기 호출을 처리하므로 async 타입 입니다.
  • 우리는 컴포넌트 자체에서 데이터베이스(MongoDB)에 연결합니다. 와우! 보통 이러한 코드는 Node.js 또는 Express에서 볼 수 있습니다, 맞나요?
  • 그런 다음 데이터베이스를 쿼리하고 렌더링을 위해 JSX에 전달할 데이터를 가져옵니다.

콘솔 로그는 브라우저 콘솔이 아닌 서버 콘솔에서 찍히는 것에 주목하세요.

또한 상태 관리 (useState)와 이펙트 관리 (useEffect)를 완전히 제거했습니다. 깔끔하고 간단합니다.

React 서버 컴포넌트를 사용하면, useEffect를 사용할 필요가 없을 수도 있습니다(영원히!).

React 서버 컴포넌트의 한계

이러한 이점도 갖고 있지만, 기억해야 할 RSC의 제한 사항도 있습니다.

  • RSC는 서버에 남아 있고 서버에서 렌더링됩니다. 클라이언트 사이드과 관련된 것이 없습니다. 이것은 서버 컴포넌트에 사용자 인터렉션을 추가할 수 없음을 의미합니다. 예를 들어, 이벤트 핸들러나 useState, useReducer, useEffect와 같은 React 훅을 서버 컴포넌트에서 사용할 수 없습니다.
  • 서버 컴포넌트에서 localstorage, bluetooth, web USB와 같은 브라우저 웹 API를 사용할 수 없습니다.
  • 클라이언트 상호 작용과 관련된 모든 것에 대해서는, 계속 클라이언트 컴포넌트를 사용해야 합니다.

이해가 되시나요? 그렇다면 어떻게 애플리케이션에 맞게 컴포넌트를 가장 잘 정리할 수 있을까요?

클라이언트와 서버 컴포넌트를 함께 사용하는 방법

당신의 앱은 서버 및 클라이언트 컴포넌트의 조합일 수 있습니다. 곧 예제를 보겠지만, 먼저 개념을 이해해 봅시다.

서버 컴포넌트는 클라이언트 컴포넌트를 가져와 렌더링할 수 있지만, 클라이언트 컴포넌트는 내부에서 서버 컴포넌트를 렌더링할 수 없습니다. 클라이언트 컴포넌트에서 서버 컴포넌트를 사용하려면 props로 전달하여 사용할 수 있습니다.

컴포넌트 계층 구조의 루트에 서버 컴포넌트를 두고 컴포넌트 트리의 말단으로 클라이언트 컴포넌트를 밀어 넣는 것이 좋습니다.

서버 컴포넌트의 최상단에서 데이터 페칭이 일어날 수 있으며, React가 허용하는 방식대로 전달할 수 있습니다. 사용자 상호 작용(이벤트 핸들러) 및 브라우저 API에 액세스는 말단의 클라이언트 컴포넌트에서 처리할 수 있습니다.

image-186

서버와 클라이언트 컴포넌트를 포함하는 컴포넌트 트리

잠깐, RSC는 서버 사이드 렌더링(SSR)과 동일하지 않나요?

아니요, 그렇지 않습니다. RSC와 SSR 둘 다 이름에 "서버"라는 단어가 들어가 있지만 비슷한 부분은 이 뿐입니다.

서버 사이드 렌더링(SSR)에서는, 서버에서 날것의 HTML을 클라이언트로 보내고, 그런 다음 모든 클라이언트 사이드 자바스크립트가 다운로드됩니다. React는 HTML을 상호작용 가능한 React 컴포넌트로 변환하기 위해 하이드레이션(hydration) 프로세스를 시작합니다. SSR에서 컴포넌트는 서버에 머무르지 않습니다.

지금까지 React 서버 컴포넌트를 통해 컴포넌트가 서버에 남아 있고 네트워크 왕복을 거치지 않고 서버 인프라에 액세스할 수 있음을 알게 되었습니다.

SSR은 애플리케이션의 초기 페이지를 더 빠르게 로드하는 데 유용합니다. 앞으로 SSR과 RSC를 문제없이 함께 사용할 수 있습니다.

Next.js(RSC 포함)와 MongoDB를 사용하여 강의 목록 페이지를 구축하는 방법

이제 React 서버 컴포넌트를 사용하는 응용 프로그램을 만들어 보겠습니다. Next.js는 최근 릴리스에서 RSC를 채택한 주요 웹 프레임워크입니다.

그래서 우리는 이제 Next.js에서 서버 컴포넌트를 만드는 방법과 클라이언트 컴포넌트를 만드는 방법이 어떻게 다른지를 보여주기 위해 강의 목록 페이지를 만들 것입니다.

여기서는 Next.js나 MongoDB를 자세히 배우지 않을 것입니다. React 서버 컴포넌트가 어떻게 동작하는지 그리고 클라이언트 컴포넌트와 어떻게 다른지 알려주기 위한 예로 이 애플리케이션을 사용하고 있습니다.

먼저 데이터 저장소에 강의 데이터를 추가해 보겠습니다. 이 앱에서는 MongoDB를 사용했습니다. 아래 이미지는 세 개의 강의에 대해 세 개의 문서가 추가된 것을 보여줍니다.

image-178

Mongo Compass - 강좌 모음

다음으로, MongoDB에 연결하는 유틸리티 함수를 만들겠습니다. 이것은 Mongoose와 MongoDB URI를 사용하여 자바스크립트 기반 프로젝트에서 MongoDB에 연결하는 데 사용할 수 있는 일반적인 코드입니다.

import mongoose from "mongoose";

export async function dbConnect(): Promise<any> {
  try {
    const conn = await mongoose.connect(String(process.env.MONGO_DB_URI));
    console.log(`Database connected : ${conn.connection.host}`);
    return conn;
  } catch (err) {
    console.error(err);
  }
}

MongoDB 연결을 위한 코드 스니펫

이제 MongoDB 문서와 매핑하는 모델을 만들어야 합니다. 여기서는 강의 데이터를 다루고 있으므로, 이에 해당하는 모델은 다음과 같습니다.

import mongoose, { Schema } from "mongoose";

const schema = new Schema({
  name: {
      required: true,
      type: String
  },
  description: {
      required: true,
      type: String
  },
  cover: {
    required: true,
    type: String
  },
  rating: {
    required: true,
    type: Number
  },
  price: {
    required: true,
    type: Number
  },
  createdOn: {
    type: { type: Date, default: Date.now }
  },
  link: {
    required: true,
    type: String
  },
  type: {
    required: true,
    type: String
  },
  comments: {
    required: false,
    type: [{ body: String, date: Date }]
  }
});

export const courses = mongoose.models.course ?? mongoose.model("course", schema);

Mongoose를 사용한 강의 모델

이제 마법이 시작됩니다! Next.js 앱 라우터를 사용하면 모든 컴포넌트가 기본적으로 서버 컴포넌트입니다. 이것은 서버 근처에 위치하고 서버 생태계에 액세스할 수 있음을 의미합니다.

아래 코드는 일반적인 Next.js 컴포넌트지만 특별한 기능이 있습니다. 이 컴포넌트에서 데이터베이스 연결을 직접 하여 어떤 state와 effect 관리를 하지 않고도 데이터를 직접 쿼리할 수 있습니다. 멋지죠?

이 컴포넌트에서 기록하는 모든 내용은 브라우저 콘솔에 찍히지 않습니다. 왜냐하면 이것은 서버 컴포넌트이기 때문입니다. 로그는 서버 콘솔에서 확인할 수 있습니다(아마도 yarn dev 명령을 사용하여 서버를 시작한 터미널에서).

데이터베이스와의 상호 작용은 비동기적 이므로, 호출 시 await 키워드를 사용하고 컴포넌트에는 async 키워드를 사용합니다. 응답을 받으면 이를 자식 컴포넌트에 props로 전달합니다.

import { dbConnect } from '@/services/mongo'
import { courses } from '@/models/courseModel'
import { addCourseToDB } from './actions/add-course'

import AddCourse from './components/AddCourse'
import CourseList from './components/CourseList'

export default async function Home() {

  // MongoDB 연결
  await dbConnect();
  
  // 모델을 이용하여 db로 부터 모든 강의를 가져옴.
  const allCourses = await courses.find().select(
  						["name", "cover", "rating"]);
  
  // 서버 콘솔에서 모든 출력값 표시
  console.log({allCourses})

  return (
    <main>
      <div>
        <h1>Courses</h1> 
        <AddCourse addCourseToDB={addCourseToDB} />
        <CourseList allCourses={allCourses} />  
      </div>
    </main>
  )
}

page.tsx - 서버 컴포넌트

Home 컴포넌트에는 아래 내용이 포함되어 있습니다.

  • 제목(Heading)
  • 강의 추가 버튼을 래핑한 컴포넌트(AddCourse)
  • 강의 목록을 표시하는 컴포넌트(CourseList)
Screenshot-2023-07-25-at-9.58.57-AM

강의 목록 페이지

서버 컴포넌트는 클라이언트 및 서버 컴포넌트 모두 렌더링할 수 있다는 것을 알고 있습니다. AddCourse 컴포넌트는 사용자 인터렉션이 필요하며, 사용자가 버튼을 클릭하여 강의를 추가해야 합니다. 따라서 이는 서버 컴포넌트가 될 수 없습니다 (앞에서 읽은 서버 컴포넌트의 제한 사항을 기억하세요)!

그러므로 AddCourse를 위한 클라이언트 컴포넌트를 만들어 봅시다. Next.js 앱 라우터를 사용하면 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다. 클라이언트 컴포넌트를 만들고 싶다면 해당 컴포넌트의 맨 위에서(import 문 이전에도 됨) 'use client'라는 지시문을 사용하여 명시적으로 만들어야 합니다.

'use client'

import { useState } from 'react';
import Modal from './Modal';
import AddCourseForm from "./AddCourseForm";

export default function AddCourse({
  addCourseToDB,
}: {
  addCourseToDB: (data: any) => Promise<void>
}) {
  const [showAddModal, setShowAddModal] = useState(false);
  const add = async(data: any) => {
    await addCourseToDB(data);
    setShowAddModal(false);
  }

  return (
    <>
    <button
      onClick={() => setShowAddModal(true)}
    >
      Add Course
    </button>
    <Modal 
      shouldShow={showAddModal} 
      body={
        <AddCourseForm 
          saveAction={add} 
          cancelAction={() => setShowAddModal(false)} />} />
    </>
  )
}

AddCourse - 클라이언트 컴포넌트

CourseList 컴포넌트는 어떠한 이벤트 핸들러도 필요하지 않으므로, 서버 컴포넌트로 유지할 수 있습니다.

import Image from 'next/image'
import Link from 'next/link'

export default function CourseList(courseList: any) {
  const allCourses = courseList.allCourses;
  return(
    <div>
      {
        allCourses.map((course: any) =>
        <Link key={course['_id']} href={`/courses/${course['_id']}`}>
          <div>
            <Image
              src={course.cover}
              width={200}
              height={200}
              alt={course.name}
            />
            <h2>{course.name}</h2>
            <p>{course.rating}</p>
          </div> 
        </Link> 
      )}
    </div>  
  )

}

CourseList - 서버 컴포넌트

또한 브라우저의 개발자 도구에서 Sources 탭을 확인하여 클라이언트에 다운로드되는 내용과 서버에 남아 있는 내용을 식별할 수 있습니다. 여기서 page.tsx 파일이나 CourseList.tsx 파일을 볼 수 있나요? 아니요. 왜냐하면 그것들은 서버 컴포넌트이며 클라이언트 번들의 일부가 아니기 때문입니다.

우리는 앱에서 명시적으로 클라이언트 컴포넌트라고 표시한 컴포넌트만 볼 수 있습니다.

image-179

클라이언트 번들 확인

이 애플리케이션의 흐름을 통해 이론이 실제와 어떻게 연결되는지 보여주었기를 바랍니다. 이제 React 앱에서 서버 컴포넌트를 사용하는 방법을 이해하셨을 것입니다.

요약

요약하면 다음과 같습니다.

  • React 서버 컴포넌트는 네트워크 왕복 없이 백엔드 접근이 가능합니다.
  • React 서버 컴포넌트를 통해 네트워크 워터폴을 피할 수 있습니다.
  • React 서버 컴포넌트는 자동 코드 분할(splitting)을 지원하며 번들 크기를 제로로 줄여 앱의 성능을 향상시킵니다.
  • 이러한 컴포넌트는 서버 측에 있으므로 클라이언트 사이드 이벤트 핸들러, state 및 effect에 액세스할 수 없습니다. 이는 이벤트 핸들러나 useState, useReducer, useEffect와 같은 React 훅을 사용할 수 없음을 의미합니다.
  • React 서버 컴포넌트는 클라이언트 컴포넌트를 가져와 렌더링할 수 있지만, 반대로는 불가능 합니다. 그러나 서버 컴포넌트를 클라이언트 컴포넌트에 props로 전달할 수 있습니다.

마치기 전에..

이상으로 마무리합니다. 이 글이 유익하고 통찰력을 얻었기를 바랍니다.

저와 소통해보세요.

  • 제 YouTube 채널인 tapaScript에서 교육을 진행하고 있습니다. JavaScript, ReactJS, Node.js, Git 및 웹 개발의 기본을 배우고 싶다면 구독해주세요.
  • 웹 개발 및 프로그래밍 팁을 매일 놓치고 싶지 않다면 Twitter 또는 LinkedIn 팔로우를 해주세요.
  • GitHub에서 제 오픈 소스 작업을 확인해보세요.

다음 글에서 뵙겠습니다. 그 동안 자신을 돌보며 행복하게 지내세요.