Progressive JSON
(overreacted.io)- 점진적 JPEG처럼, JSON 데이터도 불완전한 상태로 먼저 전송하여 클라이언트가 점차 내용 전체를 활용할 수 있는 방식 소개
- 기존 JSON 파싱 방식은 전체 데이터가 완전히 수신되기 전까지 아무런 작업이 불가능한 비효율성 문제 있음
- Breadth-first 방식으로 데이터를 여러 청크(부분)로 구분하여, 아직 준비되지 않은 부분은 Promise로 표시하고 준비되는 대로 점진적으로 채워, 클라이언트가 미완성 데이터도 활용 가능함
- 이 개념은 React Server Components(RSC) 의 핵심 혁신이며,
<Suspense>
를 통해 의도된 단계별 로딩 상태를 제어함 - 데이터 스트리밍과 의도적 UI 로딩 흐름을 분리하여 더욱 유연한 사용자 경험 제공 가능
점진적(Progressive) JPEG과 점진적 JSON의 아이디어
- 점진적 JPEG는 이미지를 위에서 아래로 한 번에 불러오는 대신, 흐릿한 상태로 전체를 먼저 보여주고 점차 선명해지는 방식임
- 이와 유사하게, JSON 전송에도 점진적 방식을 적용함으로써 전체가 완성될 때까지 기다리지 않고 일부 데이터를 즉시 사용할 수 있음
- 예시 JSON 데이터 구조에서, 일반적 방식은 마지막 바이트까지 모두 받아야지만 파싱이 가능함
- 이로 인해 클라이언트는 서버의 느린 부분(예: 느린 DB에서 comments 불러옴)까지 모두 전송될 때까지 대기해야 하며, 이는 매우 비효율적인 현재의 표준임
스트리밍 JSON 파서의 한계
- 스트리밍 JSON 파서를 도입하면 불완전한(중간) 데이터 객체 트리를 생성 가능
- 하지만, 각 객체의 필드(예: footer, 여러 개의 comment 목록 등)가 일부만 전달되는 경우 타입이 불일치하고, 완성 여부 파악이 어려워 활용도 저하 문제 발생
- HTML의 스트리밍 렌더링과 유사하게, 순서대로 스트림 처리 시 하나의 느린 부분이 전체 결과를 지연시키는 문제가 동일하게 발생함
- 일반적으로 스트리밍 JSON의 활용이 드문 이유임
Progressive JSON 구조 제안
- 기존 방식의 깊이 우선 스트리밍(즉, 트리구조 하위까지 내부 순회 전송하는 방식)이 아닌, Breadth-first(너비 우선) 방식 도입
- 최상위 객체만 먼저 전송하고, 하위 값들은 Promise와 같은 플레이스홀더로 두고 준비되는 대로 한 번에 각각의 청크로 채워나감
- 예를 들어 서버가 비동기로 데이터 로딩이 끝날 때마다 대응하는 청크를 전송, 클라이언트는 준비된 만큼만 활용 가능
- 비동기적 데이터 수신(이른 로드) 이 가능해지고, 여러 느린 부분이 모두 처리가 끝날 때까지 전체 대기하지 않아도 됨
- 클라이언트를 청크별 비순차 및 부분 순차 수신에 강하게 구성하면, 서버는 다양한 청크 분할 전략을 유연하게 적용 가능
Inlining과 Outlining: 효율적 데이터 전송
- 점진적 JSON 스트리밍 포맷은 재사용 객체(예: 동일 userInfo를 여러 군데에서 참조)도 중복 저장 없이 한 개의 청크로 따로 추출하여 각 위치에서 동일 참조 가능
- 느린 부분만 분리해 placeholder로 전송하고, 나머지는 바로 채워 효율적 데이터 스트림 구현
- 동일한 객체가 여러 번 등장하는 경우, 한 번만 전송하고 재활용(Outlining) 가능
- 이러한 방식으로 순환 참조(객체가 자기 자신을 참조하는 구조) 도 일반 JSON처럼 곤란하지 않고 청크 내 간접 참조 구조로 자연스럽게 직렬화 가능
React Server Components(RSC)의 점진적 스트리밍 구현
- 실제 React Server Components는 점진적 JSON 스트리밍 모델을 적용한 대표적 예시임
- 서버가 외부 데이터(예: Post, Comments)를 비동기로 불러오는 구조를 사용
- 클라이언트에선 아직 도착하지 않은 부분을 Promise로 두고, 준비되는 순서대로 점진적 UI 렌더링
-
React의
<Suspense>
로 의도적인 로딩 상태를 설정-
사용자 경험상 불필요한 화면 점프 방지를 위해, Promise 상태(구멍)를 바로 보여주지 않고,
<Suspense>
fallback으로 단계별 로딩 연출 가능 - 데이터가 빠르게 도착해도 실제 UI는 설계된 단계에 맞춰 점진적으로 노출되게 개발자가 제어 가능
-
사용자 경험상 불필요한 화면 점프 방지를 위해, Promise 상태(구멍)를 바로 보여주지 않고,
요약 및 시사점
- React Server Components의 핵심 혁신은 컴포넌트 트리 속성(props)을 외곽부터 점진적으로 스트리밍하는 방식에 있음
- 따라서, 서버가 완전히 모든 데이터를 준비할 때까지 기다릴 필요 없이, 주요 부분부터 점차 보여주며 로딩 대기 상태도 세밀하게 제어 가능
-
스트리밍 자체뿐 아니라, 이를 활용하는 프로그래밍 모델(React의
<Suspense>
) 같은 구조적 지원이 함께 필요함 - 이를 통해, 느린 데이터 한 부분이 전체를 지연시키는 문제 등 기존 전송 방식의 병목 현상 완화 가능
Hacker News 의견
- 일부 사람들이 이 글을 너무 글자 그대로 받아들이는 경향이 보이는데, Dan Abramov가 Progressive JSON이라는 새로운 포맷을 제안하고 있는 건 아니라는 설명
- 이 글은 React Server Components의 아이디어와, 컴포넌트 트리를 자바스크립트 객체 형태로 표현한 뒤 이를 스트림 형태로 전송하는 과정을 설명하는 내용
- 이 방식은 React 컴포넌트 트리에 ‘구멍’을 둘 수 있게 해서, 처음에는 로딩 상태나 스켈레톤 UI를 보여주고, 서버에서 실제 데이터를 받을 때 해당 부분을 완전히 렌더링하는 구조 가능
- 이렇게 하면 더 세밀한 단계에서 로딩 표시와 빠른 첫 화면 표시가 가능
- 내가 생각하기에 사람들이 이 아이디어를 확장해서 다른 방법에 적용하는 것도 괜찮다고 봄
- RSC의 데이터를 직렬화하는 방식을 React에만 국한하지 않고, 더 일반적인 패턴으로 설명하려고 했던 의도
- React Server Components에서 발견한 여러 아이디어가 다른 기술이나 생태계에도 녹아들었으면 하는 바람
- 나는 progressive loading 방식, 특히 콘텐츠가 계속 움직이는(점프하는) 경험이 썩 마음에 들지 않음
- 로딩 중에 텅 빈 상태 UI를 보여주는 패턴이 특히 신경 쓰임
- 얼마 전까지 Ember를 썼을 때도 비슷한 방식이 있었고, Ajax 엔드포인트 작성이 매우 고통스러웠던 기억
- 트리 구조 재배치로 일부 자식 요소가 파일 끝에 위치하도록 해서 DAG(비순환 그래프) 처리를 효율적으로 하려던 의도였던 듯
- SAX 스타일의 스트리밍 파서를 쓰면 데이터가 부분적으로 도착할 때 페인팅을 먼저 시작할 수 있음
- 하지만 단일 스레드 VM에서 작업 순서를 잘못 설계하면 오히려 문제만 커지는 위험 존재
- 나는 이미 AI 툴과의 연결에서 스트리밍 partial JSON(Progressive JSON) 방식을 실제로 사용 중
- 이 방식은 RSC뿐 아니라 다양한 곳에서 활용되고 실무적으로도 클라이언트, 서버 모두에 가치 있는 방법이라는 실제 경험
- Dan의 "2 computers" 발표와 최근 RSC 관련 포스팅을 다 챙겨봤음
- Dan은 React 생태계에서 최고의 설명가이지만, 기술을 이렇게까지 어렵게 설명해야 한다면
- 진짜로 필요 없는 기술이거나
- 추상화에 문제가 있는 것
- 대다수 프론트엔드 개발자가 여전히 RSC 개념을 완전히 이해하지 못함
- Vercel은 Next.js를 기본 React 프레임워크로 만들었고, RSC 채택도 이 바람을 타고 확산
- Next.js를 쓰는 사람조차 Server Component 경계를 명확히 이해하지 못하고 일종의 ‘카고 컬트’식 채택이 많음
- React가 Vite 관련 PR을 받아들이지 않은 것도 의심스러움. RSC 푸시는 실질적으로 유저나 디벨로퍼를 위한 것이 아니라 플랫폼 업체의 호스팅 플랫폼 판매 전략일지도 모른다는 생각
- 되짚어 보면, Vercel이 React 오리지널 팀을 대거 영입한 것도 React 미래를 주도하려는 의도처럼 보임
- 역사적 동기나 배경에 대한 판단이 틀렸다는 지적과 함께, Vite 지원 관련 현황에 대해서 설명
- Vite 통합은 DEV 환경에서 번들링이 필요하다는 기술적인 제약 때문에 현재 Vite 팀이 개선 진행 중이라는 언급
- 관련 작업 진행 상황: https://github.com/facebook/react/pull/33152
- 사람들이 RSC를 이해하지 못한다는 논지는 논리적으로 순환적인 주장이라는 견해
- RSC를 싫어할 수도 있지만, 그 안에는 다른 기술에 차용할 만한 흥미로운 아이디어도 충분히 있음
- 설득보다는, 신기하고 쓸모 있는 부분을 각자가 취해갔으면 하는 바람
- Dan은 React 생태계에서 최고의 설명가이지만, 기술을 이렇게까지 어렵게 설명해야 한다면
- 물론 여전히 SPA를 정적 사이트로 만들어서 CDN에 올릴 수도 있고, Next.js도 “다이나믹” 모드로 셀프 호스팅이 가능
- 다만 Next.js의 서버리스 렌더링 전체 기능을 완전하게 Vercel 말고 다른 곳에 구현하기가 어려운 현실 존재(undocumented “magic” 때문)
- 이 문제 역시 다중 플랫폼에서 일관된 API 제공을 위해 어댑터 도입을 공식적으로 제안한 상황: https://github.com/vercel/next.js/discussions/77740
- 나는 RSC 푸시가 기업 이익 때문이라기보다는, 기존 웹사이트 빌드 패턴(SSR + 클라이언트 약간의 progressive enhancement)이 실제로 많은 장점이 있다는 깨달음에서 비롯됐다는 입장
- SSR만으로도 비즈니스 로직이 불필요하게 클라이언트로 많이 옮겨가는 문제 존재
- RSC 자체는 흥미로운 기술이지만 실전에서는 그다지 합리적이지 않다는 생각
- 복잡한 컴포넌트를 렌더링하기 위해 Node/Bun 백엔드 서버를 대규모로 유지하는 부담 존재
- 차라리 정적 페이지나 React SPA + Go API 서버 조합이 훨씬 효율적
- 비슷한 결과를 훨씬 작은 리소스로 만들 수 있음
- 새로운 기술의 설명이 복잡하다고 꼭 필요 없는 기술이나 잘못된 추상화라고 단정하기 어렵고, 복잡성을 감수할 만한 가치가 있는 문제도 존재
- 앞으로 어떻게 이 기술이 진화할지 지켜보는 관점
- RSC의 코드 구조를 활용해 작은 조각으로 HTML/CSS/JS를 나누는 정적 페이지 빌드도 가능하다는 생각
- 글에서 제안한 ‘$1’ 플레이스홀더를 URI로 치환해도 서버가 필요 없을 수 있음(대부분 동적 SSR이 반드시 필요한 건 아님)
- 단점은, 이런 방식은 콘텐츠가 변경될 때 업데이트 파이프라인(특히 S3에 컴파일된 스태틱 사이트의 스트리밍 배포)에 속도 확보가 중요
- 예를 들어 많은 기사가 프리랜더링되어 있는 신문 사이트처럼, 콘텐츠 일부만 바뀔 때 효율적으로 해당 부분만 다시 빌드하는 등 스마트한 콘텐츠 diff 처리 필요
- 실무에서 성능 최적화라며 밀리세컨드 단위로 프론트엔드에서 여러 MB 데이터를 불러오고 복잡한 로직을 처리하는 반면, 실제로는 BFF나 아키텍처 개선, 더 Lean한 API 구축이 훨씬 생산적인 솔루션이라는 깨달음
- GraphQL, http2 등을 통한 시도가 있었으나 결국 본질적인 문제 해결은 아니었고, 웹 표준의 진화 없이는 패러다임 변화 없을 것이라는 의견
- 새로운 프레임워크 역시 이 한계는 동일
- RSC는 글 말미에서 설명되는 것처럼 본질적으로 BFF(Backend for Frontend) 역할이라는 설명
- API 로직을 컴포넌트화한 구조
- 내 긴 글 참고: https://overreacted.io/jsx-over-the-wire/ (BFF는 첫 섹션 중간 참고)
- "페이지 로딩 ms 단축"의 의미에 따라 다르다는 의견
- Time to first render, time to visually complete를 최적화한다면, 비어있는 skeleton UI를 먼저 보내고 API로 데이터 받아서 hydrate하는 방식이 체감상 가장 빠름
- 반대로 time to first input, time to interactive을 빠르게 하려면 유저 데이터를 바로 렌더링해 줄 수 있어야 하고, 이 경우 백엔드가 훨씬 유리(네트워크 호출 최소화)
- 대부분의 경우 유저는 이쪽을 더 선호하고, CRUD SaaS 앱은 서버 측 렌더링이, Figma처럼 디자인이 중요한 앱은 클라이언트에서 정적 데이터+추가 데이터 fetch가 적합
- "모든 문제에 통하는 한 가지 솔루션"이란 없고, 최적화 지점은 주관적 선택
- 개발 경험, 팀 구조 등 기술 선택에 영향을 주는 다양한 요소 존재
- 덕분에 내가 왜 Facebook 로딩할 때 핵심 콘텐츠가 늘 마지막에 렌더되는지 이해하게 됨
- 여기서 말하는 BFF가 뭔지 궁금하다는 질문 등장
- 약어가 너무 많아서 FE와 BFF가 뭔지 궁금해하는 반응
- Progressive JSON 아이디어를 직접 쓰고 싶진 않고, 대안이 여러 개 존재한다고 생각
- 가장 간단한 해결책은 거대한 하나의 JSON 객체를 여러 개로 분할, 즉 ‘JSON lines’로 전송
- 헤더 정보는 한 번, 거대한 배열은 한 줄씩 전송해서 스트림 처리 효율화
- 객체가 더 크면 이 방식을 재귀적으로 적용할 수도 있지만, 너무 복잡해질 수 있음
- 속성의 순서를 서버가 명시적으로 보장해서 progressive parsing과 분리도 가능
- 결국 정말 대용량 구조에서는 유용하지 않겠지만, 가장 흔한 대규모 JSON 다루는 상황에선 꽤 실용적인 도구
- 명시적으로 구멍(holes)을 표시하지 않아도 스트리밍 메시지를 순차로 전송하며 델타(diff)만 보내는 방식이 가능
- ‘Mendoza’라는 델타 포맷을 사용하면 Go, JS/Typescript에서 아주 컴팩트하게 패치(diffs) 전송 가능: https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
- zstd의 바이너리 디프 방식이나 Mendoza처럼, 직렬화 데이터의 일부만 메모리에 저장하여 효율적 패치 진행
- React 역시 차이점 비교나 변동사항만 주입하는 방식이 필요하기에 의미 있는 접근
- UI 데이터 스트리밍에서 비어 있는 배열이나 null만으로는 부족하고, 현재 어떤 데이터가 미도착(pending) 상태인지 별도 정보가 필요
- GraphQL 스트리밍 페이로드는 유효한 데이터 스키마와 미도착 정보, 그리고 이후 패치 처리라는 혼합 방식을 선택
- 어느 부분이 ‘구멍’인지 알아야 로딩 상태를 보여주기 쉬움
- 클라이언트에서 JSON을 progressively decode하려면 jsonriver라는 라이브러리를 소개: https://github.com/rictic/jsonriver
- 매우 간단한 API, 성능도 좋고 테스트도 충분함
- 스트리밍된 문자열 조각을 점점 더 완성도 높은 값으로 파싱해줌
- 최종 결과는 JSON.parse와 동일함을 보장
- 트리 데이터라면 어떤 구조에서도 적용 가능한 재밌는 방식이라는 의견
- 트리 데이터는 parent, type, data 벡터와 string table로 표현하면, 나머지는 모두 소수의 정수로 축소 가능
- string table과 type info를 헤더로 upfront 전송하고, parent, data 벡터 청크를 노드 단위로 스트리밍
- depth-first/breadth-first 스트리밍은 청크 순서만 바꿔도 충분
- 네트워크 상의 앱에서 로드 시간 UX를 많이 개선할 수 있을 것 같음
- 테이블과 노드 청크를 번갈아 전송하며 트리를 어떤 순서로든 웹에 시각화할 수 있음
- preorder traversal과 depth 정보만 있으면 node id도 없이 트리 구조 복원 가능
- 이 아이디어로 작은 라이브러리 만드는 것도 가치 있는 시도
- 대부분의 앱은 이런 ‘정교한’ 로딩 시스템이 필요 없고, 정말 간단히 여러 번 API 호출로 대체 가능한 경우가 대부분이라는 주장
- 본인이 RSC wire 프로토콜 작동 방식을 설명하려던 것뿐이며, 실제로 이런 걸 직접 구현하라고 권하는 의도는 아니라는 반론
- 다양한 툴 간 원리 파악은 결국 다양한 곳에서 아이디어를 차용하거나 리믹스하는 데 도움
- 여러 번 호출하는 전략이 n*n+1문제가 있다고 생각하지만, 객체를 OOP/ORM 스타일로 중첩해서 전송하는 대신, 주석의 경우처럼 평평하게 전송하는 방식도 가능
- 이럴 바엔 차라리 protobuf 등 타입이 명확한 엔드포인트 구성도 장점
- comments를 분리하면 2번 호출로 충분히 처리가능(페이지+글, 댓글 따로), 이렇게 하면 pre-render 최적화도 가능
- 옵션의 실제 구현 복잡도 자체를 너무 높이지 않고, 미리 정의된 좋은 툴이 있다면 일부러 딥 커스터마이즈할 필요성 적음
- 과도하게 복잡한 기능이 결국 사용자나 개발자에게 독이 될 수 있음을 인지해야 한다는 입장
- 640K도 충분하다는 얘기처럼, progressive/partial reads가 확실히 WASM 시대 UX 속도에 실질적으로 큰 역할을 할 수 있다고 생각
- protobuf 같은 바이너리 인코딩 방식에 partial read와 well-defined streaming이 붙으면 엔지니어 부담은 늘지만 결과 UX는 크게 성장할 가능성
- Progressive JPEG는 미디어 파일 특성상 필요하지만, Text/HTML에선 굳이 필요 없고, JS 번들이 커진 결과로 복잡성만 더해지는 자기모순적 상황이라는 견해
- 실제 느린 원인이 단순히 데이터의 ‘크기’ 때문만이 아니라는 점을 지적
- 서버 데이터 쿼리 자체에 시간이 오래 걸리거나, 네트워크가 느릴 때도 점진적 노출(progressive reveal)이 의미 있음
- 전체 데이터 완성까지 대기하는 대신, 적당한 타이밍에 로딩 UI 보여주는 의도적 단계별 렌더링이 실제로 사용자 경험에 이득
- 엔드포인트 분리 전략이 이미 다양한 장점(Head of line blocking 방지, 필터 옵션 향상, 라이브 업데이트, 독립적인 성능 개선 등) 갖춘 솔루션이라는 생각
- 애플리케이션을 문서 플랫폼(document platform)으로 다루려는 시도가 근본적인 문제라고 보는 입장
- 실제 애플리케이션은 ‘문서’처럼 작동하지 않고, 이 괴리를 해소하려 많은 부가 코드와 인프라가 필요해지는 상황
- 별도 엔드포인트 채택의 진짜 단점과 진화 방향에 대한 두 편의 긴 글로 보완 설명: https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
- 요약하면, 엔드포인트는 결국 서버/클라이언트 간 '공식' API 계약이되고, 점점 코드가 모듈화되면서 성능에 손해(워터폴 현상 등) 발생 가능
- 결정들을 서버에서 한 번에 처리(coalescing)가 성능과 구조적 유연성에서 더 나은 대안이 될 수 있음