NextJS와 친숙하지 않은 개발자의 SSR, RSC를 이해하기 위한 노력.. 🖋️ (+500년만에 전공 활용하기)
지난 10년간, 리액트 및 리액트 생태계는 지속적으로 진화해왔습니다. 각 버전 마다 새로운 개념, 최적화, 그리고 몇몇 패러다임의 변화를 도입하며 웹 개발로 가능하다고 생각되던 경계를 넓혀왔습니다.
리액트 서버 컴포넌트(RSC)는 리액트 훅의 등장 이래로 가장 새롭고 중요한 변화입니다. 특히 리액트 커뮤니티의 영향력 있는 목소리들이 리액트 서버 컴포넌트를 지지해왔고, 리액트의 미래에서 있어 서버 컴포넌트의 중요성을 강조해왔습니다. 하지만 이런 변화에 리액트 커뮤니티는 엇갈린 반응을 보입니다.
제게는 Linkin Park의 이 가사가 2024년에 들어서며 마주한 리액트의 진화를 둘러싼 감정을 잘 표현합니다.
일단 그것이 어떻게 작동하는지 이론을 알게 되었기 때문에,
모두가 그 다음 것도 처음과 같기를 바라죠.
새로운 아키텍처는 리액트 서버 컴포넌트를 위해 고안되었고, 서버에서만 동작하며 서버사이드렌더링의 이점을 활용할 수 있도록 합니다. 하지만 우리는 우리가 잘 알고, 애정하는 리액트에 너무 익숙해져서, 패러다임의 변화를 수용하기 망설이며 회의적인 태도를 보입니다.
그리고 서버/클라이언트 컴포넌트의 개념은 리액트 개발에 있어 큰 개선점과 유연성을 제공하지만, 오해하기 쉬운 개념을 포함하기도 합니다.
이 블로그 포스트의 목표는 당신에게 몇 년에 걸친 리액트 렌더링 진화의 여정을 안내하고, 왜 리액트 서버 사이드 컴포넌트가 불가피한지, 그리고 사용자에게 뛰어난 경험을 제공하는 비용 효율적이고 고성능 리액트 어플리케이션 개발의 미래인지 이해하도록 돕는 것입니다.
만약 당신이 한동안 개발을 해왔다면, 리액트가 싱글 페이지 어플리케이션(SPA)을 만드는 최적의 라이브러리라고 기억할 것입니다.
일반적인 SPA에서는 클라이언트가 요청을 하고, 서버가 브라우저(클라이언트)로 단일의 HTML 페이지를 전송합니다. 이 HTML 페이지는 보통 자바스크립트 파일을 참조하는 간단한 div 태그만 포함된 경우가 많습니다. 이 자바스크립트 파일은 리액트 라이브러리와 당신의 코드를 포함해 어플리케이션이 동작하는 데 필요한 모든 것을 담고 있으며, HTML 파일이 파싱될 때 다운로드됩니다.
다운로드된 자바스크립트 코드가 컴퓨터에 HTML을 생성하고 root div 요소 하위의 DOM에 집어넣으면, 당신은 브라우저의 UI를 확인할 수 있습니다.
서버 브라우저로부터 받아온 파일을 보여주는 View Source 옵션이 아닌, DOM Inspector에서 HTML이 나타나는 것을 보면 이 과정은 명백합니다.
컴포넌트 코드가 브라우저(클라이언트)에서 바로 UI로 변환되는 이 렌더링 방법은 클라이언트사이드 렌더링(CSR)로 알려져있습니다.
아래는 클라이언트사이드 렌더링을 시각화한 것입니다.
그리고 DOM inspecter와 리액트 SPA의 페이지 소스를 비교해 보면 다음과 같습니다.
CSR은 빠르게 SPA의 표준이 되었고, 널리 적용되었습니다. 그러나 머지않아 개발자들은 이 방식의 본질적인 문제점을 발견하기 시작했습니다.
첫째, div 태그만을 포함한 HTML을 생성하게 되면 검색 엔진이 인덱싱할 콘텐츠를 거의 제공하지 않기 때문에 SEO에 불리합니다. 번들 크기가 크고, 깊이 중첩된 컴포넌트로부터 API 응답을 위한 네트워크 요청이 쇄도하면 의미있는 콘텐츠가 크롤러가 인덱싱할 만큼 빠르게 렌더링되지 않을 수 있습니다.
둘째, 브라우저(클라이언트)가 데이터 페칭, UI 계산, HTML 상호작용 등 모든 작업을 처리하도록 하면 속도가 느려질 수 있습니다. 페이지가 로드되는 동안 사용자는 빈 화면이나 로딩 스피너를 보게 됩니다. 이 문제는 시간이 지나며 애플리케이션에 기능이 새로 추가될 때마다 자바스크립트 번들의 크기가 증가하면서 악화되고, 사용자가 UI를 보기 위한 대기 시간이 길어지게 합니다. 이러한 지연은 인터넷 연결 속도가 느린 유저들에게 특히 두드러집니다.
CSR은 오늘날 우리에게 익숙한 인터랙티브 웹 애플리케이션의 기반이 되었지만, SEO와 성능을 향상시키기 위해 개발자들은 더 나은 솔루션을 찾기 시작했습니다.
CSR의 단점을 극복하기 위해 Next.js와 같은 최신 React 프레임워크는 서버사이드 솔루션으로 전환했습니다. 이는 유저에게 콘텐츠를 전달하는 방식을 근본적으로 변화시킵니다.
페이지를 구성하기 위해 클라이언트 측 자바스크립트에 의존해 빈 HTML 파일을 보내는 대신, 서버가 전체 HTML을 렌더링하는 역할을 담당합니다. 이렇게 완전한 형식이 갖춰진 HTML 문서는 브라우저로 직접 전송됩니다. HTML이 서버에서 생성되므로 브라우저는 이를 빠르게 파싱하고 보여주며, 초기 페이지 로드 시간이 단축됩니다.
서버사이드 렌더링을 시각화하면 다음과 같습니다.
서버사이드 방식은 CSR과 관련된 문제를 효과적으로 해결합니다.
첫째, 검색 엔진이 서버에서 렌더링한 콘텐츠를 쉽게 인덱싱할 수 있어 SEO가 크게 개선됩니다.
둘째, 브라우저는 빈 화면이나 로딩 스피너 대신 페이지 HTML 콘텐츠를 즉시 로드할 수 있습니다.
콘텐츠를 즉시 시각화하도록 하는 SSR의 접근법은, 페이지의 인터랙션과 연관될 때 특히 복잡합니다. 페이지의 전체 상호작용은 리액트 및 애플리케이션별 코드로 구성된 자바스크립트 번들이 브라우저에 의해 완전히 다운로드되고 실행될 때까지 보류됩니다.
하이드레이션이라고 불리는 이 중요한 단계에서 서버로부터 제공된 정적 페이지가 활성화됩니다. 하이드레이션이 진행되는 동안 리액트는 브라우저를 제어하고, 제공된 정적 HTML을 기반으로 메모리에 컴포넌트 트리를 재구성하며 트리 내에 인터랙션 요소를 배치합니다.
그런 다음 리액트는 이러한 요소에 필요한 자바스크립트 로직을 바인딩합니다. 여기에는 애플리케이션 상태 초기화, 클릭 및 마우스오버와 같은 동작에 대한 이벤트 핸들러 연결, 완전한 유저 인터랙션에 필요한 기타 동적 기능 설정 등이 포함됩니다.
더 깊이 알아보자면, 서버사이드 솔루션은 정적 사이트 생성(Static Site Generation)과 서버사이드 렌더링(Server-Side Rendering)이라는 두 가지 전략으로 분류할 수 있습니다.
SSG는 애플리케이션이 서버에 배포될 때 빌드 시 발생합니다. 이로 인해 이미 렌더링되어 제공할 준비가 된 페이지가 생성됩니다. 블로그 게시물처럼 자주 변경되지 않는 콘텐츠에 이상적입니다.
반면 SSR은 유저의 요청에 따라 페이지를 렌더링합니다. 소셜 미디어 피드처럼 로그인한 유저에 따라 HTML이 달라지는 개인화된 콘텐츠에 적합합니다. 일반적으로 두 가지를 통틀어 서버사이드 렌더링 또는 SSR이라고 합니다.
서버 사이드 렌더링(SSR)은 클라이언트 사이드 렌더링(CSR)에 비해 빠른 초기 페이지 로딩 및 SEO를 제공하는 큰 발전입니다. 그러나 SSR 또한 자체적인 개선 과제들이 있습니다.
SSR의 한 가지 문제는 데이터가 로드되는 동안 컴포넌트가 렌더링을 시작했다가 일시 중지하거나 "대기"할 수 없다는 것입니다. 컴포넌트가 데이터베이스나 다른 소스(예: API)에서 데이터를 페칭해야 하는 경우, 서버가 페이지 렌더링을 시작하기 전에 이를 완료해야 합니다. 서버가 페이지의 일부를 클라이언트로 전송하기 전에 필요한 모든 데이터를 수집해야 하므로 브라우저에 대한 서버의 응답이 지연될 수 있습니다.
SSR의 두 번째 문제는 성공적인 하이드레이션을 위해서는 브라우저의 컴포넌트 트리가 서버에서 생성한 컴포넌트 트리와 정확히 일치해야 한다는 것입니다. 즉, 모든 컴포넌트에 대한 자바스크립트가 클라이언트에 로드되어야 하이드레이션을 시작할 수 있습니다.
SSR의 세 번째 문제는 하이드레이션 자체와 관련되어 있습니다. 리액트는 컴포넌트 트리를 한 번에 하이드레이션하므로, 하이드레이션이 시작되면 트리 전체가 끝날 때까지 멈추지 않습니다. 결과적으로 모든 컴포넌트가 하이드레이션되어야 상호작용을 할 수 있습니다.
이 세 가지 문제ㅡ전체 페이지에 대한 데이터를 로드하고, 전체 페이지에 대한 JavaScript를 로드하고, 전체 페이지가 하이드레이션되어야 하는 문제ㅡ는 서버에서 클라이언트까지 이어지는 모 아니면 도(all-or-nothing)식의 워터폴 문제를 일으키며, 각 문제를 해결해야 다음 문제로 넘어갈 수 있습니다. 이는 실제 앱에서 자주 발생하듯 앱의 일부가 다른 부분보다 느린 경우에 비효율적입니다.
이러한 한계점들로 인해 리액트 팀은 개선된 새 SSR 아키텍처를 도입했습니다.
React 18은 전통적인 SSR의 성능 문제를 해결하기 위해 SSR을 위한 서스펜스(Suspense)를 도입했습니다. 이 새로운 아키텍처를 통해 <Suspense>
컴포넌트로 두 가지 주요 SSR 기능을 활용할 수 있습니다:
앞서 논의한 바와 같이, 전통적으로 SSR은 모 아니면 도 방식이었습니다. 서버는 전체 HTML을 렌더링한 다음 이를 클라이언트로 전송합니다. 클라이언트는 이 HTML을 표시하고, 전체 자바스크립트 번들이 로드된 후에야 리액트가 전체 애플리케이션을 하이드레이션하여 인터랙션을 활성화합니다.
이 프로세스를 시각화하면 다음과 같습니다.
그러나 React 18에서 우리는 새로운 가능성을 보았습니다. 메인 콘텐츠 영역과 같은 페이지의 일부를 서스펜스 컴포넌트로 감싸면, 페이지의 나머지 부분에 대한 HTML 스트리밍을 위해 메인 섹션 데이터 페칭을 기다릴 필요가 없다고 리액트에게 알려줍니다. 그러면 리액트는 전체 콘텐츠 대신 로드 스피너 등의 placeholder를 전송합니다.
서버가 메인 섹션 데이터를 준비하면 리액트는 스트림을 통해 추가 HTML을 전송하고, 해당 HTML 배치에 필요한 최소한의 JavaScript가 포함된 인라인 <script>
태그를 함께 제공합니다. 그 결과, 전체 리액트 라이브러리가 클라이언트 측에 로드되기 전이라도 메인 섹션의 HTML이 사용자에게 표시됩니다.
다음은 <Suspense>
를 사용한 HTML 스트리밍을 시각화한 것입니다.
이렇게 우리의 첫 번째 문제를 해결했습니다. 전체를 가져오기 전에 일부 페이지를 보여줄 수 있게 되었습니다. 특정 섹션이 초기 HTML을 지연시키더라도, 이후 스트림에 원활하게 통합될 수 있습니다. 이것이 <Suspense>
가 서버 사이드 HTML 스트리밍을 가능하게 하는 핵심입니다.
이제 초기 HTML 전달은 빨라졌지만, 여전히 다른 과제가 남아있습니다. 메인 섹션의 자바스크립트가 로드될 때까지 클라이언트 사이드의 앱 하이드레이션을 시작할 수 없다는 것입니다. 그리고 메인 섹션의 자바스크립트 번들이 큰 경우 프로세스가 상당히 지연될 수 있습니다.
이를 완화하기 위해 코드 스플리팅을 사용할 수 있습니다. 코드 스플리팅은 코드의 특정 부분을 즉시 로딩할 필요가 없다고 표시해 번들러에게 알려 이들을 별도의 <script>
태그로 분리하는 것입니다.
코드 스플리팅을 위해 React.lazy를 사용하면 메인 섹션의 코드를 주요 자바스크립트 번들에서 분리할 수 있습니다. 결과적으로 클라이언트는 메인 섹션의 코드를 기다리지 않고, 리액트와 메인 섹션을 제외한 자바스크립트를 독립적으로 다운로드할 수 있습니다.
이는 매우 중요합니다. 메인 섹션을 <Suspense>
로 감싸면 리액트에게 이 부분이 나머지 부분의 스트리밍뿐만 아니라, 하이드레이션도 방해하지 않아야 한다고 알려주는 것이기 때문입니다. 선택적 하이드레이션이라고 불리는 이 기능은 나머지 HTML과 자바스크립트 코드가 전부 다운로드되기 전에, 섹션들이 사용 가능해지면 하이드레이션할 수 있게 해줍니다.
유저 관점에서 보면, 처음에는 HTML로 스트리밍되는 정적인 콘텐츠를 받습니다. 그런 다음 리액트에게 하이드레이션을 지시합니다. 메인 섹션의 자바스크립트 코드는 아직 없지만, 다른 컴포넌트들을 선택적으로 하이드레이션할 수 있어 문제가 없습니다.
메인 섹션은 그 코드가 로드되고 나면 하이드레이션됩니다.
선택적 하이드레이션 덕분에 일부 무거운 자바스크립트가 페이지의 나머지 부분의 상호작용을 방해하지 않습니다.
<Suspense>
를 통한 선택적 하이드레이션을 시각화하면 다음과 같습니다.
심지어, 선택적 하이드레이션은 세 번째 문제인 "모든 것을 하이드레이션해야 무언가와 상호작용할 수 있다"는 필요에 대한 해결책이 됩니다. 리액트는 가능한 한 빨리 하이드레이션을 시작해 메인 콘텐츠가 하이드레이션되기를 기다리지 않고도 헤더나 사이드 내비게이션과 같은 요소들과 상호작용할 수 있게 합니다. 이 과정은 리액트에 의해 자동으로 처리됩니다.
여러 컴포넌트가 하이드레이션을 기다리는 상황에서, 리액트는 유저 인터랙션을 기반으로 하이드레이션의 우선순위를 정합니다. 예를 들어, 사이드바가 하이드레이션될 예정이었는데 사용자가 메인 콘텐츠 영역을 클릭하면, 리액트는 클릭 이벤트의 캡처 단계에서 클릭된 컴포넌트를 동기적으로 하이드레이션합니다. 따라서 컴포넌트는 인터랙션에 즉시 응답할 준비가 되고, 사이드 내비게이션은 나중에 하이드레이션됩니다.
다음은 유저 인터랙션에 기반한 하이드레이션의 시각화입니다.
첫째, 자바스크립트 코드가 브라우저로 비동기적으로 스트리밍되더라도, 결국 유저는 웹페이지의 전체 코드를 다운로드해야 합니다. 애플리케이션에 기능이 추가될수록 유저가 다운로드해야 하는 코드의 양도 증가합니다. 여기서 중요한 질문을 던집니다. 유저가 정말 이렇게 많은 데이터를 다운로드해야 할까요?
둘째, 이러한 접근법에 따르면 실제 인터랙션의 필요성과 무관하게, 모든 리액트 컴포넌트가 클라이언트 측에서 하이드레이션을 거쳐야 합니다. 이 과정은 비효율적으로 리소스를 소비하고, 로딩 및 유저 인터랙션을 지연시킬 수 있습니다. 유저의 기기가 클라이언트 측 인터랙션이 필요하지 않을 수도 있는 컴포넌트를 처리하고 렌더링해야 하기 때문입니다. 여기서 또 다른 의문이 생깁니다. 클라이언트 측 인터랙션이 불필요한 컴포넌트까지 모두 하이드레이션해야 할까요?
셋째, 서버의 뛰어난 집중 처리 능력에도 불구하고, 대부분의 자바스크립트 실행은 여전히 유저의 기기에서 이루어집니다. 이는 특히 성능이 좋지 않은 기기에서 퍼포먼스를 저하시킬 수 있습니다. 이는 또 다른 중요한 의문을 이끌어냅니다. 이렇게 많은 작업을 사용자의 기기에서 수행해야 할까요?
이러한 과제들을 해결하기 위해서는 단순히 점진적인 단계를 밟는 것만으로는 충분하지 않습니다. 우리는 더 강력한 해결책을 위해 도약해야 합니다.
리액트 서버 컴포넌트(RSC)는 리액트 팀이 설계한 새로운 아키텍처입니다. 이 접근법은 서버와 클라이언트 환경의 강점을 모두 활용해 효율성, 로드 시간, 상호작용성을 최적화하는 것을 목표로 합니다.
이 아키텍처는 클라이언트 컴포넌트와 서버 컴포넌트를 구분하는 이중 컴포넌트 모델을 도입합니다. 이 구분은 컴포넌트의 기능에 기반한 것이 아니라, 어디에서 실행되고 어떤 환경과 상호작용하도록 설계되었는지에 따른 것입니다. 이 두 가지 유형을 자세히 살펴보겠습니다.
클라이언트 컴포넌트는 우리가 이전 렌더링 기술에서 사용하고 논의해 온 익숙한 리액트 컴포넌트입니다. 이들은 일반적으로 클라이언트 측(CSR)에서 렌더링되지만, 서버에서 한 번 HTML로 렌더링될 수도 있으므로(SSR), 유저가 빈 화면 대신 페이지의 미리 렌더링된 HTML 콘텐츠를 즉시 볼 수 있게 합니다.
"클라이언트 컴포넌트"가 서버에서 렌더링된다는 개념이 혼란스러울 수 있지만, 이를 주로 클라이언트에서 실행되지만 최적화를 위해 서버에서도 한 번 실행될 수 있는(그리고 그래야 하는) 컴포넌트로 보면 이해가 쉬우실 겁니다.
클라이언트 컴포넌트는 브라우저와 같은 클라이언트 환경에 접근할 수 있어 상태, 효과, 이벤트 리스너를 사용해 인터랙션을 처리하고, 지리위치나 localStorage와 같은 브라우저 전용 API에 접근할 수도 있습니다. 이를 통해 RSC 아키텍처가 도입되기 전 몇 년 동안 해왔던 것처럼 특정 유즈 케이스에 맞는 프론트엔드를 구축할 수 있습니다.
사실, 클라이언트 컴포넌트라는 용어는 새로운 것을 의미하지 않습니다. 그저 기존의 컴포넌트를 새롭게 도입된 서버 컴포넌트와 구별하기 위한 것입니다.
다음은 Counter 클라이언트 컴포넌트의 예시입니다.
"use client"
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>Counter</h2>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
서버 컴포넌트는 서버에서만 작동하도록 특별히 설계된 새로운 유형의 리액트 컴포넌트를 말합니다. 클라이언트 컴포넌트와 달리, 이 코드는 서버에만 존재하며 클라이언트로 다운로드되지 않습니다. 이러한 설계는 리액트 애플리케이션에 여러 이점을 제공합니다. 이를 자세히 살펴보겠습니다.
첫째, 번들 크기 측면에서 서버 컴포넌트는 클라이언트로 코드를 전송하지 않으므로 큰 의존성이 서버 측에 남아 있습니다. 이는 인터넷이 느리거나 성능이 낮은 기기를 사용하는 유저에게 유리하며, 이러한 컴포넌트에 대한 자바스크립트를 다운로드하고 파싱하며 실행할 필요가 없어집니다. 또한 하이드레이션 단계를 제거해 애플리케이션 로딩과 사용자 인터랙션 속도를 높입니다.
둘째, 데이터베이스나 파일 시스템 같은 서버사이드 리소스에 직접 접근하므로써, 서버 컴포넌트는 추가적인 클라이언트단의 처리 없이 효율적인 데이터 페칭 및 렌더링을 가능하게 합니다. 서버의 계산 능력 및 데이터 소스와의 근접성을 활용해, 계산이 많은 렌더링 작업을 관리하고 클라이언트로는 상호작용 가능한 일부 코드만 전송합니다.
셋째, 서버 컴포넌트는 서버사이드에서만 실행되므로 토큰 및 API 키와 같은 민감한 데이터와 로직을 클라이언트 측에서 분리해 보안을 강화합니다.
넷째, 서버 컴포넌트는 데이터 페칭 효율성을 향상시킵니다. 일반적으로 useEffect를 사용해 클라이언트 측에서 데이터를 가져올 때, 자식 컴포넌트는 부모 컴포넌트가 자신의 데이터를 로드할 때까지 데이터를 로드할 수 없습니다. 이러한 순차적인 데이터 페칭은 종종 성능을 저하시킵니다.
주요 문제는 왕복 자체가 아니라, 이러한 왕복이 클라이언트에서 서버로 이루어진다는 것입니다. 서버 컴포넌트를 사용하면 이러한 왕복이 서버 내에서 이루어지도록 할 수 있습니다. 그 결과 요청 지연이 감소하고 전체 성능이 향상되어 클라이언트-서버간의 워터폴 문제가 사라집니다.
다섯째, 서버에서 렌더링하면 결과를 캐싱할 수 있어 이후의 요청이나 다른 유저를 위해 재사용할 수 있습니다. 이 접근 방식은 각 요청에 필요한 렌더링 및 데이터 페칭 양을 최소화하여 성능을 크게 개선하고 비용을 줄일 수 있습니다. 이는 자주 변경되지 않는 정적 데이터에 특히 유용합니다.
여섯째, 초기 페이지 로드 및 첫 번째 콘텐츠 페인트(FCP)는 서버 컴포넌트를 통해 크게 개선됩니다. 서버에서 HTML을 생성함으로써 페이지는 자바스크립트 다운로드, 파싱, 실행에 대한 지연 없이 즉시 렌더링됩니다. 이는 상호작용이 많지 않은 마케팅 사이트 페이지에 특히 유리합니다.
일곱째, 검색 엔진 최적화(SEO)와 관련하여, 서버에서 렌더링된 HTML은 검색 엔진 봇이 완전히 접근할 수 있어 페이지의 인덱스 가능성을 높입니다.
마지막으로 스트리밍입니다. 서버 컴포넌트를 사용하면 렌더링 프로세스를 관리 가능한 청크로 나누어, 준비가 완료되는 즉시 클라이언트로 스트리밍할 수 있습니다. 따라서 사용자는 일부 페이지를 더 빨리 볼 수 있고, 전체 페이지가 서버에서 렌더링될 때까지 기다릴 필요가 없습니다.
아래는 ProductList 페이지의 서버 컴포넌트 예시입니다. 이 비동기 컴포넌트는 서버 컴포넌트가 어떻게 실행되고 데이터를 직접 가져오는지를 보여줍니다.
export default async function ProductList() {
const res = await fetch("https://api.example.com/products");
const products = res.json();
return (
<main>
<h1>Products</h1>
{products.length > 0 ? (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
) : (
<p>No products found.</p>
)}
</main>
);
}
서버 컴포넌트의 도입은 리액트 생태계에 중요한 변화이며, 효율적이고 성능이 뛰어난 애플리케이션을 구축할 수 있게 합니다. 리액트 문서에서 설명하듯 이 새로운 렌더링 전략은 서버사이드 리액트와 클라이언트사이드 리액트의 장점을 결합해 개발자가 이미 친숙한 리액트 기능으로 보다 최적화된 애플리케이션을 만들 수 있도록 합니다.
리액트 서버 컴포넌트 패러다임에서 주목해야 할 점은 Next.js 앱의 모든 컴포넌트가 기본적으로 서버 컴포넌트로 간주된다는 것입니다.
클라이언트 컴포넌트를 정의하려면 파일 상단에 특별한 지시어, 즉 "use client"를 포함해야 합니다. 이 지시문은 서버에서 클라이언트 측으로 경계를 넘는 티켓 역할을 하며, 클라이언트 컴포넌트를 정의할 수 있게 해줍니다.
이는 번들러에게 이 컴포넌트와 이 컴포넌트가 가져오는 모든 컴포넌트가 클라이언트 측 실행을 위한 것임을 알립니다. 결과적으로 컴포넌트는 브라우저 API에 접근할 수 있으며 인터랙션을 처리할 수 있게 됩니다.
💡 "use server" 지시어는 클라이언트 측 코드에서 호출할 수 있는 서버 측 함수를 표시합니다. "use server"와 서버 액션에 대해서는 별도의 글에서 다룰 예정입니다.
Next.js를 리액트 프레임워크로 가정하고 RSC 렌더링 생명주기를 살펴보겠습니다.
💡 Vercel의 Next.js 13 App Router가 리액트 서버 컴포넌트(RSC) 아키텍처를 처음으로 지원했습니다.
리액트 서버 컴포넌트(RSC)의 경우, 세 가지 요소를 고려해야 합니다. 브라우저(클라이언트), 그리고 서버 측의 Next.js(프레임워크)와 리액트(라이브러리)입니다.
브라우저가 페이지를 요청하면 Next.js 앱 라우터가 요청된 URL을 서버 컴포넌트와 매칭합니다. 그런 다음 Next.js는 리액트에게 해당 서버 컴포넌트를 렌더링하도록 지시합니다.
리액트는 서버 컴포넌트와 그 자식 컴포넌트 중 서버 컴포넌트를 렌더링하여 RSC 페이로드라는 JSON 형식으로 변환합니다. 만약 어떤 서버 컴포넌트가 일시 중단되면, 리액트는 해당 하위 트리의 렌더링을 멈추고 대신 플레이스홀더 값을 보냅니다.
한편, 클라이언트 컴포넌트들은 라이프사이클 후반부에 실행될 지침들(JavaScript 코드와 이벤트 핸들러 등)과 함께 준비됩니다.
Next.js는 RSC 페이로드와 클라이언트 컴포넌트의 자바스크립트 지침을 이용해 서버에서 HTML을 생성합니다. 이 HTML은 브라우저로 스트리밍되어 즉시 빠르고 상호작용이 불가능한 프리뷰를 보여줍니다.
동시에, Next.js는 리액트가 각 UI 단위를 렌더링할 때마다 RSC 페이로드를 스트리밍합니다.
브라우저에서 Next.js는 스트리밍된 리액트 응답을 처리합니다. 리액트는 RSC 페이로드와 클라이언트 컴포넌트 지시에 따라 UI를 점진적으로 렌더링합니다.
모든 클라이언트 컴포넌트와 서버 컴포넌트가 로드되면 최종 UI 상태가 사용자에게 표시됩니다.
클라이언트 컴포넌트는 하이드레이션을 거쳐 정적인 앱을 상호작용 가능하도록 변환합니다.
이것이 초기 로딩 순서입니다. 다음으로, 앱의 일부를 업데이트하는 순서를 살펴보겠습니다.
브라우저가 특정 UI, 예를 들어 하나의 완전한 페이지를 재요청합니다.
Next.js는 요청을 처리하고 이를 요청된 서버 컴포넌트와 매칭합니다. Next.js는 리액트에게 컴포넌트 트리를 렌더링하도록 지시합니다. 리액트는 초기 로딩과 유사하게 컴포넌트를 렌더링합니다.
그러나 초기 단계와 달리 업데이트를 위해 HTML을 생성하지는 않습니다. Next.js는 응답 데이터를 클라이언트로 점진적으로 스트리밍합니다.
스트리밍된 응답을 받으면 Next.js는 새로운 결과로 페이지를 재렌더링합니다.
리액트는 새로 렌더링된 결과를 기존 컴포넌트와 조정(병합)합니다. UI 형식이 HTML이 아닌 특별한 JSON 형식이기 때문에, 포커스나 입력 값처럼 중요한 UI 상태를 보존하면서 DOM을 업데이트할 수 있습니다.
이것이 Next.js의 App Router에서 RSC 렌더링 생명주기의 핵심입니다.
리액트 서버 컴포넌트 아키텍처에서는 서버 컴포넌트가 데이터 페칭 및 정적인 렌더링을 담당하고, 클라이언트 컴포넌트는 애플리케이션의 인터랙션 요소를 렌더링하는 작업을 맡습니다.
결론적으로 RSC 아키텍처는 리액트 애플리케이션이 단일 언어, 단일 프레임워크, 그리고 일관된 API를 이용해 서버와 클라이언트 렌더링의 장점을 모두 활용할 수 있게 합니다. RSC는 고전적인 렌더링 기술을 개선하고, 한계를 극복하게 합니다.
RSC에 대한 더 자세한 맥락과 포괄적인 멘탈 모델을 위해서는 Next.js 문서를 참조하거나 YouTube에서 Next.js 튜토리얼을 시청하세요.
클라이언트 사이드 렌더링(CSR)의 문제
이를 해결하기 위한 서버 사이드 렌더링(SSR) 탄생! 그러나.. SSR의 한계
이를 해결하는 SSR Suspense! 그러나 여기도 문제가 있음
이를 해결하기 위한 RSC(React Server Components)의 탄생!
초기로딩 🌀
브라우저가 서버에 페이지 요청 ➡️ NextJS가 서버 컴포넌트를 매칭 ➡️ React가 서버 컴포넌트 트리 생성 + 클라이언트 컴포넌트 준비 ➡️ NextJS가 HTML 생성 ➡️ 브라우저에 빠른 초기 화면 표시 (미리보기) ➡️ RSC 페이로드 및 CC 자바스크립트 코드 스트리밍 & 점진적 렌더링 및 하이드레이션 ➡️ 상호작용 가능한 UI 완성!
업데이트 ♻️
브라우저가 특정 UI 재요청 ➡️ NextJS가 서버 컴포넌트를 매칭 ➡️ React가 서버 컴포넌트 렌더링 ➡️ NextJS가 RSC 페이로드 스트리밍 ➡️ React가 reconciliation을 통해 UI 업데이트