diff --git "a/_posts/\352\270\260\354\210\240\353\270\224\353\241\234\352\267\270/devlog-6.md" "b/_posts/\352\270\260\354\210\240\353\270\224\353\241\234\352\267\270/devlog-6.md" new file mode 100644 index 0000000..4b520f5 --- /dev/null +++ "b/_posts/\352\270\260\354\210\240\353\270\224\353\241\234\352\267\270/devlog-6.md" @@ -0,0 +1,176 @@ +--- +title: "프론트엔드 번들링과 압축 전송 톺아보기" +excerpt: "Rendered? Gzip? 빌드 결과물이 CSR, SSR, RSC에서 사용자에게 도달하기까지의 흐름" +coverImage: "/assets/blog/posts/devlog/devlog-6/cover.png" +date: "2026-03-27T14:20:00" +ogImage: + url: "/assets/blog/posts/devlog/devlog-6/cover.png" +--- + +> 이글은 번들 분석 도구를 써보다가 깊게 조사하게된 번들링과 압축 전송에 관한 글입니다. + +## 번들 라이브러리를 써보다가 + +안녕하세요! 개발자 김영덕입니다. +  +사이드 프로젝트에 미사용 코드,라이브러리를 정리하다가 "번들 크기 전후 한번 비교 해볼까" 싶었습니다. 그래서 rollup-plugin-visualizer를 설치해서 번들을 시각화해봤는데요. 탭이 **Rendered**와 **Gzip** 두 개의 탭으로 나뉘어져 있었어요. 두 탭이 사용되는 시점이 궁금해서 검색해보다가 제대로 딥 다이브 하게된 내용을 소개합니다. + +![rendered, gzip 두가지의 탭을 제공하는 visualizer](/assets/blog/posts/devlog/devlog-6/1.png) + +### Rendered탭의 의미 + +pnpm run build를 실행하면 vite의 경우 dist 하위 폴더에 JS 파일들이 생성되는데여. 이 JS파일들이 **Rendered**를 지칭합니다. Vite가 소스코드를 트리쉐이킹, 미니파이, 청크분할 등의 최적화를 거쳐서 만든 **번들링 결과물**의 크기를 의미합니다. + +### Gzip탭의 의미 + +Rendered상태의 파일을 웹서버(Nginx) 또는 CDN(CloudFront, Vercel Edge)이 **Gzip 압축해서 전송**할 때의 크기를 말합니다. 이게 **Gzip** 크기입니다. (참고 : 요즘은 gzip말고도 brotli등 다양한 압축방식들이 있습니다.) + +### 간단한 최적화결과 + +![간단하게 chunk로 쪼갠 결과물](/assets/blog/posts/devlog/devlog-6/3.png) + +코드스플리팅과 라이브러리 청크분리 설정을 통해 새빨갛던 bundle 데이터를 쪼개주었습니다. 적용방법은 매우 간단하여 이를 후술 하지는 않겠습니다. (~~아쉬운건 색깔을 통한 심각도를 정확히 한번에 알수가없다. 보라색은 어느정도란 말인가?~~) + +## 궁금했던 점: 로컬 빌드 결과물과 Vercel에 올라간 파일은 다른건가? + +번들 분석을 하다보니 이런 궁금증이 생겼습니다. 로컬에서 pnpm run build를 통해 나온 결과물은 Rendered인데, Vercel에 배포된거는 Gzip을 사용한다? 그러면 **로컬에서 빌드한 것과 실제 배포된 것이 다른 용량의 결과물을 사용하는건가?** 생각이 들었습니다. +  +결론부터 말하면, **같은 결과물(Rendered), 같읍 압축(Gzip)을 동일한 플로우로 진행합니다.** 차이가 생기는 건 그냥 실행 환경입니다. 환경에 상관없이 사이트에 사용자가 접속하면 웹서버가 Rendered 파일을 Gzip으로 압축해서 보내주는건데, 이건 파일을 변환하는게 아니라 네트워크 전송 단계에서 압축하는 것입니다. 브라우저가 받으면 자동으로 압축을 풀고, 브라우저는 원래의 Rendered 파일을 실행합니다. + +## 번들링 구조를 보다보니 — 배포 환경별로 Gzip전송을 어디서 하는지 궁금해졌다. + +Rendered와 Gzip의 관계를 이해하고 나니, 보통 프론트엔드 배포방식에는 AWS EC2에 직접 서버를 띄우는 방식도 있고, S3 + CloudFront로 서버 없이 서빙하는 방식도 있고, Vercel이나 Netlify 같은 호스팅 플랫폼도 있는데요. 각각 어떤 식으로 Gzip 전송을 처리하는지 궁금해졌습니다. + +### EC2 방식: 서버를 직접 띄우는 구조 + +EC2는 **AWS가 제공하는 가상 컴퓨터 한 대**입니다. 서울 리전에 만들면 서울 데이터센터에 리눅스 서버가 하나 생기는거고, 거기에 직접 Nginx(웹서버)를 설치하고, 빌드결과물을 올려두는 구조입니다. +  +Nginx 설정에 gzip on; 이 있으면 전송 시 Gzip 압축을 하여 전송을 해주는 겁니다. +  +이때 EC2는 빈 리눅스 서버기때문에 Nginx같은 웹서버는 따로 설치를 해줘야 합니다. + +즉 EC2는 컴퓨터만 빌려주는거고, 그 위에 뭘 설치하고 어떻게 설정할지는 전부 개발자/인프라 담당자 몫입니다. +온프레미스도 가상화 여부만 다를 뿐, 서버 위에서 하는 작업(Nginx 설치, 설정 등)은 EC2와 동일합니다. + +### S3 + CloudFront 방식: 서버 없이 서빙하는 구조 + +CloudFront는 AWS가 **전 세계에 이미 깔아놓은 엣지 서버 네트워크**입니다. 개발자가 서버를 만드는게 아니라, AWS가 이미 만들어놓은 인프라를 이용하는 겁니다. + +이 구조에서는 **서버가 없습니다.** S3는 파일 저장소 역할만 하고, CloudFront가 사용자에게 파일을 전달합니다. + +### CloudFront(CDN)는 어떻게 동작하는가? + +1. 개발자가 dist/ 파일을 S3에 업로드 +2. CloudFront에 "이 S3를 써라" 세팅 +3. 유저가 접속하면: + + 첫 번째 유저 (캐시 없음) + 유저 → 가까운 엣지 서버 → "캐시 미스" → S3에서 가져옴 → 유저에게 전달 + 가까운 엣지 서버에 데이터 캐싱 + + 두 번째 유저 (캐시 있음) + 유저 → 가까운 엣지 서버 → "캐시 히트" → 바로 전달 (S3 안 거침) + +CloudFront 자체는 컴퓨터가 아니라 CDN Content Delivery Network서비스(캐시 + 전달 네트워크)라는 점입니다. 파일을 캐싱해서 빠르게 뿌려주는 역할만 합니다. Gzip/Brotli 압축도 CloudFront에서 해주는데, 기본값은 꺼져있고 압축 옵션을 켜줘야 한다고 합니다. + +## 실제로 Gzip 전송을 확인해봐야지 + +여기까지 이해하고 나니, 실제 서비스에서 정말 Gzip으로 전송되는지 직접 확인해보고 싶었습니다. 브라우저 개발자도구 Network 탭에서 확인할 수 있습니다. +  +네이버에 들어가 네트워크탭의 API통신 하나를 선택하여 Response Headers를 봤더니 + +![네이버 응답 헤더에서 Content-Encoding: gzip 확인](/assets/blog/posts/devlog/devlog-6/2.png) + +**Content-Encoding: gzip** 을 확인 할 수 있었습니다. +  +이런식의 gzip을통한 전송이 가능한 이유는 HTTP 프로토콜에 이런 약속이 있기 때문입니다: + +브라우저 → 요청 헤더: "Accept-Encoding: gzip" (나 gzip 풀 수 있어) +서버 → 응답 헤더: "Content-Encoding: gzip" (이거 gzip으로 보낼게) +브라우저 → 자동으로 압축 해제 후 JS를 파싱해서 실행 + +## 궁금한 케이스 총정리 + +### 1. 번들링 → 전송 → 렌더링 플로우 + +React 소스코드 (~500kB라 가정) +↓ [번들링] Vite가 트리쉐이킹 + 미니파이 + 청크 분할 +Rendered (~120kB) ← 빌드 결과물, dist/assets/\*.js +↓ 배포 시점에 웹서버/CDN에 세팅해놓음 +↓ +↓ [전송] 유저가 요청할 때마다 웹서버(Nginx) 또는 CDN(CloudFront, Vercel)이 gzip/brotli 압축 +Gzip (~30kB) ← 네트워크로 실제 전송되는 크기 +↓ [렌더링] 브라우저가 gzip 해제 → 다시 120kB → JS 파싱 → React 실행 → DOM 생성 → 화면에 그림 + +- **번들링**: 소스코드 → Rendered (개발자가 최적화할 수 있는 영역) +- **전송**: Rendered → Gzip (웹서버 또는 CDN이 유저 요청 시마다 담당) +- **렌더링**: Gzip 해제 → 브라우저가 JS를 파싱하고 실행 → DOM 생성 → 화면에 그리는 영역 + +### 1-2. 청크 분할 유무에 따른 플로우 + +이전의 예시들은 청크 분할을 제외한 예시였는데요 번들링 시 청크 분할(Code Splitting)을 설정해주면 dist하위폴더에 하나의 파일이 아니라 각각의 청크로 쪼갤 수 있습니다. 쪼개진 페이지는 React의 lazy기능을 통해 해당 소스코드에 접근할 때 그 청크만 다운로드하도록 지연 로딩 설정이 가능합니다. + +[청크 분할 안 했을 때] +유저가 로그인 페이지 접속 +→ index.js (120kB) 전체를 gzip 압축 → ~30kB 전송 → 느림 (모든 페이지 코드 포함) + +유저가 대시보드로 이동 +→ index.js가 이미 캐싱되어 있으므로 추가 다운로드 없음 → 빠름 + +유저가 어드민으로 이동 +→ index.js가 이미 캐싱되어 있으므로 추가 다운로드 없음 → 빠름 + +[청크 분할 했을 때] +유저가 로그인 페이지 접속 +→ index.js (40kB) + login.js (20kB)만 gzip 압축 → ~15kB 전송 → 빠름 (초기 로딩이 가벼움) + +유저가 대시보드로 이동 +→ dashboard.js (30kB)를 그때서야 gzip 압축 → ~8kB 전송 + +유저가 어드민으로 이동 +→ admin.js (30kB)를 그때서야 gzip 압축 → ~8kB 전송 + +한 번에 gzip 압축해서 보내는 양 자체가 줄어들게 되어 초기 로딩 속도가 크게 빨라집니다. FCP 지표에 영향 + +### 2. Next JS같은 서버사이드 렌더링, 리액트 서버 컴포넌트는 Gzip전송을 어떻게? + +위의 플로우는 React CSR 기준인데요 Next.js같은 SSR 프레임워크나 리액트 서버 컴포넌트(RSC)는 전송 방식이 어떻게 다를지 궁금해졌씁니다. + +**SSR (Server Side Rendering)** +유저가 페이지 요청 +→ Node.js 서버가 React를 실행해서 **완성된 HTML**을 만듦 +→ 이 HTML을 gzip 압축해서 전송 +→ 브라우저가 HTML을 받아 바로 화면을 그림 (JS 다운로드 전에도 화면이 보임) +→ 이후 JS 번들(Hydration용)을 gzip 압축해서 추가 전송 +→ JS가 로드되면 Hydration을 통해 인터랙션이 가능해짐 + +**RSC (React Server Components)** +RSC는 SSR과 gzip 전송 대상 자체가 다릅니다. SSR은 완성된 HTML을 gzip으로 보내지만, RSC는 **RSC Payload**라는 별도의 직렬화된 데이터를 gzip 압축해서 스트리밍합니다. + +유저가 페이지 요청 +→ Node.js 서버가 서버 컴포넌트를 실행 +→ 실행 결과를 **RSC Payload**(JSON과 유사한 직렬화 포맷)로 만듦 +→ 이 RSC Payload를 gzip 압축해서 스트리밍 전송 +→ 브라우저의 React가 RSC Payload를 받아 DOM을 구성 +→ 클라이언트 컴포넌트의 JS 번들만 별도로 gzip 압축해서 전송 + +서버 컴포넌트로 설정된 JS 코드는 클라이언트 번들에 포함되지 않기 때문에, JS로 만들어지는 번들 크기 자체가 줄어드는 효과도 있습니다. + +![RSC Payload 응답 헤더 — Content-Type: text/x-component, Content-Encoding: br](/assets/blog/posts/devlog/devlog-6/rsc_1.png) + +![RSC Payload 미리보기 — 직렬화된 React 컴포넌트 트리](/assets/blog/posts/devlog/devlog-6/rsc_2.png) + +실제로 제 블로그의 네트워크 탭을 확인해보면 Content-Type이 text/x-component(RSC Payload 전용 타입)이고, Content-Encoding이 br(Brotli)로 압축 전송되는 것을 확인할 수 있습니다. + +### 3. 프로젝트가 작을때는 압축해서 전송하는게 더 느릴 수도 있는거 아닌가? + +gzip 압축과정을 보면서 이런 생각이 들었습니다. 서울에서 부산까지 휴대폰 하나 배달한다 치면 이삿짐센터에 꽁꽁 싸서 보낼 필요가 있을까? 그냥 바로 출발하는 게 더 빠르지 않을까? + +맞습니다. 실제로 아주 작은 파일은 gzip 압축/해제 오버헤드가 전송 시간 절약보다 클 수 있습니다. 일반적으로 **1kB 미만**의 파일은 압축 효과가 거의 없거나 오히려 손해일 수 있습니다. gzip은 압축 메타데이터 자체가 수십 바이트를 차지하기 때문에, 원본이 작으면 압축 후 크기가 비슷하거나 오히려 커지는 경우도 있습니다. + +그래서 Nginx 같은 웹서버에서는 gzip_min_length 설정으로 특정 크기 이하의 파일은 압축하지 않도록 설정할 수 있습니다. 역시 압축설정 또한 상황에 맞게 쓰는 것이 중요하다는 내용입니다. + +## 마치며 + +사실 어느정도의 흐름만 알고 있었는데, AI에이전트시대에 들어가면서 제대로 아는것이 중요하다 생각하게 되어서 이번 기회에 한번 깊게 공부 해보았습니다. +  +번들 분석 도구의 탭 두 개에서 시작한 궁금증이 공부하다보니 계속 궁금한게 많아졌는데요, 좋은 프론트엔드 개발자가 되기 위해 앞으로도 딥하게 공부하고 글을 써보겠습니다~ diff --git a/public/assets/blog/posts/devlog/devlog-6/1.png b/public/assets/blog/posts/devlog/devlog-6/1.png new file mode 100644 index 0000000..28bf017 Binary files /dev/null and b/public/assets/blog/posts/devlog/devlog-6/1.png differ diff --git a/public/assets/blog/posts/devlog/devlog-6/2.png b/public/assets/blog/posts/devlog/devlog-6/2.png new file mode 100644 index 0000000..169837d Binary files /dev/null and b/public/assets/blog/posts/devlog/devlog-6/2.png differ diff --git a/public/assets/blog/posts/devlog/devlog-6/3.png b/public/assets/blog/posts/devlog/devlog-6/3.png new file mode 100644 index 0000000..b487227 Binary files /dev/null and b/public/assets/blog/posts/devlog/devlog-6/3.png differ diff --git a/public/assets/blog/posts/devlog/devlog-6/cover.png b/public/assets/blog/posts/devlog/devlog-6/cover.png new file mode 100644 index 0000000..1666d3c Binary files /dev/null and b/public/assets/blog/posts/devlog/devlog-6/cover.png differ diff --git a/public/assets/blog/posts/devlog/devlog-6/rsc_1.png b/public/assets/blog/posts/devlog/devlog-6/rsc_1.png new file mode 100644 index 0000000..f4c7a32 Binary files /dev/null and b/public/assets/blog/posts/devlog/devlog-6/rsc_1.png differ diff --git a/public/assets/blog/posts/devlog/devlog-6/rsc_2.png b/public/assets/blog/posts/devlog/devlog-6/rsc_2.png new file mode 100644 index 0000000..2900d11 Binary files /dev/null and b/public/assets/blog/posts/devlog/devlog-6/rsc_2.png differ