RSC, Suspense , concurrency mode, use, hydrate 등
최근 리액트의 변화 흐름은 제 머리로는 코어컨셉을 이해하기도 힘겹습니다.
next.js 와 같은 프레임워크에서 RSC를 적극적으로 사용하고 있으니
아..그렇다.. 뭐.. 서버겠구나.. 하고 넘어가지만 그렇다고
RSC !== SSR 은 아니라는 말을 들으니 오히려 더 혼란스러워지는 이 기분..
여러분도 아실까요? 아시겠죠... 네..
🤯 RSC는??
react 18 버전부터 도입된 개념으로서 React Server Components 의 줄임말입니다.
서버에서만 렌더링 되어 전달되는 컴포넌트를 의미합니다.
리액트 서버 컴포넌트는 HTML 이 아닌 "특별한 형태로" 서버에서 렌더링 되어 클라이언트에게 전달됩니다.
이러한 리액트 서버 컴포넌트는 서버에서 렌더링을 수행한다는 방법을 통하여
기존의 리액트 컴포넌트가 할 수 없었던 다양한 일을 수행할 수 있도록 도와줍니다.
🤯 RSC가 해결하는 기존의 문제점들
RSC가 해결하는 기존의 문제는 크게 두가지로 정리할 수 있다고 저는 생각합니다.
1. 데이터 페칭의 워터폴 문제
2. 라이브러리 번들사이즈 문제
이 두가지는 어떤 원인에서 문제가 되는 것일까요?
🤯 데이터 페칭의 워터폴 문제
부모 자식 관계에 있는 여러개의 컴포넌트에서
각각의 컴포넌트가 모두 API 호출이 필요한 상황을 가정해봅시다.
function KakaopayHome({ memberId }) {
return (
<MemberDetails memberId={memberId}>
<MoneyBalance memberId={memberId}></MoneyBalance>
<PaymentHistory memberId={memberId}></PaymentHistory>
</MemberDetails>
);
}
이 코드는 카카오 기술 블로그에서 발췌한 코드입니다.
memberId를 prop으로 전달받고 그 멤버아이디를 기반으로
여러가지 컴포넌트에게 아이디를 전달해주고 있습니다.
이렇듯 여러가지의 컴포넌트가 각각의 정보를 필요로 하는 상황에서 개발자는 두가지 선택지를 가집니다
1. 부모 컴포넌트에서 하나의 거대한 API를 호출하고 그 결과물을 자식에게 prop으로 내려주기
2. 각각의 컴포넌트가 필요로 하는 api는 그 컴포넌트에서 페칭을 수행하기
1번 방식을 선택하면 어떨까요?
1번 방식의 장단점을 고려해보면 다음과 같습니다.
장점 : API 요청수, 네트워크 비용을 줄일 수 있다.
단점 : 부모 , 자식 컴포넌트가 서로 강결합되고 유지보수가 어려워진다.
단점 : 만약 컴포넌트를 재사용하거나, 구성이 바뀌게 되면 불필요한 정보까지 페칭하는 over-fetching이 일어날 수 있다.
2번 방식은 그럼 어떨까요?
장점 : 각 컴포넌트가 렌더링에 필요한 정보만을 가져와서 보여줄 수 있다.
단점 : 네트워크 상황이 좋지 못한 경우(high latency가 발생하는 경우) 네트워크 요청에 시간이 오래걸린다.
단점 : 부모 컴포넌트 렌더링 -> 부모 컴포넌트 데이터 페칭 -> 자식 컴포넌트 렌더링 -> 자식 컴포넌트 데이터페칭
과 같이 부모 컴포넌트의 데이터 페칭이 완료되기 전까지 자식컴포너트의 렌더가 지연되기 때문에
데이터페칭에 waterfall 현상이 발생하게 됩니다.
이 문제를 해결할 방법은 없을까요?
🤯 라이브러리 번들 사이즈 문제
리액트는 기본적으로 view를 담당하는 라이브러리라고 주장합니다.
따라서 리액트 생태계는 개발에 필요한 요소들을 외부 라이브러리에서 충족시키는 형태로 발전했습니다.
그리고 개발자는 필요에 따라 여러가지 라이브러리를 사용하게 됩니다.
그런데 이 라이브러리에는 피할 수 없는 번들 사이즈 문제가 있습니다.
최근에는 대부분의 경우 트리쉐이킹이 지원되지만
그럼에도 불구하고 번들사이즈의 증가는 필연적입니다. 코드가 있으니까 코드의 사이즈도 커지죠..
그리고 번들 사이즈의 증가는 자연스레 TTI(Time To Interactive) 시간을 늘립니다.
번들이 커질수록 번들을 받아오는데 걸리는 시간도 오래 걸리게 되고
그럼 사용자의 유의미한 상호 작용이 가능한 시간도 지연되게 됩니다.
🤯 RSC는 SSR일까?
리액트 서버 컴포넌트는 이름에도 서버가 들어가있듯이 서버사이드렌더링을 할 것 같이 느껴집니다.
그러나... RSC와 SSR은 상호협력 관계에 더 가깝습니다.
우선 전통적인 의미의 SSR을 떠올려볼 필요가 있습니다.
참고로 next.js 에서는 SSR 대신 Dynamic pre-rendring 이라는 표현으로 부르기도 한답니다.
서버사이드 렌더링은 서버에서 HTML을 미리 만들어두고 전송합니다.
반면 클라이언트 사이드 렌더링은 빈 HTML 문서를 전송하고 JS 를 이용하여 동적으로
DOM 요소들을 생성하며 콘텐츠들을 채우게 됩니다.
따라서 클라이언트 사이드 렌더링은 자바스크립트 번들을 모두 전송받기 전까지는
사용자가 흰화면을 볼 수 밖에 없는 구조가 됩니다.
반면 서버사이드 렌더링의 경우에는 사용자가 상호작용을 하는데에는 여전한 시간이 걸리지만
(상호작용을 위해서는 자바스크립트 파일이 필요합니다.
추가로 정적인 HTML에 자바스크립트를 붙여주는 것을 하이드레이션이라고도 표현합니다.)
사용자에게 유의미한 콘텐츠를 처음 보여주는 시간은 클라이언트사이드렌더링에 비해
훨씬 빠를 수 밖에 없는 것이지요
SSR은 참..직관적입니다. html을 리턴해줍니다.
반면 RSC는?
앞서 RSC가 반환하는 것은 HTML 이 아닙니다.
M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":"@4"}]}]]}],["$","section","null",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click a note on the left to view something! 🥺"}]}]}]}]]}]
M5:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J4:["$","ul",null,{"className":"notes-list","children":[["$","li","1",{"children":["$","@5",null,{"id":1,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","2",{"children":["$","@5",null,{"id":2,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","3",{"children":["$","@5",null,{"id":3,"title":"I wrote this note today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note today"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","4",{"children":["$","@5",null,{"id":4,"title":"Make a thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a thing"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","6",{"children":["$","@5",null,{"id":6,"title":"Test Noteeeeeeeasd","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Test note's text"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Test Noteeeeeeeasd"}],["$","small",null,{"children":"11/29/22"}]]}]}]}],["$","li","7",{"children":["$","@5",null,{"id":7,"title":"asdasdasd","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"asdasdasd"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"asdasdasd"}],["$","small",null,{"children":"11/29/22"}]]}]}]}]]}]
이 코드가 바로 rsc의 반환값을 직렬화한 것입니다.
RSC는 마치 JSON과 비슷하게 생긴 무언가를 반환합니다.
M으로 시작하는 라인은 클라이언트 번들에서 컴포넌트 함수를 조회하는 데 필요한 정보와 클라이언트 컴포넌트의 module.reference를 정의합니다.
J 로 시작하는 라인은 앞에서 M 라인이 정의한 클라이언트 컴포넌트를 참조하는 것으로 실제 리액트 컴포넌트의 element 트리를 정의합니다.
S는 리액트 서스펜스에 관한 부분입니다.
그런데 J 라인들을 읽어보면 의아한 곳들이 발견됩니다 @5 와 같이 @가 정의되어있는 부분인데요
이 부분은 아직 데이터 페칭이 완료되지 않았기 때문에 임시로 보여지는 placeholder라고 볼 수 있습니다.
이렇듯 RSC는 모든 데이터를 기다리는 것이 아니라 한행이 완성되면
그 행을 즉각적으로 반영하여 작업을 하고 아직 그릴 수 없는 부분은 체크만 해두고 넘어간다는 것입니다.
그리고 이것은 raw html이 아니라 앞서 본것과 같은 스트림 형태의 JSON 으로 이루어진다는것입니다.
그 외에도 SSR은 작은 변경 사항이 있을 때에도 전체 페이지를 다시 요청해야한다거나
RSC는 일부분만 서버에 요청해서 바꿔올 수 있다거나.. 등등 다양한 차이가 있지만
제가 느끼기에 가장 직관적이고 커다란 차이는 이 부분이라고 생각해서
이 부분만 서술하였습니다. 그 외에도 많은 차이가 있어요!
🤯 왜 굳이 이렇게 해야하는데?
결국 RSC는 리액트 프로젝트에서 클라이언트 컴포넌트와 함께 사용되어야 하는 녀석이기 때문
HTML을 파싱해서 react element로 만드는 것보다
RSC가 반환하는 스트림 데이터를 활용하여 react element를 만드는 것이 더 쉽기 때문입니다.
🤯 마치며
솔직히 너무 어려워서 이해하는 것도 급급한 것 같습니다.
본문보다는 잘 쓰여진 제 레퍼런스 참고 목록들을 두루 살펴보시는 것을 추천드립니다.
🤯 레퍼런스
https://tech.kakaopay.com/post/react-server-components/
https://www.youtube.com/watch?v=TQQPAU21ZUw
https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md
https://github.com/reactwg/server-components/discussions/4
https://velog.io/@leehyunho2001/Hydrate
https://velog.io/@superlipbalm/hydration-tree-resumability-map
https://patterns-dev-kr.github.io/rendering-patterns/react-server-components/
https://www.plasmic.app/blog/how-react-server-components-work
https://shopify.github.io/hydrogen-v1/tutorials/react-server-components
https://www.webscope.io/blog/server-components-vs-ssr
https://yceffort.kr/2022/01/how-react-server-components-work
'react' 카테고리의 다른 글
실습과 함께 배우는 리액트 쿼리로 낙관적 업데이트 하는 법 (1) | 2023.10.15 |
---|---|
리액트 타이핑효과 커스텀 훅 만들기 (0) | 2023.08.15 |
리액트를 사용하는 이유는 무엇일까? (2) | 2023.08.04 |
reactquery useQuery 자동 가져오기 막는 법 (0) | 2023.07.31 |
react-error-boundary 라이브러리로 에러처리하기 (0) | 2023.07.20 |