리액트 서버 컴포넌트 이해하기
원문: Alice Alexandra Moore, "Understanding React Server Components"
리액트 서버 컴포넌트(RSC)는 리액트의 기본 기능을 순수 렌더링 라이브러리를 넘어 데이터 가져오기 및 원격 클라이언트-서버 통신을 프레임워크 내에서 통합하는 방식으로 확장합니다.
이제부터 RSC가 왜 만들어졌는지, RSC가 무엇을 가장 잘하는지, 그리고 언제 사용해야 하는지에 대해 알아볼 것입니다. 또한 Next.js가 App Router를 통해 RSC 구현 세부 사항을 어떻게 용이하게 하고 향상하는지도 다룰 것입니다.
서버 컴포넌트가 왜 필요할까요?
리액트 이전의 생태계를 살펴봅시다. PHP 같은 언어를 사용할 때는 클라이언트와 서버의 관계를 아주 강하게 결합했습니다. 모놀리식 아키텍처에서는 만들고 있는 페이지 내에서 서버에 데이터를 요청할 수 있었습니다. 하지만 단점도 있었습니다. 특히 모놀리식 애플리케이션을 확장하는 데 어려움이 있었는데, 이는 팀 간 종속성 문제와 높은 트래픽 수요 때문이었습니다.
리액트는 기존 코드 베이스에 점진적으로 적용되고 구성될 수 있도록 만들어졌습니다. 풍부한 상호작용을 갈망하는 세상에 대응하여 클라이언트와 서버의 관심사를 분리함으로써 프런트엔드가 훨씬 더 유연하게 구성될 수 있었습니다. 이는 특히 팀에게 중요했습니다. 서로 다른 개발자가 만든 두 개의 리액트 컴포넌트가 같은 프레임워크 내에서 작동하기 때문에 함께 잘 동작할 수 있었습니다.
이를 달성하기 위해 리액트는 기존 웹 표준 위에 혁신을 더해야 했습니다. 지난 10년 동안 다중 페이지 애플리케이션(MPA)과 단일 페이지 애플리케이션(SPA), 클라이언트 사이드 렌더링과 서버 사이드 렌더링의 진화를 거치면서 빠른 데이터 제공, 풍부한 상호작용 제공, 훌륭한 개발자 경험 유지라는 목표는 동일하게 유지되었습니다.
서버 사이드 렌더링과 리액트 Suspense가 해결한 문제는 무엇일까요?
지금의 서버 컴포넌트가 있기까지 해결해야 했던 다른 문제들이 있었습니다. RSC의 필요성을 이해하기 위해서는 먼저 서버 사이드 렌더링(SSR)과 Suspense의 필요성을 이해하는 것이 도움이 됩니다.
SSR은 초기 페이지 로드에 집중하여 클라이언트로 사전 렌더링된 HTML을 전송합니다. HTML은 클라이언트에서 다운로드한 자바스크립트로 hydrate 되어 일반적인 리액트 앱처럼 동작하기 전까지 기다려야 합니다. 또한 SSR은 페이지를 직접 탐색할 때 한 번만 발생합니다.
SSR만으로는 사용자는 HTML을 더 빨리 받지만 자바스크립트와 상호작용 하기 전에 "전부 보이거나 아무것도 보이지 않거나"하는 워터폴에서 기다려야 합니다.
모든 데이터는 서버에서 가져오기 전에는 보여줄 수 없습니다.
모든 자바스크립트를 서버에서 다운로드해야 클라이언트가 hydrate 할 수 있습니다.
모든 hydration이 클라이언트에서 완료되기 전까지는 아무것도 상호작용 할 수 없습니다.
이 문제를 해결하기 위해 리액트는 Suspense를 만들었습니다. Suspense를 사용하면 서버 사이드 HTML 스트리밍과 클라이언트의 선택적 hydration이 가능해집니다. 컴포넌트를 <Suspense>
로 감싸면 서버에게 해당 컴포넌트의 렌더링과 hydration의 우선순위를 낮추라고 알릴 수 있어, 더 무거운 컴포넌트에 막힘 없이 다른 컴포넌트가 로딩될 수 있습니다.
여러 컴포넌트를 <Suspense>
로 감싸면 리액트는 작성한 순서대로 트리를 따라 애플리케이션을 최적으로 스트리밍 할 수 있습니다. 그러나 사용자가 특정 컴포넌트와 상호작용을 시도하면 해당 컴포넌트가 다른 컴포넌트보다 우선순위를 가지게 됩니다.
이는 상황을 크게 개선하지만 여전히 몇 가지 문제가 남아있습니다.
전체 페이지의 모든 데이터는 컴포넌트를 보여주기 전에 서버에서 가져와야 합니다. 이를 우회하는 유일한 방법은
useEffect()
훅으로 클라이언트 사이드에서 데이터를 가져오는 것인데, 이는 서버 사이드에서의 데이터 요청보다 왕복 시간이 길고 컴포넌트가 렌더링되고 hydrate 된 후에만 발생합니다.모든 페이지의 자바스크립트는 브라우저로 비동기 스트리밍이 되더라도 결국 다운로드가 되어야 합니다. 애플리케이션 복잡성이 증가할수록 사용자가 다운로드하는 코드의 양이 늘어납니다.
hydration을 최적화했음에도 불구하고 사용자는 클라이언트 사이드 자바스크립트를 다운로드하고 컴포넌트가 구현될 때까지 해당 컴포넌트와 상호작용 할 수 없습니다.
자바스크립트 연산의 대부분은 결국 클라이언트에서 발생하며 다양한 기기에서 실행될 수 있습니다. 클라이언트보다 더 강력하고 예측 가능한 서버로 자바스크립트 연산을 옮기는 것은 어떨까요?
리액트 서버 컴포넌트가 없는 Next.js에서 데이터 가져오기는 추가적인 API 계층이 필요합니다.
웹 표준이 자바스크립트 프레임워크의 한계를 따라잡으면서 또 한 번의 도약이 필요한 시점입니다. 여기 더 빠른 애플리케이션을 구성할 수 있는 더 좋은 방법이 있습니다.
리액트 서버 컴포넌트가 하는 일이 무엇일까요?
리액트 서버 컴포넌트는 위에 언급된 문제들을 해결하기 위해 만들어졌습니다. RSC는 개별적으로 데이터를 가져오고 서버에서 전적으로 렌더링합니다. 그 결과 HTML을 클라이언트 사이드 리액트 컴포넌트 트리에 스트리밍하여 필요에 따라 다른 서버 컴포넌트와 클라이언트 컴포넌트 사이에 교차 배치됩니다.
이 과정은 클라이언트 사이드에서 리렌더링을 할 필요가 없어져 성능이 향상됩니다. 클라이언트 컴포넌트의 경우 hydration이 RSC 스트리밍과 동시에 발생할 수 있으며 클라이언트와 서버 사이에서 계산 부하가 공유되기 때문에 가능합니다.
다시 말해 훨씬 더 강력하고 물리적으로 데이터 소스에 가까운 서버가 계산 집약적인 렌더링을 처리하고 상호작용이 있는 코드만 클라이언트에 전달합니다.
상태 변경으로 인해 RSC를 다시 렌더링해야 하는 경우 서버에서 새로 고침을 하고 강력 새로고침 없이 기존 DOM에 원활하게 병합합니다. 따라서 뷰의 일부가 서버에서 업데이트되더라도 클라이언트 상태는 유지됩니다.
RSC의 성능과 번들 크기
RSC는 클라이언트 사이드 자바스크립트 번들의 크기를 줄이고 로딩 성능을 개선하는 데 도움이 됩니다.
일반적으로 애플리케이션을 탐색하는 동안 클라이언트는 모든 코드와 데이터 종속성을 다운로드한 다음 실행합니다. 코드 분할 기능이 있는 리액트 프레임워크가 없다면 현재 페이지에서 사용자에게 필요하지 않은 페이지의 코드까지 전송될 수 있습니다.
하지만 RSC는 앱 데이터 소스에 더 가까운 서버에서 모든 종속성을 해결합니다. 또한 서버에서만 코드를 렌더링하므로 (모바일 기기와 같은) 클라이언트 기기보다 훨씬 빠릅니다. 그런 다음 리액트는 처리된 결과물과 클라이언트 컴포넌트만 브라우저로 전송합니다.
RSC는 클라이언트 사이드 자바스크립트 페이로드를 급격히 줄입니다.
다시 말해 서버 컴포넌트를 사용하면 초기 페이지 로드가 더 빠르고 가벼워집니다. 기본 클라이언트 사이드 런타임은 캐시가 가능하고 크기가 예측 가능하며 애플리케이션이 성장해도 증가하지 않습니다. 사용자가 직면하는 자바스크립트는 주로 클라이언트 컴포넌트를 통해 애플리케이션이 더 많은 클라이언트 사이드 상호작용이 필요할 때 추가됩니다.
RSC 끼워 넣기 및 Suspense 통합
RSC는 클라이언트 사이드 코드와 완전히 교차되어 클라이언트 컴포넌트와 서버 컴포넌트가 동일한 리액트 트리에서 렌더링될 수 있습니다. 애플리케이션 코드의 대부분을 서버로 이동시킴으로써 RSC는 클라이언트 사이드 데이터 불러오기 워터폴을 방지하고 데이터 종속성을 서버 사이드에서 빠르게 해결할 수 있도록 돕습니다.
전통적인 클라이언트 사이드 렌더링에서는 컴포넌트가 비동기 작업이 완료될 때까지 렌더링 과정을 "일시 중지"하고 (대체 상태를 보여주기 위해) 리액트 Suspense를 사용합니다. RSC를 사용하면 데이터 가져오기와 렌더링이 모두 서버에서 발생하므로 Suspense가 서버 사이드에서도 대기 시간을 관리하여 대체 상태와 완료된 페이지를 더 빠르게 렌더링 할 수 있도록 왕복 시간을 단축합니다.
중요한 점은 클라이언트 컴포넌트는 초기 로드 시 여전히 SSR로 처리된다는 것입니다. RSC 모델은 SSR이나 Suspense를 대체하는 것이 아니라 이들과 함께 작동하여 애플리케이션의 모든 부분을 사용자가 필요로 할 때 제공하는 것입니다.
Next.js에서 리액트 서버 컴포넌트를 사용하면 동일한 컴포넌트에서 데이터 가져오기와 UI 렌더링을 수행할 수 있습니다. 또한 서버 액션은 자바스크립트가 페이지에 로딩되기 전에 사용자가 서버 사이드 데이터와 상호작용할 수 있는 방법을 제공합니다.
RSC의 한계
서버 컴포넌트로 작성된 모든 코드는 직렬화가 가능해야 하므로 useEffect()
나 상태와 같은 라이프사이클 훅을 사용할 수 없습니다.
하지만 서버 액션을 통해 클라이언트에서 서버와 상호작용할 수 있습니다. 이에 대해서는 아래에서 다룰 예정입니다.
또한 RSC는 웹소켓을 통한 지속적인 업데이트는 지원하지 않습니다. 이러한 경우에는 클라이언트 사이드에서 데이터를 가져오거나 폴링하는 접근 방식이 필요합니다.
Vercel의 시니어 개발자 대표인 Delba de Oliveira가 리액트 코어 팀의 Andrew Clark와 Sebastian Markbåge와 함께 리액트와 서버 컴포넌트 등에 대해 논의합니다.
리액트 서버 컴포넌트를 사용하는 방법
RSC의 장점은 작동 원리를 완전히 알지 못해도 이를 활용할 수 있다는 것입니다. Next.js 13.4에 도입된 App Router에서는 가장 완전한 기능 구현을 제공하며 모든 컴포넌트가 기본적으로 서버 컴포넌트입니다.
useEffect()
나 상태와 같은 라이프사이클 이벤트를 사용하려면 클라이언트 컴포넌트를 추가해야 합니다. 클라이언트 컴포넌트를 선택하려면 컴포넌트 상단에 "use client"를 작성하면 됩니다. 더 자세한 조언을 원한다면 문서를 참조하는 것이 좋습니다.
서버 컴포넌트와 클라이언트 컴포넌트 균형 맞추기
RSC는 클라이언트 컴포넌트를 대체하기 위한 것이 아니라는 점에 유의해야 합니다. 정상적인 애플리케이션은 동적 데이터 가져오기를 위해 RSC를 활용하고 풍부한 상호작용을 위해 클라이언트 컴포넌트를 사용합니다. 문제는 각 컴포넌트를 언제 사용할지 결정하는 것입니다.
개발자는 서버 사이드 렌더링과 데이터 불러오기에는 RSC를 활용하고 상호작용 기능과 사용자 경험에는 클라이언트 컴포넌트를 활용하는 것을 고려하세요. 적절한 균형을 맞추면 고성능의 효율적이고 매력적인 애플리케이션을 만들 수 있습니다.
가장 중요한 것은 느린 컴퓨터, 느린 휴대폰, 느린 와이파이 등 애플리케이션을 비표준 환경에서 계속 테스트하는 것입니다. 올바른 컴포넌트 조합으로 애플리케이션이 얼마나 더 잘 작동하는지 놀랄 수도 있습니다.
RSC는 사용자에게 과도한 클라이언트 사이드 자바스크립트로 부담을 주는 문제에 대한 완전한 해결책은 아니지만 사용자 기기에 계산 부담을 싣는 시점을 선택할 힘을 확실히 제공합니다.
Next.js로 개선된 데이터 가져오기
RSC는 서버에서 데이터를 가져와 백엔드 데이터에 안전하게 접근할 수 있을 뿐만 아니라 서버와 클라이언트의 상호작용을 줄여 성능을 향상합니다. Next.js의 향상된 기능과 결합하면 RSC는 스마트 데이터 캐싱, 한 번의 왕복에서 여러 번의 데이터 가져오기, 자동 fetch()
요청 중복 제거를 가능하게 하여 데이터를 클라이언트 사이드로 보내는 효율성을 최대화합니다.
아마도 가장 중요한 것은 서버에서 데이터를 가져오는 것이 클라이언트 사이드 데이터 가져오기 워터폴을 방지한다는 것입니다. 요청이 쌓이고 순차적으로 해결되어야 다음 단계를 진행할 수 있는 것이죠. 서버 사이드 가져오기는 이러한 문제를 해결합니다. 서버 사이드 가져오기는 클라이언트를 전체적으로 차단하지 않고 훨씬 더 빠르게 해결되기 때문에 오버헤드가 훨씬 적습니다.
또한 더 이상 Next.js의 getServerSideProps()
와 getStaticProps()
같은 페이지 수준의 특정 메서드가 필요하지 않습니다. 이러한 메서드는 개별 컴포넌트를 세밀하게 제어할 수 없고 데이터를 과도하게 가져오는 경향이 있었습니다. (사용자가 페이지를 탐색할 때 실제로 상호작용한 컴포넌트와 상관없이 모든 데이터를 가져왔습니다.)
Next.js App Router에서 가져온 모든 데이터는 기본적으로 정적이며 빌드 시점에 렌더링됩니다. 그러나 이는 쉽게 변경할 수 있습니다. Next.js는 fetch
옵션 객체를 확장하여 캐싱 및 자동 갱신에 유연성을 제공합니다.
{next: {revalidate: number}}
옵션을 사용하여 정적 데이터를 설정된 간격으로 또는 백엔드 변경이 발생할 때 새로 고칠 수 있으며 (Incremental Static Regeneration), {cache: 'no-store'}
옵션을 동적 데이터 (서버 사이드 렌더링)에 대한 가져오기 요청에 전달할 수 있습니다.
이 모든 것이 Next.js 앱 라우터 내의 리액트 서버 컴포넌트를 효율적이고 안전하며 동적인 데이터 불러오기를 위한 강력한 도구로 만들며 고성능 사용자 경험을 제공하기 위해 기본적으로 캐싱됩니다.
서버 액션: 가변성을 위한 리액트의 첫걸음
RSC의 맥락에서 서버 액션은 서버 사이드의 RSC에서 정의한 함수로 서버·클라이언트 경계를 넘어 전달할 수 있습니다. 사용자가 클라이언트 사이드에서 앱과 상호작용할 때 서버 액션을 직접 호출하여 서버 사이드에서 안전하게 실행할 수 있습니다.
이 접근 방식은 클라이언트와 서버 간의 원격 프로시저 호출(RPC) 경험을 매끄럽게 제공합니다. 별도의 API 경로를 작성하여 서버와 통신하는 대신 클라이언트 컴포넌트에서 서버 액션을 직접 호출할 수 있습니다.
또한 Next.js App Router는 스마트 데이터 캐싱, 재검증 및 Mutation을 중심으로 구축되어 있다는 점도 명심하세요. Next.js의 서버 액션을 통해 동일한 서버 왕복 요청에서 캐시를 변경하고 리액트 트리를 업데이트할 수 있으며 탐색을 통해 클라이언트 캐시의 무결성을 유지할 수 있습니다.
구체적으로 서버 액션은 데이터베이스 업데이트나 폼 제출과 같은 작업을 처리하도록 설계되었습니다. 예를 들어 서버 액션을 사용하면 자바스크립트가 아직 로딩되지 않은 경우에도 사용자가 폼과 상호작용할 수 있으며 서버 액션이 폼 데이터의 제출과 처리를 담당합니다.
서버 액션이 제공하는 기회는 점진적인 향상과 API 개발 작업을 줄이는 것 모두에 있어 접근성, 사용성, 개발자 경험에 크게 기여합니다.
무거운 작업은 Next.js에게 맡기세요
Next.js는 리액트 서버 컴포넌트, 서버 액션, Suspense, 트랜지션 등 리액트 아키텍처를 통합한 첫 번째 프레임워크입니다.
제품 개발에 집중하는 동안 Next.js는 전략적인 스트리밍과 스마트 캐싱을 활용하여 애플리케이션 렌더링이 블로킹되지 않고 최고 속도로 동적 데이터를 제공합니다.
Next.js는 안정성, 신뢰성, 이전 버전과의 호환성을 희생하지 않으면서도 새로운 리액트 기능의 최전선을 유지하기 위해 노력하고 있습니다. 앞으로도 팀이 빠르게 반복할 수 있는 똑똑한 기본 설정을 제공하면서도 프로젝트의 유연성과 확장 가능성을 유지할 것입니다.
이제 어디로 갈까요?
요약하자면 리액트 서버 컴포넌트는 컴포넌트 내에서 서버와 바로 상호작용할 수 있는 네이티브 방식을 제공하여 동적 데이터와 상호작용하는 코드와 인지적 부담을 줄입니다. 클라이언트 컴포넌트는 이전과 마찬가지로 완전히 기능적이고 사용 가능합니다. 이제는 각 컴포넌트를 언제 사용할지를 선택하는 것이 중요한 일입니다.
이 주제에 대해서 더 많은 안내가 필요하다면 편하게 Next.js 문서를 방문하세요. 추가적으로 App Router 플레이그라운드도 있으니 차이를 직접 경험해 보세요.
리액트 서버 컴포넌트에 관한 더 많은 글에 관심이 있다면 다음 글들이 유익할 것입니다.
팀의 애플리케이션을 App Router 및 리액트 서버 컴포넌트로 마이그레이션하는 데 직접적인 지원이 필요하시면 언제든지 문의해 주세요.
Subscribe to my newsletter
Read articles from 윤정민 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by