서버 컴포넌트 이해하기

서버컴포넌트를 알아보자!

2023-10-05

리액트 서버 컴포넌트는 순수한 랜더링을 넘어 프레임워크 내의 데이터 가져오기 및클라이언트-서버 통신을 통합합니다.

우리는 왜 리액트 서버 컴포넌트가 만들어졌고, 언제 그것을 사용할지 이야기 해버려고 합니다.

또한 앱 라우터를 이용해 Next.js가 리액트 서버 컴포넌트의 구현을 어떻게 향상시켰는지 알아보려고 합니다.

우리는 왜 서버 컴포넌트가 필요한가요?

리액트 이전의 생태계를 눈여겨 볼 필요가 있습니다. PHP와 같은 언어는 클라이언트-서버가 더 긴밀한 관계를 갖고 있었습니다. 모놀로식 아키텍쳐에서는 작성 중인 페이지 내에서 바로 데이터를 호출하기 위해 서버로 향할 수 있었습니다.

마크다운 이미지

그러나,이것은 여러 단점이 존재했습니다. 여러 팀이 존재하고 높은 트래픽의 요구에 따라 단일 애플리케이션을 확장하는 것에 어려움이 존재했습니다.

리액트는 코드베이스의 이러한 복잡성을 위해 만들어졌습니다. 클라이언트와 서버의 관심사를 분리해 프론트엔드를 더 유연하게 구성할 수 있게 되었습니다. 이것은 특히 팀에게 더 중요했습니다.

여러 세기에 걸친 진화(MPA와 SPA), (CSR,SSR)의 최종적인 목표는 동일합니다. 사용자에게 빠른 데이터를 제공하고, 뛰어난 상호작용을 제공하며, 뛰어난 개발자 경험을 제공하는 것이 그 목표입니다.

서버 사이드 랜더링과 Suspense는 무엇을 해결했습니까?

서버컴포넌트를 따라서 , 해결해야할 다른 중요한 문제점이 존재했습니다. 왜 리액트 서버 컴포넌트가 나왔는지 이해하기 전에, 먼저 서버사이드 랜더링과 서스펜스의 필요성을 파악하는 것이 도움이 됩니다.

서버사이드 랜더링은 초기 데이터의 로드에 집중합니다. pre-rendered된 HTML을 클라이언트에 보내고 이것은 다운된 자바스크립트에 Hydrated되고 그 후 비로소 일반적인 리액트 앱처럼 동작합니다. SSR은 또한 오직 한번만 발생합니다. 바로 페이지에 직접 이동하는 그 시점입니다.

서버사이드 랜더링으로, 사용자는 HTML을 더 빠르게 받을 수 있습니다. 그러나 Hydrated되기 전, 자바스크립트와 상호작용하기 전에 거대한 기다림이 존재할 수 있습니다.

이것을 해결하기 위해 리액트에서는 Suspense를 개발합니다. Suspense는 서버사이드-HTML 스트리밍을 허용하고 클라이언트에 대한 선택적인 Hydration을 허용합니다.

마크다운 이미지

컴포넌트를 Suspense를 감싸서, React에게 해당 컴포넌트를 기다리지말고,나머지 페이지를 다른 요소에 의해 차단되지 않고, 로드되도록 지시할 수 있습니다. (서버에게 해당 컴포넌트의 랜더링과 Hydration을 제거하도록 지시합니다).

만약 사용자가 특정 컴포넌트와 상호작용하려고 시도한다면, 해당 구성요소는 다른 구성요소보다 더 높은 우선순위가 지정됩니다.

이로인해(Suspense의 등장) 상황은 크게 개선되었지만 여러 문제가 남아있습니다.

리액트 서버 컴포넌트가 없다면 나오는 흐름도입니다.

마크다운 이미지

더 빠르게 흐름도를 개선할 수 있는 방법이 존재합니다.

리액트 서버 컴포넌트는 무엇을 하나요?

이러한 이슈를 해결하기 위해 리액트는 서버컴포넌트를 만들었습니다. 리액트 서버 컴포넌트는 개별적으로 데이터를 받아오고 온전히 서버에서 랜더링됩니다. 그리고 HTML의 결과물이 클라이언트 리액트 컴포넌트 트리에 스트리밍됩니다.필요에 따라 다른 컴포넌트들과 상호작용합니다.

마크다운 이미지

이러한 과정은 클라이언트의 재랜더링을 제거할 수 있고, 성능을 향상시킬 수 있습니다.

즉, 가장 가까운 데이터자원인 서버에서, 집약적인 랜더링을 처리하고, 클라이언트의 부담을 줄여줍니다. 만약 서버컴포넌트가 다시 랜더링될 필요가 있다면(상태의 변경으로), 서버에서 새로 갱신되고 새고 로침 없이 기존 DOM에 반영이됩니다. 결국, 서버에서 일부가 업데이트 되어도, 클라이언트의 상태는 유지됩니다.

리액트 서버 컴포넌트 : 성능과 번들 크기

리액트 서버 컴포넌트는 클라이언트의 번들 크기를 줄여주고 성능을 개선할 수 있습니다.

전통적으로, 클라이언트는 애플리케이션을 위해, 모든 의존성과 코드를 다운로드 합니다. 리액트의 code-splitting이 없다면, 이것은 사용자에게 자신이 있는 페이지에서 사용되지 않는 일부의 코드를 보낼 수도 있음을 의미합니다. 그러나 리액트 서버 컴포넌트는 모든 의존성을 서버에서 해결합니다. 리액트 서버 컴포넌트들의 코드들은 모두 서버에서 랜더링되고, 이 작업은 클라이언트의 컴퓨터나 휴대폰보다 훨씬 더 빠릅니다. 그 다음 이 처리된 결과와 클라이언트 요소만 브라우저로 내보냅니다.

마크다운 이미지

다른 말로는, 리액트 서버컴포넌트로, 초기의 페이지 로드가 빨라지고 가벼워질 수 있게 만들 수 있다는 것을 의미합니다. 따라서 기본 클라이언트의 코드의 크기가 예측 가능해지며, 프로그램이 커짐에 따라 클라이언트 코드의 크기가 증가하지 않을 수도 있습니다.

리액트 서버 컴포넌트는 클라이언트 컴포넌트와 같은 트리에서 랜더링될 수 있습니다. 주요한 애플리케이션의 코드를 서버로 넘김으로써, 클라이언트 측 데이터가 서버로부터의 데이터에 의존하는 문제를 빠르게 해결하는 데 도움이 될 수 있습니다.

전통적인 CSR방식에서 컴포넌트들은 리액트 서스펜스를 비동기 작업이 완료되기를 기다리는 동안 랜더링 과정을 "일시정지"하는 것에 사용합니다.리액트 서버컴포넌트는 데이터 패칭과 랜더링을 서버에서 처리함으로써, 서스펜스가 완료된 페이지를 더 빠르게 랜더링 할 수 있습니다.

리액트 서버컴포넌트는 SSR과 Suspense를 대체하는 목적이 아닙니다.오히려 사용자가 필요로 할떄 애플리케이션의 부분을 제공하기 위해 함께 동작할 수 있습니다.

리액트 서버컴포넌트를 써서, 다음의 흐름도를 구성할 수 있습니다.

마크다운 이미지

Nextjs와 리액트 서버 컴포넌트를 이용해, 데이터 패칭과 UI랜더링을 동일한 컴포넌트에서 끝낼 수 있습니다. 추가적으로 서버 액션은 유저에게 페이지에서 자바스크립트가 로드되기 전에 서버측 데이터와 상호작용할 수 있는 방법을 제공합니다.

리액트 서버 컴포넌트 : 한계

모든 서버컴포넌트 코드는 라이프사이클 훅을 사용할 수 없습니다.(useState나 useEffect등). 그러나 여전히 클라이언트와 서버 액션을 갖고 상호작용할 수도 있습니다. (13.4버전에 server actions가 추가되었습니다.)

또한, 리액트 서버 컴포넌트는 웹 소켓과 같은 지속적인 연결을 지원하진 않습니다. (해당 경우 폴링 방식이 사용될 수 있습니다)

서버 컴포넌트와 클라이언트 컴포넌트의 균형

리액트 서버컴포넌트가 완전히 클라이언트 컴포넌트를 대체하지 못한다는 걸 아는것은 중요합니다. 각 컴포넌트는 언제 사용할지 결정되어야 하며, 동적 데이터 패칭을 위해 서버컴포넌트를, 더 좋은 상호작용을 위해 클라이언트 컴포넌트를 사용하는 것입니다.

서버사이드 랜더링과 데이터가져오기에 리액트 서버 컴포넌트를 사용하는 것을 고려해보세요. (반면 클라이언트 컴포넌트는 상호작용과 관련한 것들을 처리합니다.). 올바른 균형을 맞추면, 효율적이고 매력적인 애플리케이션을 구축할 수 있습니다.

중요한 것은 애플리케이션을 일반적인 환경이 아닌 상태에서 테스트해보는 것입니다. 컴퓨터가 느리거나, 느린 와이파이 등의 환경에서 , 어쩌면 당신의 애플리케이션이 컴포넌트의 올바른 조합으로 잘 작동하는 것을 보고 놀랄지도 모릅니다.

리액트 서버 컴포넌트는 클라이언트 측의 자바스크립트를 많이 사용하는 사용자에게 부담을 주는 문제에 대해 완전한 해결책은 아니지만,사용자의 장치에 대한 부하를 줄이고 애플리케이션의 성능을 향상할 수 있습니다.

개선된 Next js의 데이터 호출

리액트 서버 컴포넌트는 서버로부터 데이터를 불러옵니다, 이는 서버-클라이언트 간의 상호작용을 줄이고, 백엔드 데이터에 보다 안전하게 접근할 수 있도록 도와줍니다. Next는 기본적인 fetch API를 확장해 캐싱과 재검증을 할 수 있도록 도와줍니다.이 확장된 fetch는 서버컴포넌트, Route handler, 서버 액션에서 사용할 수 있습니다.

서버측에서 데이터를 불러오는 것은 전체 클라이언트를 차단하지 않고 더 신속하게 해결할 수 있기 때문에 오버헤드가 적습니다. Next의 App Router에서 모든 가져오는 데이터는 정적이며 빌드 타임 때 랜더링됩니다. 다만 이는 fetch options으로 쉽게 수정할 수 있습니다.

async function getData() {
  const res = await fetch('https://api.example.com/...')
 
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return <main></main>
}

데이터 캐싱

캐싱은 데이터를 저장하므로, 모든 요청 시 데이터를 매번 다시 가져올 필요가 없습니다. 빌드 시간이나 요청 시간에 데이터를 가져와 캐시하고, 해당 요청을 재사용 할 수 있습니다.

//force-cache는 기본값입니다.
fetch('https://,,,',{cache:'force-cache'})

next.revalidate옵션을 fetch에 사용해, 재검증 시간을 각각의 fetch 요청마다 설정할 수 있습니다. 이것은 Data-Cache를 재검증 할 것이고, 새 데이터를 가져와 구성 요소가 다시 서버에서 랜더링되어집니다.

//1시간마다 재검증
fettch('https://...',{next:{revalidate:3600}})

(날이 갈수록 험난해지는 프론트엔드 세상..)

Understanding React Server Components

마크다운 이미지