회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
본문은 요즘IT와 번역가 Chase가 함께 조쉬 코모(Josh Comeau)의 글 <Making Sense of React Server Components>을 번역한 글입니다. 필자인 조쉬 코모는 언스플래쉬, 칸 아카데미 등에서 일했고 현재는 웹사이트 Joshwcomeau.com를 운영하며 리액트, CSS, 애니메이션 등에 대한 다양한 아티클과 강의를 제공하고 있습니다. 이 글은 몇 달 전 리액트 팀이 공개한 리액트 서버 컴포넌트를 소개하고 있습니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
본문은 요즘IT와 번역가 Chase가 함께 조쉬 코모(Josh Comeau)의 글 <Making Sense of React Server Components>을 번역한 글입니다. 필자인 조쉬 코모는 언스플래쉬, 칸 아카데미 등에서 일했고 현재는 웹사이트 Joshwcomeau.com를 운영하며 리액트, CSS, 애니메이션 등에 대한 다양한 아티클과 강의를 제공하고 있습니다. 이 글은 몇 달 전 리액트 팀이 공개한 리액트 서버 컴포넌트를 소개하고 있습니다.
필자에게 허락을 받고 번역했으며, 글에 포함된 각주(*표시) 중 ‘번역자주’로 표시된 것 외의 주석은 모두 원문의 주석이며, 글에 포함된 링크 또한 원문에 따라 표시했습니다.
리액트가 올해로 벌써 출시 10주년을 맞이했다니 세월이 참 빠릅니다!
공개와 동시에 전 세계의 개발자 커뮤니티를 열광시킨 리액트는 지난 10년간 많은 변화를 거쳤습니다. 리액트 팀은 더 나은 문제 해결법을 찾으면 바로 적용해 왔으며, 때로는 급진적인 변화도 마다하지 않았습니다.
그리고 몇 달 전, 리액트 팀은 서버에서만 단독으로 실행되는 리액트 서버 컴포넌트(React Server Components)를 공개하며 또 한 번 패러다임을 전환했습니다.
이에 따라 온라인 커뮤니티에는 큰 파란이 일고 있습니다. 사람들은 리액트 서버 컴포넌트라는 게 대체 무엇인지, 어떻게 작동하는 건지, 어떤 장점이 있는지, 또 서버사이드 렌더링과는 어떻게 조화를 이루는지 등에 대해 많은 질문을 하고 있습니다.
저는 많은 실험을 통해 리액트 서버 컴포넌트에 대한 저의 궁금증을 해소했습니다. 솔직히 생각했던 것보다 훨씬 더 기대됩니다. 정말 멋지거든요!
그래서 이 글에서는 여러분의 이 패러다임에 대한 이해를 돕기 위해 리액트 서버 컴포넌트를 소개하려 합니다.
대상 독자이 튜토리얼 글은 리액트 경험이 있으며, 리액트 서버 컴포넌트에 대해 궁금해하는 개발자를 위해 작성되었습니다. 리액트 전문가만 이 글을 이해할 수 있는 건 아니지만 이제 막 발을 들인 개발자에게는 꽤 혼란스러울 수 있습니다. |
서버 사이드 렌더링(SSR, Server-side rendering)의 작동 원리를 이해하면 리액트 서버 컴포넌트가 무엇인지 감을 잡는 데 도움이 될 겁니다. 만약 여러분이 이미 SSR에 익숙하다면 다음 단락으로 건너뛰셔도 됩니다!
제가 리액트를 사용하기 시작한 2015년에는 리액트 설정의 대부분은 "클라이언트 사이드” 렌더링 전략을 사용했습니다. 그러면 사용자는 아래와 같은 HTML 파일을 다운로드하게 되겠죠.
이 Bundle.js 스크립트에는 리액트와 기타 서드파티 종속성(third-party dependencies), 그리고 개발자의 코드 등 애플리케이션을 마운트하고 실행하는 데 필요한 모든 것이 포함되어 있습니다.
자바스크립트의 다운로드와 파싱이 완료되면 즉각 리액트가 작동해서 모든 DOM(문서 객체 모델, Document Object Model)을 불러오고, 비어있는 <div id="root">에 저장합니다.
이 접근 방식에는 작업 수행에 시간이 걸린다는 것, 그리고 작업 진행 중에는 사용자에게 텅 빈 흰색 화면만 보인다는 문제가 있습니다. 더 큰 문제는 기능을 추가하면 자바스크립트 번들의 크기가 증가하기 때문에 작업 시간도 비례해서 늘어난다는 겁니다.
이런 부정적인 경험을 개선하기 위해 서버 사이드 렌더링이 등장했습니다. 이 전략에서 서버는 클라이언트 사이드 렌더링에서처럼 텅 빈 HTML 파일을 전송하는 대신, 직접 애플리케이션을 렌더링한 후 HTML을 생성해 냅니다. 결과적으로 사용자는 완전한 형식의 HTML 문서를 받게 되죠.
물론 리액트를 클라이언트 단에서도 실행해서 인터랙티브한 부분을 처리해야 하므로 HTML 파일에는 여전히 <script> 태그가 포함되기는 합니다. 하지만 리액트가 브라우저에서 동작하는 방식에는 차이가 있습니다. DOM 노드를 처음부터 하나하나 새로 만드는 대신 이미 만들어진 HTML을 차용하는데, 이 과정을 하이드레이션이라고 합니다.
저는 리액트 팀의 핵심 멤버인 댄 아브라모브(Dan Abramov)가 하이드레이션을 묘사하는 방식을 특히 좋아합니다.
“하이드레이션은 '건조한' HTML에 상호작용성(interactivity)과 이벤트 핸들러라는 '물'을 공급하는 것과 같습니다.”
JS 번들이 다운로드되면 리액트는 애플리케이션 전체를 빠르게 실행하여 가상의 UI 스케치를 만듭니다. 그 이후에는 스케치를 DOM에 맞추며, 이벤트 핸들러를 붙이고 이펙트를 실행하는 등의 작업을 수행합니다.
따라서 서버가 초기 HTML을 생성하기 때문에 자바스크립트 번들을 다운로드, 파싱하는 동안에도 사용자에게는 텅 빈 화면이 아니라 어느 정도 콘텐츠가 노출됩니다. 그런 다음 서버의 리액트가 중단한 부분을 클라이언트 측의 리액트가 이어받아 DOM을 적용하고 상호작용성을 추가합니다. 이것이 바로 서버사이드 렌더링입니다.
포괄적 용어(An umbrella term) 서버 사이드 렌더링에 관해 이야기할 때 일반적으로 다음과 같은 흐름을 상상하곤 합니다.
이는 서버 사이드 렌더링을 구현할 수 있는 한 가지 방법이지 유일한 방법은 아닙니다. 애플리케이션을 빌드할 때 HTML을 생성하는 선택지도 있습니다.
일반적으로 리액트 애플리케이션은 컴파일을 통해 JSX를 일반 자바스크립트로 변환하고 모듈을 번들링해야 합니다. 이때 동일한 프로세스를 태우되, 모든 경로의 HTML을 "미리 렌더링"해두면 어떨까요?
이러한 서버 사이드 렌더링의 하위 변형을 정적 사이트 생성(SSG, Static Site Generation)이라고 부릅니다.
"서버 사이드 렌더링"은 여러 가지 전략을 아우르는 포괄적인 용어입니다. 이 모든 전략에는 한 가지 공통점이 있는데, 최초 렌더링이 Node.js와 같은 서버 런타임에서 ReactDOMServer API의 사용을 통해 이루어진다는 점입니다. 정확히 어느 시점인지는 부차적인 요소입니다. 온디맨드이든 컴파일 타임이든 서버 사이드 렌더링인 건 똑같습니다. |
리액트가 데이터를 가져오는 것(data-fetching)에 관해 이야기해 보겠습니다. 일반적으로 네트워크를 통해 두 개의 애플리케이션이 통신합니다.
-> 클라이언트 사이드의 리액트 앱
-> 서버 사이드의 REST API
클라이언트는 React Query나 SWR, Apollo와 같은 것을 사용하여 백엔드에 네트워크 요청을 하고, 백엔드는 데이터베이스에서 데이터를 가져온 후에 네트워크를 통해 전송합니다. 그래프로 시각화하면 아래와 같습니다.
그래프에 대해 이 글에는 위와 같은 '네트워크 요청 그래프'가 몇 차례 등장할 예정입니다. 이 그래프는 여러 가지 렌더링 전략에 따라 데이터가 클라이언트(브라우저)와 서버(백엔드 API) 사이를 이동하는 방식을 시각화하도록 디자인되었습니다.
하단의 숫자는 분이나 초가 아닌 가상의 시간 단위를 나타냅니다. 실제로는 다양한 요인에 의해 수치가 크게 달라집니다. 이 그래프는 개념의 이해를 돕기 위한 것으로, 실제 데이터를 모델링한 것이 아닙니다. |
상기 그래프는 클라이언트 사이드 렌더링 전략의 흐름을 보여줍니다. 흐름은 클라이언트가 HTML 파일을 수신하는 것으로 시작됩니다. 이 파일에 콘텐츠는 없지만 하나 이상의 <script> 태그가 포함되어 있습니다.
자바스크립트가 다운로드, 파싱되면 리액트 앱이 부팅되어 여러 개의 DOM 노드를 생성하고 UI를 채웁니다. 하지만 지금 단계에서는 실제 데이터가 없으므로 로딩 상태의 셸(헤더와 푸터 등 일반 적인 레이아웃)만 렌더링할 수 있습니다.
여러분도 알게모르게 이런 패턴을 많이 보셨을 것입니다. 예를 들어 우버이츠(Uber Eats)의 웹사이트에 들어가 보면 예제처럼 셸을 먼저 렌더링한 후에 레스토랑 데이터를 채웁니다.
리액트가 컨텐츠 데이터 데이터를 뿌려줄 때까지 사용자는 이렇게 텅 빈 로딩 화면 밖에 볼 수 없습니다.
이번에는 다른 전략을 살펴보죠. 아래 그래프는 동일한 데이터 페칭 패턴이지만 클라이언트 대신 서버 사이드 렌더링을 사용한 흐름을 묘사합니다.
서버 사이드 렌더링에서는 서버가 첫 번째 렌더링을 수행합니다. 즉, 사용자가 처음 받는 HTML 파일은 텅 빈 화면이 아닙니다.
사용자에게는 비어있는 흰색 페이지가 아니라 셸 정도는 바로 보이기 때문에 사용자 경험상 클라이언트 사이드 렌더링보다는 낫습니다만, 궁극적인 문제를 해결하지는 못했습니다. 사용자는 로딩 화면이 아닌 콘텐츠(레스토랑, 호텔 목록, 검색 결과, 메시지 등)를 보기 위해 앱을 방문하니까요.
사용자 경험적 차이를 비교하기 위해 그래프에 몇 가지 웹 성능 메트릭을 추가해 보겠습니다. 두 그래프를 비교하며 플래그가 어떻게 다른지 살펴보세요.
각각의 플래그는 보편적으로 사용되는 웹 성능의 지표입니다. 자세한 설명은 아래와 같습니다:
서버가 렌더링을 먼저 수행하면 최초의 '쉘(shell)'을 더 빠르게 그릴 수 있습니다. 이렇게 하면 사용자는 로딩 속도가 상대적으로 조금 더 빠르다고 느낄 수 있으며, 사이트가 정상적으로 동작하고 있다는 인상을 받을 수 있습니다.
그리고 이것은 특정 상황에서는 중요한 개선 포인트가 될 수도 있습니다. 어떤 사용자는 내비게이션 링크를 클릭하기 위해 헤더의 로딩만 기다리는 경우가 있을지도 모르죠.
하지만 이 흐름이 조금 어색하게 느껴지지 않나요? SSR 그래프를 보면 요청(request)이 서버에서 시작된다는 것을 알 수 있습니다. 만약 네트워크를 오가야 하는 두 번째 요청 없이 첫 번째 요청 중에 데이터베이스 작업을 수행하면 어떤 일이 생길까요?
이렇게 하면 클라이언트와 서버를 왔다 갔다 하는 대신에 완전히 채워진 UI를 사용자에게 바로 전송할 수 있습니다. 데이터베이스 쿼리를 최초 요청의 일부로서 수행하기 때문이죠.
이 방법이 동작하려면 서버에서만 실행되는 코드를 리액트에 전달해서 데이터베이스 쿼리 작업을 할 수 있어야만 합니다. 이전의 리액트에서는 불가능한 작업이었죠. 서버 사이드 렌더링을 하더라도 모든 컴포넌트는 서버와 클라이언트 양쪽에서 렌더링해야 했습니다.
리액트 생태계에서는 이 문제에 대한 여러 해결책이 제시되어 왔습니다. 아예 Next.js나 Gatsby와 같은 메타 프레임워크(meta-frameworks)*는 서버에서만 코드를 실행하는 자체적인 방법을 만들어버렸습니다.
*"메타 프레임워크"는 라우팅이나 데이터 관리와 같은 추가 기능을 추가하여 React를 기반으로 구축되는 프레임워크입니다.
예를 들어, 레거시 "페이지" 라우터를 사용한 Next.js 코드를 봐주세요.
서버가 요청 받으면 getServerSideProps 함수가 호출되어 props 객체를 리턴합니다. 그런 다음 해당 프로퍼티는 컴포넌트로 전달되어 서버에서 먼저 렌더링되고, 클라이언트에서 하이드레이션됩니다.
여기서 영리한 점은 getServerSideProps가 클라이언트에서 다시 실행되지 않는다는 것입니다. 사실 이 함수는 자바스크립트 번들에도 포함되지도 않습니다!
이 접근 방식은 시대를 한참이나 앞선 정말 대단한 접근 방식입니다. 하지만 여기에는 몇 가지 단점이 있습니다:
리액트 팀은 이 문제를 공식적으로 해결할 방법을 찾기 위해 조용히 노력해 왔고, 그들이 내놓은 해결책이 바로 리액트 서버 컴포넌트입니다.
요약하자면 리액트 서버 컴포넌트는 완전히 새로운 패러다임입니다. 새 패러다임에서 개발자들은 오직 서버에서만 실행되는 컴포넌트를 생성할 수 있습니다. 또, 리액트 컴포넌트 내에서 바로 데이터베이스 쿼리를 작성하는 것과 같은 작업을 할 수 있습니다!
"서버 컴포넌트"의 간단한 예시를 살펴보겠습니다.
사실 몇 년 동안 리액트를 사용해 온 제가 이 코드를 처음 보았을 때는 정말 엉망이라고 생각했습니다.
본능적으로 이런 생각이 들더군요 "함수 컴포넌트 안에 직접적으로 ‘async’ 키워드를 사용할 수 없어! 더군다나 그런 식으로 렌더 함수 내에서 사이드 이펙트(side effect)를 직접 사용하면 안 되잖아!”
여기서 이해해야 할 핵심은 서버 컴포넌트를 다시 렌더링하지 않아도 된다는 겁니다. 서버에서 한번 렌더링 된 값은 클라이언트로 전송되어 제자리에 고정되기 때문에 라우터 레벨의 변경이 없다는 전제하에(다른 페이지로 이동하는 등) 절대 변경되지 않습니다.*
*적어도 라우터 수준에서 새 페이지로 이동하는 것과 같은 일이 발생할 때까지는 그렇지 않습니다.
이는 리액트 API의 상당 부분이 서버 컴포넌트와 호환되지 않는다는 말이기도 합니다. 예를 들어 상황에 따라 변경되어야 하는 상태(state)와 클라이언트에서 렌더링 된 후에 실행되는 이펙트는 더 이상 사용할 수 없겠죠.
물론 덜 신경 써도 되는 것들도 있습니다. 예를 들어 기존 리액트에서는 사이드 이펙트가 매 렌더링마다 반복되지 않도록 useEffect 콜백이나 이벤트 핸들러 같은 것들에 넣어야 했는데, 컴포넌트가 한 번만 실행되는 패러다임에서는 이런 걱정을 할 필요가 없습니다!
서버 컴포넌트 자체는 생각보다 간단하지만, "리액트 서버 컴포넌트" 패러다임은 훨씬 더 복잡합니다. 왜냐하면 우리는 여전히 일반적인 컴포넌트를 사용하고 있고, 그 둘을 서로 맞추는 방법이 꽤 헷갈릴 수 있기 때문입니다.
새로운 패러다임에서는 "전통적인" 리액트 컴포넌트를 클라이언트 컴포넌트라고 부릅니다. 저는 솔직히 이 이름이 마음에 들지 않습니다.
"클라이언트 컴포넌트"라는 이름은 이러한 컴포넌트가 클라이언트에서만 렌더링 된다는 것을 암시하지만, 실제로는 클라이언트, 서버 양측 모두에서 렌더링 되기 때문입니다.
새로운 용어가 헷갈릴 수 있으니 한번 정리하겠습니다.
리액트 서버 컴포넌트 vs 서버 사이드 렌더링 헷갈릴 만한 개념을 하나 더 정리하겠습니다: 리액트 서버 컴포넌트는 서버 사이드 렌더링을 대체하지 않습니다. 리액트 서버 컴포넌트를 "SSR의 버전 2.0"으로 생각하면 안 됩니다.
대신 서로 완벽하게 맞아떨어지는 두 개의 퍼즐 조각, 혹은 서로를 보완하는 두 가지 맛이라고 생각하시면 편합니다.
초기 HTML을 생성하기 위해서 여전히 서버 사이드 렌더링에 의존해야 합니다. 다만 리액트 서버 컴포넌트는 특정 컴포넌트가 클라이언트 쪽 자바스크립트 번들에 포함되지 않게, 즉 서버에서만 실행되도록 할 수 있습니다.
사실 서버 사이드 렌더링 없이 리액트 서버 컴포넌트를 사용하는 것도 가능하지만, 함께 사용하면 더 나은 결과를 얻을 수 있습니다. 예시를 보고 싶으시다면 리액트 팀에서 만든 SSR이 없는 최소한의 RSC 데모를 확인해주세요. |
보통 새로운 리액트 기능이 나오면 기존 프로젝트에서 종속성의 버전을 최신으로 업데이트해서 사용할 수 있습니다. npm install 리액트@latest만 치면 끝나는 일이죠.
하지만 안타깝게도 리액트 서버 컴포넌트는 그렇게 간단한 처리만으로 사용할 수 없습니다. 제가 이해한 바로는 리액트 서버 컴포넌트는 번들러, 서버, 라우터 등 리액트 외부의 여러 가지 요소와 긴밀하게 통합되어야 합니다.
이 글을 쓰고 있는 현재(2023년 9월 19일), 리액트 서버 컴포넌트를 사용할 수 있는 유일한 방법은 Next.js 13.4 이상에서 새롭게 재설계된 "앱 라우터"를 사용하는 것뿐입니다.
앞으로는 더 많은 리액트 기반 프레임워크들이 리액트 서버 컴포넌트를 지원하기를 기대합니다. 리액트의 핵심 기능을 특정 도구 하나에서만 사용할 수 있다는 것은 조금 이상한 느낌입니다! 리액트 문서에는 "최첨단 프레임워크" 섹션이 있는데, 이 섹션에는 리액트 서버 컴포넌트를 지원하는 프레임워크가 나열되어 있습니다. 이 페이지를 가끔 확인하여 새로운 옵션이 추가되는지 살펴볼 계획입니다.
리액트 서버 컴포넌트 패러다임에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트라고 가정합니다. 그렇기 때문에 클라이언트 컴포넌트를 사용하려면 지시문을 사용해야 합니다.
상단의 독립적으로 적힌 'use client' 문자열은 이 파일의 컴포넌트들이 클라이언트 컴포넌트이며, 클라이언트에서 다시 렌더링할 수 있도록 JS 번들에 포함해야 한다는 것을 리액트에 알리는 역할을 합니다.
컴포넌트의 유형을 지정하는게 매우 이상하게 느껴질 수 있지만, 선례가 없는 건 아닙니다. 자바스크립트에서 "엄격 모드(Strict Mode)"를 선택하는 "use strict" 지시문도 있으니까요.
새로운 패러다임에서는 컴포넌트가 디폴트로 서버 컴포넌트로 취급되기 때문에 서버 컴포넌트에는 ‘use server' 지시문을 쓰지 않습니다. 사실 'use server'는 이 글 내용의 범위를 벗어나는 완전히 다른 기능인 서버 액션(Server Actions)에 사용됩니다.
어떤 것을 클라이언트 컴포넌트로 지정해야 하냐면요 어떤 기준으로 서버 / 클라이언트 컴포넌트를 구분해야 하는지 궁금하실 수 있습니다.
일반적으로는 서버 컴포넌트로 만들 수 있다면 그렇게 만들어야 합니다. 서버 컴포넌트는 더 간단하고 이해하기 쉽습니다. 또, 클라이언트에서 실행되지 않기 때문에 자바스크립트 번들에 코드가 포함되지 않는다는 성능상의 이점도 있습니다. 리액트 서버 컴포넌트 패러다임의 장점 중 하나는 페이지 인터랙티브(TTI) 지표를 개선하는 잠재력이 있다는 것입니다.
하지만 그렇다고 해서 단순히 클라이언트 컴포넌트를 없애거나 최소한으로 줄이는 것을 목표로 잡으면 안 됩니다! 지금까지 모든 리액트 앱의 모든 리액트 컴포넌트는 클라이언트 컴포넌트였다는 사실을 잊지 마시고요.
리액트 서버 컴포넌트로 한번 작업 해보면 서버/클라이언트 컴포넌트를 지정하는 것이 매우 직관적이라는 것을 알게 될 것입니다. 상태 변수나 이펙트를 사용하는 컴포넌트는 반드시 클라이언트에서 실행해야 하니까 'use client' 지시문을 붙이면 되고, 나머지는 서버 컴포넌트로 남겨두면 됩니다. |
리액트 서버 컴포넌트에 익숙해졌을 때 처음 가졌던 질문 중 하나는 '컴포넌트의 속성(Props)이 바뀌면 어떻게 되는가?'였습니다.
예를 들어 아래의 서버 컴포넌트를 봐주세요.
최초 서버 사이드 렌더링에서 ‘hits’의 숫자 값이 0이라고 가정해 보겠습니다. 그러면 해당 컴포넌트는 이런 마크업을 생성하겠죠.
여기서 ‘hits’의 값이 변경되면 어떻게 될까요? 이것이 상태 변수이고, 값이 0에서 1로 바뀌었다고 가정해 봅시다. 이때 HitCounter는 다시 렌더링 되어야만 하지만 서버 컴포넌트이기 때문에 렌더링 되지 못합니다!
서버 컴포넌트에만 집중해서 보면 이 문제를 이해하기 어려울 수 있습니다. 좀 더 전체적인 관점에서 애플리케이션의 구조를 살펴보겠습니다.
다음과 같은 컴포넌트 트리가 있다고 가정해 보겠습니다:
트리의 컴포넌트가 모두 서버 컴포넌트라면 이해하지 못할게 하나도 없을 겁니다. 컴포넌트 중 어떤 것도 다시 렌더링하지 않으므로 속성(props)은 절대 변하지 않겠죠.
이번에는 Article 컴포넌트가 hits 상태 변수를 소유하고 있다고 가정해 보죠. 상태가 변경될 수 있게 하려면 클라이언트 컴포넌트로 변환해야 합니다.
여기서 문제가 보이시나요? Article 컴포넌트가 다시 렌더링 되면 HitCounter 및 Discussion을 포함한 모든 하위 구성 요소도 다시 렌더링 됩니다. 하지만 하위 컴포넌트들이 서버 컴포넌트라면 다시 렌더링할 방법이 없습니다.
이처럼 대처 불가능한 상황을 방지하기 위해 리액트 팀은 규칙을 추가했습니다: 클라이언트 컴포넌트가 임포트하는 컴포넌트는 반드시 클라이언트 컴포넌트가 되어야 한다. 즉, Article 컴포넌트에 'use client' 지시문을 사용하면 HitCounter와 Discussion의 인스턴스가 클라이언트 컴포넌트로 변환된다는 겁니다.
저는 리액트 서버 컴포넌트를 실제로 사용해 보면서 이 패러다임이 클라이언트와의 경계(client boundaries)를 만들기 위한 움직임이라는 것을 깨달았습니다. 실제로 어떤 일이 벌어지는지를 다시 한번 같은 예시와 함께 보여드리겠습니다.
Article 컴포넌트에 'use client' 지시문을 추가하면 "클라이언트 경계(Client Boundary)"가 생성되고, 경계 안에 있는 모든 컴포넌트가 암시적으로 클라이언트 컴포넌트로 변환됩니다. 그러니까 Article의 하위에 있는 Hitcounter와 같은 컴포넌트에는 명시적으로 'use client' 지시어가 없더라도 이 특정 상황에서는 클라이언트에서 하이드레이트/렌더링 됩니다.
즉, 클라이언트에서 실행해야 하는 모든 파일에 일일이 'use client'를 적을 필요가 없습니다. 새 클라이언트 경계를 생성해야 할 때만 추가하면 됩니다.
처음 클라이언트 컴포넌트가 서버 컴포넌트를 렌더링할 수 없다는 사실을 알게 되었을 때는 상당한 제약으로 느껴졌습니다. 만약 애플리케이션의 상위 컴포넌트에서 상태를 사용해야 한다면 어떻게 해야 할까요? 모든 것이 클라이언트 컴포넌트가 되어야 한다는 뜻일까요?
이 같은 경우에는 대부분 애플리케이션을 재구성하여 소유자가 변경되도록 하면 문제를 해결할 수 있습니다.
글로만 풀어서 설명하기 다소 까다롭기 때문에 예를 들어 설명하겠습니다:
사용자가 다크 모드/라이트 모드를 전환할 수 있도록 하려면 리액트 상태를 활용해야 합니다. 이 작업은 애플리케이션 트리의 꽤 높은 위치에서 이루어져야 하며, 이렇게 하면 <body> 태그에 CSS 변수 토큰을 적용할 수 있습니다.
상태를 사용하려면 먼저 Homepage를 클라이언트 컴포넌트로 만들어야 합니다. Homepage는 애플리케이션의 최상위 컴포넌트이므로 Header와 MainContent도 암시적으로 클라이언트 컴포넌트로 변경됩니다.
이 문제를 해결하기 위해 색상 관리 항목을 독립적인 컴포넌트로 분리하여 새 파일로 옮겨보겠습니다.
그런 다음 Homepage에서 새로 만든 컴포넌트를 아래와 같이 사용합니다.
이제 Homepage는 더 이상 상태나 다른 클라이언트 사이드 리액트의 기능을 사용하지 않기 때문에 ‘use client’ 지시문을 삭제해도 됩니다. 이러면 Header와 MainContent가 클라이언트 컴포넌트로 변환되는 문제가 해결됩니다.
근데 잠시만요, 조금 이상하지 않나요? 클라이언트 컴포넌트인 ColorProvider는 관계상 Header와 MainContent의 부모입니다. 따라서 컴포넌트를 분리했지만, 여전히 트리에서는 더 높은 위치에 있겠죠?
클라이언트 경계에 있어서는 부모/자식 관계가 중요하지 않습니다. 예시에서는 Homepage가 Header와 MainContent를 불러와 렌더링하기 때문에 Homepage가 이들 컴포넌트의 프로퍼티를 결정합니다.
기억하세요, 우리가 해결하려는 문제는 서버 컴포넌트를 다시 렌더링할 수 없으므로 해당 프로퍼티에 새 값을 부여할 수 없다는 것입니다. 처리를 마친 환경에서는 Header와 MainContent에 대한 프로퍼티를 결정하는 Homepage가 서버 컴포넌트이므로 문제가 없습니다.
정말이지 이해하기 어려운 개념입니다. 저도 몇 년이나 리액트를 사용했음에도 불구하고 매우 헷갈렸습니다. 저도 감을 잡기 위해 상당한 실습이 필요했습니다.
더 정확하게 말하면, 'use client' 지시어는 파일이나 모듈 레벨에서 작동합니다. 즉, 클라이언트 컴포넌트 파일이 임포트한 모듈도 전부 클라이언트 컴포넌트여야 합니다.
예시 코드의 색상 테마 변경 기능 위의 예시에서 색상 테마를 변경하는 기능이 없다는 것을 눈치챘을 수도 있습니다. setColorTheme은 호출하지 않았습니다.
예시를 최대한 간단하게 만들기 위해서 몇 가지 부분은 생략했습니다. 완성도 있는 코드에서는 컨텍스트(context)를 사용(consume)하여 모든 자손이 setter 함수를 사용할 수 있도록 했을 겁니다. 컨텍스트를 사용하는 게 클라이언트 컴포넌트이기만 하면 모든 것이 잘 작동할 겁니다. |
개념 소개는 충분히 했으니 조금 더 상세한 내용을 살펴보겠습니다. 서버 컴포넌트를 사용하면 출력은 어떻게 될까요? 실제로 무엇이 생성될까요?
아주 간단한 리액트 애플리케이션부터 시작해 보겠습니다.
리액트 서버 컴포넌트 패러다임에서 모든 컴포넌트는 기본적으로 서버 컴포넌트라고 말씀드렸습니다. 예시의 컴포넌트를 클라이언트 컴포넌트라고 명시하지 않았기 때문에(또는 클라이언트 경계 내에서 렌더링하지 않았으므로) 이 컴포넌트는 서버에서만 렌더링 됩니다.
브라우저에서 이 애플리케이션을 방문하면 다음과 같은 HTML 문서를 받게 됩니다.
예시를 위한 간소화 간단하게 설명하기 위해 몇 가지 조정을 했습니다. 예를 들어, 실제 리액트 서버 컴포넌트 컨텍스트에서 생성된 JS는 HTML 문서의 크기를 줄이기 위해 문자열화된 JSON 배열을 사용하는데, 이 부분은 최적화를 위한 것입니다.
또한 HTML에서 중요하지 않은 부분(예: <head>)을 모두 제거했습니다. |
예시의 HTML 문서에는 리액트 애플리케이션이 생성한 UI인 "Hello world!" 단락이 포함되어 있습니다. 이는 서버 사이드 렌더링 덕분에 나타나는 것이며, 리액트 서버 컴포넌트가 직접적으로 수행한 결과물은 아닙니다.
그 아래에는 JS 번들을 로드하는 <script> 태그가 있습니다. 이 번들에는 리액트와 같은 종속 요소(dependencies)와 애플리케이션에 사용되는 모든 클라이언트 컴포넌트가 포함되어 있지만 Homepage 컴포넌트는 서버 컴포넌트이므로 이번들에 들어있지 않습니다.
마지막으로, 두 번째 <script> 태그에는 인라인 JS가 있죠.
이 부분은 정말 흥미롭습니다. 여기서 프로그래머가 한 일은 본질적으로 리액트에게 "Homepage 컴포넌트 코드가 빠져 있지만 걱정하지 마."라고 얘기한 것과 같습니다.
일반적으로 리액트가 클라이언트에서 하이드레이션될 때, 모든 컴포넌트를 빠르게 렌더링하여 애플리케이션의 가상 표현(virtual representation)을 만듭니다. 그러나 서버 컴포넌트는 코드가 JS 번들에 포함되어 있지 않기 때문에 이 작업을 수행할 수 없습니다.
따라서 렌더링된 값과 함께 서버에서 생성한 가상의 표현을 전송하며, 리액트가 클라이언트에서 로드되면 내용을 재생성하는 대신 재사용합니다.
이것이 이전의 ColorProvider 예제가 작동할 수 있는 이유입니다. Header와 MainContent의 결과물은 children 프로퍼티를 통해 ColorProvider 컴포넌트로 전달됩니다. ColorProvider는 원하는 만큼 렌더링할 수 있지만, 데이터는 서버에 의해 고정된 정적 데이터입니다.
서버 컴포넌트가 어떻게 직렬화(serialized)되고 네트워크를 통해 전송되는지 실제로 보고 싶으시다면 개발자 알바르 라겔뢰프(Alvar Lagerlöf)가 만든 RSC Devtools를 읽어 보세요.
서버 컴포넌트에는 서버가 필요하지 않습니다. 이 글의 앞부분에서 서버 사이드 렌더링은 다음과 같은 다양한 렌더링 전략을 포괄하는 '포괄적 용어'라고 언급했습니다:
리액트 서버 컴포넌트는 두 전략을 모두 지원합니다. 서버 컴포넌트가 Node.js 런타임에서 렌더링 될 때 리턴되는 자바스크립트 객체가 생성됩니다. 이는 온디맨드로 생성될 수도 있고, 빌드 중에 생성될 수도 있습니다.
즉, 서버 없이도 리액트 서버 컴포넌트를 사용할 수 있고, 여러 개의 정적 HTML 파일을 생성하고 원하는 곳에 호스팅할 수 있다는 뜻입니다! 그리고 이것이 사실 Next.js 앱 라우터에서 기본적으로 일어나는 일이며, "온디맨드"가 필수가 아니라면 이 모든 작업은 빌드 과정에서 미리 수행됩니다. |
리액트를 안 쓰면요? 애플리케이션에 클라이언트 컴포넌트를 포함하지 않는다면 리액트를 다운로드할 필요가 있는지 의문이 들 수 있습니다. 혹은 리액트 서버 컴포넌트를 사용해서 자바스크립트가 없는 정적인 웹사이트를 구축할 수 있을지 궁금하실 수도 있죠.
여기서 문제는 리액트 서버 컴포넌트는 아직은 Next.js 프레임워크에서만 사용할 수 있으며, 이 프레임워크에는 라우팅과 같은 것을 관리하기 위해 클라이언트에서 실행해야 하는 코드가 많이 있다는 것입니다.
하지만 아이러니하게 이는 실제로는 더 긍정적인 사용자 경험을 제공하는 경향이 있습니다. 예를 들어 Next.js의 라우터는 완전히 새로운 HTML 문서를 로드할 필요가 없기 때문에 일반적인 <a> 태그보다 링크 클릭을 더 빠르게 처리합니다.
잘 구조화된 Next.js 애플리케이션은 자바스크립트가 다운로드되는 동안에도 계속 작동하지만, 자바스크립트가 로드되면 훨씬 더 빠르고 원활하게 작동합니다. |
리액트 서버 컴포넌트는 리액트에서 서버 전용 코드를 실행하는 최초의 "공식적인" 방법입니다. 그러나 더 넓은 리액트 생태계의 관점에서 보면 새로운 것은 아닙니다! 앞서 언급했듯이 2016년부터 Next.js에서 서버 전용 코드를 실행할 수 있었기 때문이죠!
가장 큰 차이점은 이전과 달리 컴포넌트 내에서 서버 전용 코드를 실행하는 방법이 생겼다는 점입니다.
가장 명백한 이점은 성능입니다. 서버 컴포넌트가 자바스크립트 번들에 포함되지 않으므로 다운로드해야 하는 자바스크립트의 양과 하이드레이션해야 하는 컴포넌트의 수가 줄어듭니다.
하지만 이것이 가장 흥미로운 부분은 아닙니다. 솔직히 말해서 대부분의 Next.js 앱의 '페이지 상호작용(interactive)’은 이미 충분히 빠릅니다.
시맨틱 HTML*원칙을 준수한 대부분의 어플리케이션은 리액트가 하이드레이션되기 전에도 작동할 것입니다. 링크를 따라가거나, 양식을 제출하거나, 혹은 아코디언을 확장 및 축소할 수 있을 겁니다(<details>과 <summary>을 사용해서). 그리고 솔직히 보통의 경우 리액트가 하이드레이트하는 데 몇 초 정도가 걸리더라도 지장은 없습니다.
*번역자 주: 단순히 표현이나 모양을 정의하기 보다는 웹 페이지와 웹 애플리케이션에 있는 정보의 의미 또는 의미를 강화하기 위해 HTML 마크업을 사용하는 것
반대로 정말 멋진 포인트는 더 이상 기능과 번들 크기의 트레이드오프를 고려하지 않아도 된다는 겁니다!
예를 들어, 대부분의 기술 블로그에는 일종의 구문 강조 라이브러리가 필요합니다. 제 블로그에서는 프리즘(Prism)을 사용하고 있습니다. 코드 스니펫을 보면 이렇습니다.
인기 있는 모든 프로그래밍 언어를 지원하는 적절한 구문 강조 라이브러리의 크기는 몇 메가바이트나 되기 때문에 JS 번들에 넣기에는 너무 큽니다. 따라서 필수적이지는 않은 지원 언어와 기능을 포기해야 하는 경우가 많았습니다.
이번엔 구문 강조 표시를 서버 컴포넌트에서 수행한다고 가정해 보겠습니다. 이 경우에는 라이브러리 코드가 JS 번들에 포함되지 않기 때문에 라이브러리의 크기는 전혀 상관없습니다. 따라서 라이브러리의 모든 기능을 사용할 수 있습니다.
그리고 이것이 리액트 서버 컴포넌트와 함께 작동하도록 디자인된 최신 구문 강조 패키지 “브라이트(Bright)”의 핵심 아이디어입니다.
이런 점이 바로 제가 리액트 서버 컴포넌트의 등장을 반기는 이유입니다. JS 번들에 포함하기에는 비용이 너무 많이 들어서 불가능한 것들이 이제는 서버에서 무료로 실행될 수 있으며, 번들에 어떠한 용량도 추가하지 않으면서 사용자에게 더 좋은 경험을 제공할 수 있습니다.
성능과 UX외로 사용성 또한 리액트 서버 컴포넌트의 큰 장점입니다. 한동안 RSC를 사용해 보니, 서버 컴포넌트가 얼마나 사용하기 쉬운지 알게 되었습니다. 종속성 배열(dependency arrays), 오래된 클로저(stale closures), 메모화(memoization) 또는 변경 사항으로 인해 발생하는 기타 복잡한 문제에 대해 걱정할 필요가 없죠.
하지만 어쨌거나 불과 몇 달 전에 베타 버전이 출시된 리액트 서버 컴포넌트는 아직 초기 단계입니다! 앞으로 리액트 커뮤니티가 브라이트와 같은 혁신적인 솔루션을 내놓으며 이 새로운 패러다임을 어떻게 발전시킬지 정말 기대됩니다. 한 명의 리액트 개발자로서 정말 신나는 시기입니다!
리액트 서버 컴포넌트는 그 자체로도 흥미로운 발전이지만, 사실 “모던 리액트"라는 퍼즐의 한 조각에 불과합니다.
리액트 서버 컴포넌트와 서스펜스(Suspense), 그리고 새로운 스트리밍 SSR 아키텍처를 결합하면 정말 흥미로워집니다. 이 아키텍처를 사용하면 이렇게 멋진 작업을 할 수 있습니다:
해당 아키텍처는 이 글의 범위를 벗어나기 때문에 자세한 내용은 Github에서 확인해 보세요!
리액트 서버 컴포넌트는 중요한 패러다임 전환입니다. 저는 앞으로 몇 년 동안 서버 컴포넌트를 활용하는 브라이트와 같은 도구가 더 많이 만들어지면서 리액트 생태계가 어떻게 발전할지 매우 기대됩니다.
앞으로 리액트로 빌드하는 게 더 멋질 것 같은 예감이 드네요.
<원문>
Making Sense of React Server Components
위 번역글의 원 저작권은 Josh Comeau에게 있으며, 요즘IT는 해당 글로 수익을 창출하지 않습니다.