React 렌더링의 진화 : CSR,SSR 그리고 RSC까지
F-Lab : 상위 1% 개발자들의 멘토링
안녕하세요! F-Lab에서 멘토를 맡고 있는 플랫폼 기업에서 근무하는 프론트엔드 시니어 개발자 Gotama 입니다!
개요
React는 2013년 오픈소스로 공개된 이후 웹 개발 생태계에 큰 변화를 가져왔습니다. 초기에는 단순한 UI 라이브러리로 시작했지만, 지난 10년간 지속적인 발전을 통해 단일 렌더링 방식에서 이제는 다양한 렌더링 전략을 지원하는 포괄적인 생태계로 진화했습니다.
이 글에서는 React 렌더링 전략이 어떻게 진화해왔는지를 분석하고, 각 방식의 특징, 장단점 그리고 앞으로의 전망에 대해 살펴보겠습니다. 특히 React의 새로운 렌더링 전략 방식인 RSC에 대해 더욱 자세히 알아볼 예정인데요. RSC가 어떻게 기존 전략들의 한계를 극복하고, 새로운 가능성을 열어가는지 알아볼 것입니다. 이러한 분석은 React 개발자에게 아주 중요합니다. 이제 React 개발자는 프로젝트의 요구사항을 토대로 최적의 렌더링 전략을 선택하고 구현하는 것이 중요한 능력으로 자리매김 했기 때문입니다.
React 등장과 함께한 CSR과 SPA
웹 애플리케이션의 복잡성 증가에 따라, 전통적인 서버 중심 렌더링 방식과 DOM 조작 라이브러리(예: jQuery)의 문제점이 나타나기 시작했습니다. 이러한 패러다임의 전환기에 AngularJS와 React의 등장으로 CSR과 SPA 개념이 대중화되었는데요. 특히 React는 이러한 시대적 요구에 부응했습니다. 컴포넌트 기반 아키텍처와 가상 DOM을 도입하여 CSR을 효율적으로 구축할 수 있도록 지원하여 대중적인 라이브러리로 자리매김 하였습니다.
SPA의 부상과 React
- 사용자 경험 요구 증가 : 웹이 단순한 문서에서 복잡한 애플리케이션으로 진화하면서, 데스크톱 애플리케이션에 버금가는 반응성과 풍부한 상호작용이 요구되었습니다. React는 이러한 요구를 충족시키기 위해 컴포넌트 기반 아키텍처와 가상 DOM을 도입하여 효율적인 UI 업데이트를 가능하게 했습니다.
- 모바일 기기의 보편화 : 2010년 구글의 ‘Mobile First’ 전략 발표 이후, 모바일 환경에서의 웹 성능 최적화가 중요해졌습니다. 기존에 다소 무거웠던 jQuery보다 React의 경량화된 라이브러리 구조와 효율적인 렌더링 메커니즘은 모바일 환경에서의 성능 향상에 크게 기여했습니다.
- 단일 페이지 웹 앱의 등장 : SPA는 페이지 새로고침 없이 동적으로 콘텐츠를 업데이트할 수 있게 해주었습니다. React의 선언적 UI 구현 방식과 컴포넌트 기반 구조는 SPA 개발을 용이하게 만들었습니다.
React가 이끈 CSR 렌더링
- Ajax의 진화 : XMLHttpRequest와 FetchAPI의 발전으로, 비동기 데이터 통신이 가능해졌습니다. 이에 따라서 페이지의 전체 새로고침 없이도 필요한 부분만을 API 요청을 통해 해당 부분만 업데이트가 가능해졌는데요. React는 컴포넌트 생명주기 메서드와 훅을 통해 효율적인 데이터 fetching 상태 관리를 구현했습니다.
- 사용자 경험 향상 : CSR은 단일 페이지에서 필요한 부분만 업데이트하여 네이티브 앱과 유사한 UX를 제공합니다. React의 가상 DOM과 재조정(Reconciliation) 알고리즘은 이러한 부분 업데이트를 매우 효율적으로 수행합니다.
- 프론트엔드와 백엔드의 분리 : 이 시점부터는 API 기반를 통신으로 프론트엔드와 백엔드 개발의 독립성이 높아졌습니다. React는 이러한 분리를 더욱 촉진하여 백엔드 API와의 통합을 위한 다양한 패턴과 라이브러리(Redux, React Query)가 발전되었습니다.
SPA는 시대적 흐름의 결과(복잡한 웹앱, 모바일기기 보편화) 에 의해 탄생되었고, CSR은 기술적 발전의 산물(AJAX) 입니다. React는 이런 시기에 SPA와 CSR의 요구를 충족시키는 적합한 솔루션을 제공했습니다. 컴포넌트 기반의 아키텍처와 가상 DOM을 통한 효율적인 렌더링 그리고 선언적 UI 프로그래밍 모델로 웹 개발의 새로운 패러다임을 제시했습니다. 또한 이 시기에 오픈소스 커뮤니티가 폭발적으로 큰 관심을 받게 되면서 의도적, 우연적 요소가 맞아 떨어지며 React를 통한 웹개발의 흐름이 탄생하게 되었습니다.
React가 이끄는 SSR: CSR 한계 극복하기
CSR이 웹 개발의 패러다임을 변화시켰지만, 시간이 지나면서 CSR만의 한계점들도 드러나기 시작했습니다. React는 이런 문제를 해결하기 위해 SSR(Server-Side Rendering)을 지원하기 시작했는데요. 주목할 만한 점은 단순 SSR이 아닌, CSR과 SSR의 장점을 모두 활용할 수 있는 하이브리드 렌더링 방식을 가능하게 했다는 것입니다. 페이지의 주요한 부분은 SSR을 통해 빠른 초기 로딩과 SEO를 제공하고, 나머지 부분은 CSR을 통해 동적이면서 뛰어난 사용자 경험을 유지할 수 있게 해주었습니다. 이때 Next.js 프레임워크는 이런 React SSR 기능을 더욱 쉽고 효율적으로 구현할 수 있게 해주었습니다. 이런 유연성은 복잡한 웹 애플리케이션의 다양한 요구사항을 충족시키는데 큰 역할을 하게 되었습니다.
React 에서 CSR과 SSR이 동시에 가능한 이유: Hydration
- Hydration은 SSR과 CSR을 결합하는 React의 메커니즘입니다. 이 과정을 통해 서버에서 생성된 정적 HTML을 클라이언트에서 완전히 상호작용이 가능한 완전한 React 애플리케이션으로 ‘되살리는’ 것인데요.
- SSR을 통해 서버에서 생성된 초기 HTML이 클라이언트에 전달되고 브라우저는 즉시 화면에 렌더링 합니다. 이 시점에서는 페이지는 보이지만 상호작용은 불가능한데요. 이후 ReactDOM의 hydrateRoot() 메서드 를 통해 렌더링된 HTML과 가상 DOM을 비교하여 이벤트 리스너를 연결합니다. 이때 정적인 HTML이 동적인 React 애플리케이션이 됩니다.
Hydration의 개념과 오해
- Hydration 에러가 발생하는 이유 : 이유는 2번 렌더링 하기 때문입니다. 서버에서 생성된 HTML은 브라우저 에서 즉시 렌더링 됩니다. 이 시점에서는 Hydration 에러가 발생하지 않습니다. 왜냐하면 아직 React가 클라이언트에서 실행되지 않았기 때문이죠. 이후에 ReactDOM.hydrateRoot() 메서드를 통해 렌더링된 HTML과 클라이언트 가상 DOM을 비교하게 되고, 이 과정에서 렌더링 결과가 일치하지 않을때 Hydration 에러가 발생합니다.
- 왜 2번 렌더링 하는가? : 2 번의 렌더링은 비효율적인것 같아 보이는데요. 두 번 렌더링을 하게 되면 최초 초기 로딩 시 최소한의 정보만을 포함하고 있어 빠르게 렌더링이 가능합니다. 또한 검색엔진은 빠른 렌더링을 중요한 랭킹 요소로 고려하므로 SEO에도 유리 합니다. 초기 렌더링 이후에는 DOM 요소에 속성을 매칭 시키는 작업을 하는데요. 이 이벤트 리스너를 붙이는 작업은 페이지를 다시 그리지는 않으므로 어떤 추가적인 큰 비용이 발생하지는 않습니다. 따라서, Hydration은 빠른 초기 로딩과 상호작용을 모두 제공하기 위한 React의 렌더링 전략입니다.
- Hydration은 Next의 기능이다? : 많은 개발자들이 Hydration을 Next.js의 고유 기능으로 오해하곤 합니다. 이는 Next.js가 React 기반의 SSR을 쉽게 구현할 수 있게 해주는 인기 있는 프레임워크이기 때문일 것입니다. 그러나 Hydration은 ReactDOM 에서 제공하는 hydrateRoot() 메서드의 기능입니다. React의 SSR은 복잡한 과정을 포함하고 적절한 타이밍에 hydrateRoot를 호출해야 합니다. Next.js는 이런 복잡성을 추상화하여 개발자 경험을 향상시키고 hydrateRoot호출 타이밍과 방식을 조정하여 성능을 최적화 합니다. 따라서 개발자는 복잡한 SSR을 직접 구현하는 대신 애플리케이션 로직에 집중하여 효율적으로 SSR 렌더링 애플리케이션을 구축할 수 있습니다.
React에서 SSR과 CSR의 통합은 기존 CSR의 단점을 보완하는 진화한 렌더링 전략입니다. 그리고 이 기술적 구현이 가능한 이유는 바로 Hydration이며 이를 통해 빠른 초기 로딩과 CSR의 풍부한 상호작용이 결합되었습니다. 이는 웹 개발에서 '최적의 사용자 경험'을 제공하기 위한 노력의 결과이며, CSR과 SSR의 장점을 결합한 하이브리드 접근 방식이 주요 트렌드로 자리 잡게 되었습니다. 따라서 SSR의 재등장은 웹의 다양한 요구사항을 충족시키기 위한 React의 유연성과 강력함을 보여주는 진화된 형태의 렌더링 전략으로 볼 수 있습니다.
React의 서버 컴포넌트(RSC): 렌더링의 새로운 패러다임
SSR은 CSR의 여러 한계점을 해결했지만, 여전히 최적화와 개발 복잡성 측면에서 과제들이 존재했습니다. 특히 데이터 fetching 최적화 서버 리소스에 대한 효율적 접근, 그리고 React 개발자들이 빈번히 직면하는 하이드레이션 문제 등이 주요 이슈로 대두되었습니다.
기존 SSR 방식에서도 이러한 문제들을 해결할 수 있었으나, 그 과정에서 상당한 수준의 복잡한 커스텀 로직이 요구되어 개발효율성이 저하되는 문제가 있었습니다. 이런 배경에서 React Server Component(RSC)가 등장하게 되었습니다.
1. RSC 의 기술적 특성
- 기본 동작 원리 : 컴포넌트가 서버에 있으면 데이터를 미리 가져오고 처리하여 클라이언트에 전달 합니다. 이때 가상DOM과 유사한 형태로 렌더링되어 클라이언트에 전달됩니다. 이러한 방식으로 인해 클라이언트는 서버에서 받은 렌더링 결과를 기반으로 UI를 구성합니다.
- React18의 RSC 지원 : React18 부터는 RSC 지원을 위해 ‘react/server’ API를 도입하였습니다. 이 API는 서버에서 React 컴포넌트를 실행하고 렌더링을 가능하게 하고 비동기 컴포넌트를 자연스럽게 처리할 수 있게 해줍니다.
- React19 의 ‘use’훅과 RSC 통합 : use 훅은 RSC를 염두에 두고 설계되었다고 볼 수 있습니다. RSC가 도입되면서 서버에서 실행되는 데이터 fetching 패턴에 대한 접근이 필요했고 ‘use’훅은 이러한 요구사항을 충족시키는 방향으로 설계되었습니다. RSC와 Suspense는 React 팀이 추구하는 방향으로 ‘use’훅은 두 기능을 자연스레 연결하는 역할을 합니다. 따라서 개발자는 클라이언트 컴포넌트와 서버 컴포넌트 컴포넌트의 위치(서버 또는 클라이언트)에 관계없이 동일한 패턴으로 데이터를 다룰 수 있습니다.
<code class="language-javascript">// 서버 컴포넌트 async function ServerComponent({ id }) { const data = use(fetchDataOnServer(id)); return <div>{data}</div>; } // 클라이언트 컴포넌트 function ClientComponent({ id }) { const data = use(fetchDataOnClient(id)); return <div>{data}</div>; }
2. RSC 의 장점
- 자유로운 서버 리소스 직접 접근과 빠른 속도: 서버에서 실행되기 때문에, 데이터베이스나 파일 시스템과 같은 서버 리소스에 직접 접근할 수 있습니다. 이는 속도와 보안적인 측면 에서 큰 이점을 제공하는데요. 프론트에서 API를 호출하는 것보다, 서버에서 직접 DB를 호출하는것이 브라우저를 거치지 않고, 동일 인프라 내에서는 실행되기 때문에 접근 속도가 더욱 빠릅니다.
<code class="language-javascript">import { db } from './database'; async function UserList() { const users = await db.query('SELECT * FROM users'); return ( <ul> {users.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); }
- 번들 크기 감소 : RSC 컴포넌트의 자바스크립트 코드는 웹팩 같은 번들러 에서 자동으로 제외 됩니다. 이 코드는 서버에서 실행되므로 클라이언트에서 실행될 필요가 없기 때문인데요. ‘server.js’ 확장자를 가진 파일들이 자동으로 서버 컴포넌트로 인식되어 번들 대상에서 자동으로 제외됩니다. 이는 “Zero-Bundle-Size Components” 개념인데요. 따라서 클라이언트 자바스크립트 번들 사이즈가 감소하고 브라우저 개발자 도구 Source 탭에서 해당 코드를 볼 수 없습니다. 또한 console.log 는 브라우저가 아닌 서버에서 실행되므로, 개발 서버를 실행한 터미널에서 로그확인이 가능합니다.
- 개발 경험의 통합 : 개발자는 서버와 클라이언트 컴포넌트를 거의 동일한 방식으로 작성할 수 있습니다. React의 선언적 컴포넌트 프로그래밍 모델을 서버 사이드 로직까지 적용된 것인데요. 개발자는 비즈니스 로직에 따라 자유롭게 컴포넌트를 구성하기 때문에 학습 곡선이 감소하고, 코드의 재사용성도 증가합니다. 클라이언트, 서버컴포넌트는 자연스럽게 통합, 구현할 수 있기 때문에 React개발을 더욱 직관적이고 효율적으로 할 수 있습니다.
3. RSC 의 오해와 진실
- RSC는 SSR을 대체할까요? : React Server Components(RSC)는 서버 사이드 렌더링(SSR)을 대체할 수 없습니다. RSC는 CSR, SSR같은 별도의 렌더링 전략방식 이라기 보다는, SSR을 보완하고 확장하는 개념 이기 때문입니다. RSC는 기본적으로 SSR을 기반으로 서버 로직을 React 컴포넌트 에서 직접 작성할 수 있는 개념입니다. RSC는 서버 렌더링의 이점을 유지하면서도 개발자가 더 쉽고 효율적으로 서버-클라이언트 로직을 통합하여 SSR을 더 강력하고 유연하게 만드는 도구라고 볼 수 있습니다.
- RSC의 데이터 구조 : RSC는 기본적으로 가상DOM구조를 기반으로 합니다. 컴포넌트 트리를 표현하는 자바스크립트 객체 구조 인데요. 가상 DOM과의 차이점은 RSC는 서버에서 생성되다보니, 네트워크 전송을 위해 최적화된 형식을 사용합니다. 또한 서버 컴포넌트와 클라이언트 컴포넌트를 구분하여 참조합니다. 이 데이터는 청크 기반으로 여러 청크로 나뉘어 전송되며, 대괄호로 둘러싸인 배열형태로 각 청크 타입을 포함합니다. React는 이 스트림을 해석하여 UI를 구성합니다.
<code class="language-javascript">M1:[ ["1", "div", null, {"children": ["2", "Hello, World!"]}], ["5", "ServerComponent", {"id": "123"}, "0"], ["6", "ClientComponent", {"onClick": "1"}, "1"], "1", {"id": "button_1"} ]
RSC는 기존 SSR의 한계를 극복하고 서버와 클라이언트의 장점을 모두 활용할 수 있는 더 효율적인 렌더링과 데이터 처리를 가능하게 하는 접근 방식을 제공합니다. 따라서 최적의 웹 애플리케이션 성능과 사용자 경험을 위해서는 SSR기반 위에서 RSC를 적절히 조합하여 사용하는 것이 권장됩니다. 이러한 접근 방식은 초기 로딩 성능, SEO, 그리고 풍부한 상호작용성을 모두 고려한 균형 잡힌 웹 애플리케이션 개발을 가능하게 합니다
결론
“State of React 2023” 보고서에 따르면, 2028년경에는 RSC를 포함한 ‘풀스택’ 버전과, 기존 클라이언트 중심 단일 페이지 애플리케이션인 SPA 의 두 가지 주요 패러다임이 공존할 것으로 예상된다고 합니다. 이처럼 React 는 다양한 use case에 대응할 수 있는 유연성을 유지하면서도, 서버 사이드 기능을 강화하는 방향으로 발전할 것임을 시사합니다.
따라서 React 개발자는 이러한 변화를 이해하고, 각 프로젝트의 고유한 요구사항에 가장 적합한 렌더링 전략을 선택하는 것이 중요합니다. CSR, SSR, RSC모두 각각의 장단점을 지니고 있기 때문에 각 렌더링 전략의 장단점을 깊이 이해하고 프로젝트의 특성에 맞는 최적의 솔루션을 선택할 수 있는 전략적 사고가 요구됩니다. 이를 통해, 각 렌더링 방식간의 트레이드 오프를 신중히 고려하여 비즈니스 목표 달성과 함께 최상의 사용자 경험 제공이라는 두 가지 목표를 동시에 달성할 수 있습니다.
이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.