거대한 번들 사이즈는 시작에 불과했다. 설령 사용자가 인내심을 갖고 그 코드 덩어리를 모두 내려받는다 해도, 고통은 끝나지 않았다. React 팀의 또 다른 엔지니어, 로렌 탄(Lauren Tan)은 크롬 개발자 도구의 ‘네트워크(Network)’ 탭을 보며 쓴웃음을 지었다.
그녀의 화면에는 계단식으로 길게 늘어선 막대그래프가 펼쳐져 있었다. 마치 폭포수처럼, 하나의 요청이 끝나자 다른 요청이 시작되고, 그 요청이 끝나자 또 다른 요청이 이어지는 절망적인 행렬이었다. 개발자들 사이에서 ‘네트워크 폭포수(Network Waterfall)’라 불리는 현상이었다.
문제의 원인은 React의 렌더링 방식에 있었다.
먼저 최상위 컴포넌트인 App
이 클라이언트에서 렌더링된다. App
은 렌더링 과정에서 자신이 보여줄 사용자 프로필 정보가 필요하다는 것을 깨닫고, 서버에 데이터 요청을 보낸다.
GET /api/user/profile
서버가 응답할 때까지, 화면의 프로필 영역은 텅 비어있다. 몇백 밀리초 후, 프로필 데이터가 도착한다. React는 그 데이터를 받아 UserProfile
컴포넌트를 렌더링한다. 그런데 UserProfile
컴포넌트는 자신이 보여줘야 할 친구 목록 데이터가 필요했다. 그래서 다시 서버에 요청을 보낸다.
GET /api/user/friends
다시 기다림의 시간. 친구 목록이 도착하면, React는 FriendList
컴포넌트를 렌더링하고, 화면에 친구들의 이름이 하나둘 나타난다. 하지만 그중 한 친구 컴포넌트는 또 그 친구의 최신 게시물이 필요할지도 모른다.
GET /api/posts/latest?userId=...
이런 식이었다.
컴포넌트 A가 렌더링되어야 필요한 데이터를 알 수 있고, 그 데이터가 도착해야 자식 컴포넌트 B가 렌더링된다. 그리고 B 역시 자신의 데이터를 가져오기 위해 또다시 네트워크를 건너야 했다. 모든 과정이 순차적으로, 직렬로 일어났다. 최종 화면이 완성되기까지의 시간은 이 모든 요청 시간을 합한 것과 같았다.
“이건 너무 비효율적이야.”
로렌은 중얼거렸다.
“페이지를 보여주는 데 프로필, 친구 목록, 최신 게시물이 필요하다는 건 이미 정해진 사실인데, 왜 우리는 이걸 하나씩 차례대로 물어봐야만 하는 거지?”
사용자가 보는 화면은 처참했다. 처음엔 로딩 스피너만 보이다가, 잠시 후 프로필 사진이 툭 나타난다. 또 잠시 후 친구 목록이 그려지고, 마지막으로 게시물이 채워진다. 전체 로딩이 끝날 때까지 화면은 여러 번 깜빡이며 배치가 계속 바뀌었고, 사용자 경험은 조각나 있었다.
팀은 이 문제를 해결하기 위해 GraphQL 같은 기술을 도입해 필요한 모든 데이터를 한 번의 요청으로 가져오는 방법을 시도하기도 했다. 하지만 그것은 또 다른 복잡성을 낳았고, 모든 프로젝트가 채택할 수 있는 범용적인 해결책은 아니었다.
로렌은 화이트보드 앞에 섰다. 그리고 ‘Client’와 ‘Server’라고 적힌 두 개의 상자를 그렸다. 지금까지의 모든 문제는 클라이언트가 너무 많은 일을, 너무 늦게 시작하기 때문에 발생했다.
그녀는 펜을 들어 두 상자 사이를 가로지르는 화살표들을 그렸다. 폭포수처럼 쏟아지는 화살표들.
“만약….”
그녀의 눈빛이 날카롭게 빛났다.
“이 요청들이 클라이언트가 아니라, 애초에 다른 곳에서 시작될 수 있다면? 우리가 페이지를 그리기도 전에, 필요한 데이터가 무엇인지 미리 알 수만 있다면?”
그것은 현재의 React 아키텍처에서는 불가능한 일이었다. 컴포넌트가 렌더링되기 전까지는 아무도 그 안에 어떤 데이터 요청 코드가 숨어있는지 알 수 없었으니까.
이 절망적인 폭포수를 멈추기 위해서는, 물이 떨어지기 시작하는 그 근원, 즉 렌더링과 데이터 요청의 순서를 근본적으로 뒤집어야만 했다. 로렌은 자신이 그린 폭포수 그림을 가만히 응시했다. 이 계단식 절망을 끊어낼 방법은 분명히 존재해야만 했다.