Ray Book
에세이

Netflix는 왜 React를 걷어냈는가, 2017년의 답이 프레임워크의 디폴트가 되기까지

Netflix가 랜딩 페이지에서 클라이언트 React를 떼어낸 결정에서 시작해, 그 문제의식이 어떻게 Next.js의 오늘로 이어졌는지, 그 여정이 개발자에게 남기는 질문을 따라간다

essayreactssrcachenetflix

먼저, 오해부터 풀고 가자

한 번씩 타임라인에 이런 문장이 뜬다. "Netflix가 React를 버렸다." 어떤 곳에선 React의 종말처럼 다뤄지고, 어떤 곳에선 찻잔 속 태풍쯤으로 소비된다. 양쪽 다 조금씩 빗나가 있다.

사실관계부터 정리하자. Netflix는 React를 버리지 않았다. 내부 도구와 로그인 이후의 앱 영역은 여전히 React 기반이다. 심지어 랜딩 페이지에서도 서버 사이드 React는 그대로 남아 있다 . 걷어낸 것은 정확히 클라이언트 사이드 React 다. 서버는 React로 HTML을 만들고, 브라우저에는 그 HTML과 최소한의 바닐라 JS만 내려보내기로 한 것이다.

이 구분이 중요하다. "React를 버렸다"가 아니라 "React를 서버에만 두기로 했다". 결과는 공개돼 있다. 번들 200KB 이상 감소, TTI 50% 이상 개선. 이 숫자들은 Addy Osmani의 케이스 스터디Jake Archibald의 글에서 확인할 수 있다.

이 글은 "React가 좋냐 나쁘냐"를 가리는 글이 아니다. 2017년의 Netflix가 어디서 걷어냈는지, 그 선택이 이후 9년 동안 프레임워크를 어떻게 바꿔 놓았는지, 그리고 그 흐름이 지금 우리가 SSR 페이지를 만드는 방식과 어떻게 이어져 있는지를 따라가 본다.

랜딩 페이지라는 특수한 전장

Netflix의 비로그인 랜딩 페이지는 세상에서 가장 많이 열리는 HTML 문서 중 하나다. 그런데 이 페이지는 겉과 달리 기술적으로 굉장히 지루하다. 인터랙션이랄 게 거의 없다. 큼직한 히어로 이미지, 이메일 입력 한 줄, "지금 시작하기" 버튼. 그게 전부다.

여기서 중요한 건 두 가지 숫자다.

첫째, 이 페이지에 도달하는 사람 대부분은 같은 HTML 을 받아도 아무 문제가 없다. 지역·언어 정도만 분기되면 충분하고, 그 분기도 수십 가지를 넘지 않는다. 즉 이 페이지는 본질적으로 캐시 친화적 이다.

둘째, 이 페이지에서는 TTI(Time To Interactive)와 LCP(Largest Contentful Paint)가 전환율에 직결 된다. 500ms가 전환율 몇 퍼센트를 좌우하는 전장이다. 200KB짜리 번들이 여기서는 사치가 아니라 출혈이다.

두 숫자를 같이 놓고 보면 문제의 형상이 드러난다. 대부분의 사용자에게 같은 HTML을 주면 되는, 그리고 1바이트의 JS도 아까운 페이지. 이런 페이지에 클라이언트 React 런타임을 올리는 건 좀 과장하면 전단지 한 장 뽑으려고 공장 라인을 돌리는 것과 비슷하다.

SSR은 HTML을 돌려주지만, 캐시를 돌려주지는 않는다

여기서 많은 사람들이 반문한다. "React도 SSR 되잖아. HTML 내려주면 되는 거 아냐?"

맞다. React는 HTML을 내려준다. 하지만 그 HTML은 하이드레이션을 전제로 한 HTML이다. 여기서 모든 게 갈린다.

Next.js, Remix 같은 프레임워크의 SSR이 하는 일을 한 줄로 요약하면 이렇다.

요청 → 서버에서 React 컴포넌트 렌더 → HTML 생성 → 전송

                          클라이언트에서 동일한 컴포넌트 트리 재생성

                          이벤트 핸들러 부착 (hydration)

겉으로 보면 전통적인 서버 렌더와 비슷하다. 그런데 속은 다르다.

서버가 뱉은 HTML은 혼자서 일하지 못한다. 그 옆에 같은 트리를 다시 그릴 수 있는 JS 번들이 반드시 따라붙어야 한다. 버튼 하나 눌러도 의미 있는 반응이 나오려면 하이드레이션이 끝나야 한다. 그 사이의 몇 백 밀리초는 사용자에겐 "페이지가 멈춘 시간"이다.

여기까지는 잘 알려진 이야기다. 진짜 함정은 캐시 쪽에 있다.

캐시의 관점에서 본 React SSR

CDN 엣지가 하는 일은 단순하다. URL을 키로 삼고, 그 키에 대한 응답을 들고 있다가 다음 사람에게 돌려준다. 엣지가 캐시를 잘 쓰려면 조건이 하나뿐이다. 같은 키에 같은 응답을 줄 수 있어야 한다.

2017년 당시 대부분의 React SSR 앱은 이 조건을 자주 깼다. 그것도 꽤 미묘한 방식으로.

  • 로그인 상태에 따라 헤더가 바뀐다. 쿠키 기반으로 HTML이 갈라진다.
  • A/B 테스트 분기로 사용자마다 다른 HTML이 나간다.
  • 최근 본 상품, 개인화 추천 등 사용자 단위 렌더가 섞인다.
  • 플래그·피처 토글의 조합 수만큼 HTML이 갈라진다.

이게 React 탓이냐면 당연히 아니다. 오히려 React의 장점이다. 어떤 상태든 선언적으로 HTML로 풀어낼 수 있다는 것. 문제는 이 장점이 CDN 엣지 입장에서는 골치 아픈 특성이라는 점이다. 엣지가 들고 있는 HTML은 누구에게도 딱 맞지 않아서 결국 오리진까지 간다 .

그래서 React SSR을 잘 쓰는 팀은 대개 둘 중 하나를 했다. 페이지를 잘게 쪼개서 개인화된 조각만 클라이언트에서 채우거나, 수작업으로 부분 캐싱을 구성하거나. 둘 다 React의 책임은 아니지만, React가 전제하는 렌더 모델이 캐시와 구조적으로 어긋나기 때문에 필요해진 공사였다.

Netflix는 다른 길을 골랐다. 그 길은 간단했다. "이 페이지는 개인화가 필요 없다. 그러면 애초에 하이드레이션이 필요 없다."

Netflix가 한 일, 클라이언트 런타임을 떼어낸 HTML

다시 짚자. Netflix가 한 일은 이렇다. 서버에서는 계속 React로 HTML을 렌더한다. 다만 그 HTML과 함께 React 클라이언트 번들을 내려보내지 않는다. 페이지에 꼭 필요한 인터랙션, 그러니까 언어 선택기나 이메일 입력 검증 같은 것들은 Jake Archibald의 정리에 따르면 300줄 남짓의 바닐라 JS로 재작성됐다.

결과는 담백하다. 번들 200KB 이상 감소, TTI 50% 이상 개선. 클라이언트 프레임워크를 걷어냈다는 이유 하나로.

더 흥미로운 건 이 HTML이 얻은 새로운 속성이다. 이 HTML은 혼자서 일한다. 하이드레이션을 기다리지 않고, 동반 번들이 없어도 버튼이 눌린다. 그리고 가장 중요한 건, 이 HTML은 엣지에서 그대로 캐시된다 . 지역과 언어 정도만 키에 더하면 끝이다.

요청이 들어오면 대부분의 경우 오리진까지 가지 않는다. 엣지가 응답을 바로 돌려준다. 이건 단순한 성능 최적화가 아니라, 트래픽이 열 배가 돼도 오리진 부하가 거의 늘지 않는 구조로 바꾸는 일이다.

2017년의 답이 프레임워크의 디폴트가 되기까지

여기서 이 글이 2017년에 멈췄다면 지금 와서 읽을 이유는 별로 없다. Netflix가 손으로 풀었던 문제는 그 뒤 9년 동안 업계 전체가 다른 각도로 다시 풀었고, 그 여정이 곧 오늘의 프레임워크다.

2019, Next.js ISR. 정적 생성과 서버 렌더의 중간 지점. 빌드 타임에 HTML을 만들어 엣지에 올려 두고, 일정 주기로 뒤에서 조용히 재생성한다. 개인화가 덜 필요한 페이지는 이제 프레임워크가 알아서 캐시 친화적으로 만들어 준다. Netflix가 수작업으로 달성했던 "엣지에서 멈추는 HTML" 이 도구 한 줄 설정으로 가능해진 시점이다.

2020, React Server Components 발표. Dan Abramov와 Lauren Tan이 공개한 RFC는 한 문장으로 요약된다. "클라이언트 번들에 포함되지 않는 React 컴포넌트." 2017년 Netflix가 한 장면을 위해 손으로 잘라냈던 경계를, React라는 언어 안에 공식 문법으로 집어넣는 일이었다.

2022, React 18 Streaming SSR. 하이드레이션을 전제로 한 SSR의 병목, 그러니까 "전체가 준비될 때까지 기다리는 시간" 을 쪼갰다. 준비된 부분부터 HTML을 흘려보내고, 느린 부분만 나중에 메운다. 페이지 전체를 "하이드레이션 가능한 하나의 덩어리" 로 보던 관점이 처음으로 부서졌다.

2023, Next.js App Router. RSC가 디폴트가 됐다. 클라이언트 컴포넌트는 이제 "use client" 를 맨 위에 찍어야 쓰는 옵트인 이다. 이 순서의 역전이 중요하다. 2017년까지는 "기본은 클라이언트 React, 필요하면 서버로 내린다" 였다면, 2023년부터는 "기본은 서버, 필요하면 클라이언트로 올린다" 로 바뀌었다. Netflix가 페이지 수준에서 내린 결정이, 이제 컴포넌트 수준의 디폴트로 내려왔다.

2024, Next.js Partial Prerendering. 한 페이지 안에서 정적 셸과 동적 홀을 자동으로 분리한다. 헤더·풋터·변하지 않는 콘텐츠는 엣지에서 캐시되고, 개인화된 부분만 런타임에 스트리밍으로 채워진다. 2017년 Netflix가 "페이지 자체를 캐시 가능하게" 만들었다면, PPR은 "페이지 안쪽을 조각별로 캐시 가능하게" 만드는 일반화다.

같은 방향의 다른 각도들도 같이 걸어왔다. Astro의 아일랜드 아키텍처, Qwik의 resumability, Remix의 로더·액션 모델. 전부 한 질문을 공유한다. "어디까지가 서버의 일이고, 어디부터가 클라이언트의 일인가."

이 질문은 2017년 Netflix 엔지니어 한 명이 랜딩 페이지 앞에서 던진 질문과 글자 그대로 같다. 다만 그때는 대답을 사람이 손으로 했고, 지금은 프레임워크가 디폴트로 준다.

그래서 2026년의 우리는

지금 Next.js로 SSR 페이지를 만들 때, 우리는 2017년 Netflix가 손으로 한 일을 프레임워크의 디폴트로 받는다. RSC를 기본으로 쓰고, 인터랙션이 필요한 부분만 "use client" 로 찍는다. 정적 셸은 엣지가 가져가고, 개인화된 조각만 런타임에 채워진다. 이건 몇 년 전이라면 아키텍처 회의 반나절짜리 결정이었다. 지금은 파일 맨 위 한 줄로 표현된다.

그런데 바로 이 지점에서 역설이 생긴다. 디폴트가 "가벼운 쪽" 으로 바뀐 시대에도, 프로젝트는 여전히 무거워진다.

왜인가. 프레임워크는 도구를 바꿨지만, 손에 밴 습관까지 바꾸지는 못하기 때문이다. App Router 프로젝트를 열어 보면 "use client" 가 상단에 붙은 파일이 절반을 훌쩍 넘는 경우가 흔하다. 서버 컴포넌트를 쓰라고 프레임워크가 길을 깔아 놨어도, 사람은 익숙한 useState로 먼저 손이 간다. PPR은 나왔지만 실제로 켜고 쓰는 팀은 드물다. 기본값이 바뀌었다고 해서 코드가 저절로 가벼워지지는 않는다.

2017년의 Netflix가 내린 결정을 한 줄로 다시 쓰면 이렇게 된다. "이 페이지에 클라이언트 React는 필요하지 않았다. 그래서 떼어냈다." 이 문장은 2026년에도 문법 그대로 유효하다. 다만 대상이 바뀌었을 뿐이다. 이제 우리가 앞에서 묻는 건 "이 컴포넌트에 use client가 필요한가", "이 데이터 패칭이 정말 런타임이어야 하는가", "이 상태가 캐시 키를 쪼개고 있지 않은가" 같은 것들이다.

도구는 문제에 종속된다

여기까지 왔으니 이 글을 쓴 진짜 이유를 꺼낼 수 있다.

React는 나쁘지 않다. 다만 어떤 문제에는 과잉이었다.

"좋은 도구"라는 말에는 늘 "어떤 문제에"가 숨어 있다. 같은 도구가 어떤 문제에선 정답이고, 어떤 문제에선 부담이다. Netflix의 랜딩 페이지가 클라이언트 React에 맞지 않았던 건 React가 뭘 잘못해서가 아니다. 그 페이지가 React가 잘하는 것을 필요로 하지 않았기 때문이다.

프레임워크 선택은 종교가 아니라, 문제를 먼저 정의한 뒤에 풀려야 하는 질문이다. 나는 이걸 머리로는 오래전부터 알고 있었다. 그런데도 현업에서는 자주 순서를 거꾸로 밟는다. React부터 고르고 난 뒤에 "이걸로 어떻게 풀지"를 고민한다. 프레임워크가 RSC 기반으로 바뀐 지금도 마찬가지다. App Router를 먼저 고르고, 그다음에 "왜 굳이 RSC여야 하는지"를 되짚지 않는다. 디폴트만 바뀌었을 뿐, 순서를 거꾸로 밟는 습관은 그대로다.

추상화의 비용은 여전히 캐시에서 드러난다

React 컴포넌트를 쓸 때 우리는 비용을 거의 느끼지 않는다. 리렌더가 좀 자주 일어나네, 번들이 좀 커졌네. 이 정도가 체감의 전부다. 그런데 추상화의 진짜 대가는 여기서 치러지지 않는다. 엣지에서, 캐시에서, 트래픽 곡선에서 치러진다.

하이드레이션은 공짜가 아니다. 개인화는 캐시와 싸운다. 상태는 편리하지만 캐시 키를 쪼갠다. 이 세 문장은 어떤 프레임워크를 써도 피할 수 없다. RSC와 PPR은 이 세 문장의 비용을 더 잘 드러내 주는 구조일 뿐, 없애 주지는 않는다. 서버 컴포넌트를 쓴다고 해서 상태가 캐시 키를 쪼개지 않는 건 아니다. 다만 프레임워크가 그 경계를 파일 상단의 지시자로 가시화해 준다는 점이 다르다.

문제는 이 비용이 코드에서는 여전히 잘 보이지 않는다 는 점이다. useState 한 줄 추가한 대가가 엣지에서 얼마나 큰지, 에디터는 알려주지 않는다. 그래서 우리는 추상화를 과소비한다. 대가가 아직 청구되지 않았기 때문이다.

캐시를 중심에 놓고 아키텍처를 다시 보면 많은 게 달라 보인다. 모든 상태는 "이건 캐시를 쪼개는가?" 앞에 한 번 선다. 모든 SSR 페이지는 "이건 엣지에서 멈추는가, 오리진까지 가는가?" 앞에 한 번 선다. 이 질문들은 프레임워크가 대신 물어 주지 않는다. 오히려 좋은 추상화일수록 이 질문들을 잘 안 보이게 감싸도록 설계돼 있다.

좋은 추상화일수록 감추는 게 많다. 그래서 좋은 추상화일수록 위험하다 . 감춰진 비용은 어느 순간 한꺼번에 돌아온다. Netflix가 2017년에 걷어낸 건 React가 아니라, 오랫동안 자기도 모르게 쌓아 온 그 감춰진 비용이었다. 2026년의 우리가 걷어내야 할 것도 정확히 같은 종류다. 이름만 바뀌었다.

빼는 것이 여전히 가장 어렵다

엔지니어링에서 쌓는 결정은 비교적 쉽다 . 라이브러리 하나 추가하는 PR은 리뷰 몇 줄로 머지된다. 왜 추가하는지만 그럴듯하면 대체로 통과한다.

걷어내는 결정은 어렵다. 왜 빼는지, 빼도 안전한지, 뺀 뒤에 뭐가 대체하는지, 기존 코드가 이걸 얼마나 의존하는지. 증명해야 할 것이 훨씬 많다. 걷어낸 뒤의 이득은 대개 보이지 않는 형태로 온다. 잃지 않은 성능, 피한 장애, 쪼개지지 않은 캐시. 빼서 얻은 것들은 숫자로 잘 드러나지 않는다.

프레임워크가 빼기를 쉽게 만들어 줬다는 오해가 있다. "use client" 안 찍으면 되잖아, PPR 켜면 되잖아. 하지만 실제 프로젝트에서 빼는 일은 옵션 하나 끄는 게 아니다. 이미 그 위에 올라탄 코드 수천 줄을 되돌리는 일이다. 2017년 Netflix가 어려웠던 건 기술적 난이도가 아니라, 이미 잘 돌아가는 코드를 떼어내자 고 조직 안에서 말을 꺼내는 일이었다. 그 난이도는 2026년에도 전혀 줄지 않았다.

Netflix가 클라이언트 React를 걷어낸 결정이 내게 인상 깊었던 건 성능 수치 때문이 아니었다. 큰 조직에서 누군가 "이거 떼어내자"라고 말했고, 그 말이 실제로 실행됐다는 사실 자체가 인상 깊었다. 이건 기술 결정이기 전에 문화 결정이다. 기본값을 의심하는 말이 자연스럽게 나올 수 있는 조직이라는 뜻이다.

나는 요즘 내 코드를 볼 때, 뭘 추가했느냐보다 뭘 지웠느냐 를 먼저 본다. 한 주 동안 커밋에서 -+보다 많았다면 대체로 좋은 주였다. 이 습관은 프레임워크 선택에도 그대로 올라간다. 이 추상화는 내 문제를 풀고 있는가, 아니면 내 문제를 가리고 있는가.

마치며

2017년 Netflix의 교훈이 낡았다는 말은 반만 맞다. 그 구체적 해법(바닐라 JS 300줄, 손으로 만든 캐시 친화적 HTML)은 이제 필요 없다. 프레임워크가 같은 효과를 디폴트로 준다. 하지만 그 해법을 낳은 질문 은 여전히 유효하다. "이 페이지에 이 도구가 정말 필요한가" 는 매 프로젝트마다 새로 물어야 하는 질문이다.

Next.js가 ISR에서 App Router, PPR로 걸어온 길은 결국 Netflix가 랜딩 페이지 앞에서 던졌던 그 질문을 업계 전체가 공유하게 된 과정이다. 프레임워크는 답을 계속 갱신한다. ISR이 있었고, RSC가 왔고, PPR이 왔고, 내년엔 또 다른 이름이 올 것이다. 그런데 질문은 갱신되지 않는다. 어디까지가 서버의 일이고, 어디부터가 클라이언트의 일인지. 이 페이지는 캐시되어야 하는지, 매번 새로 그려져야 하는지. 이 컴포넌트는 클라이언트로 내려보낼 가치가 있는지.

과거에서 배운다는 건 2017년 Netflix의 코드를 흉내 내자는 게 아니다. 그때 그 사람들이 던졌던 질문을 지금 내 코드 앞에서 다시 던져 보자는 뜻이다. 도구는 바뀐다. 질문을 갱신하는 건 여전히 사람의 일이다.

"좋은 엔지니어링은 무엇을 더할지보다, 무엇을 더하지 않을지를 아는 것이다."

Netflix의 결정은 내게 그 문장의 가장 큰 실전 예시였다. 그리고 10년 가까이 지난 지금도 이 예시가 낡지 않는 건, 그게 정답이어서가 아니라 그게 질문 이어서다.

참고 자료