폭포수와 기다림

32025년 08월 18일3

거대한 번들 사이즈는 시작에 불과했다. 설령 사용자가 인내심을 갖고 그 코드 덩어리를 모두 내려받는다 해도, 고통은 끝나지 않았다. 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 아키텍처에서는 불가능한 일이었다. 컴포넌트가 렌더링되기 전까지는 아무도 그 안에 어떤 데이터 요청 코드가 숨어있는지 알 수 없었으니까.

이 절망적인 폭포수를 멈추기 위해서는, 물이 떨어지기 시작하는 그 근원, 즉 렌더링과 데이터 요청의 순서를 근본적으로 뒤집어야만 했다. 로렌은 자신이 그린 폭포수 그림을 가만히 응시했다. 이 계단식 절망을 끊어낼 방법은 분명히 존재해야만 했다.