1_JxxD8ZeQZSMz8tmZGhuQGA.jpg

1. 들어가며

건설 현장 앱 ! 빠르고 신선하게 !

건설 현장에서 검측 데이터를 실시간으로 기록하고 관리하기 위해 모바일 앱을 개발하게 되었습니다. 현장 특성상 복잡한 데이터 입력 폼과 대시보드가 필요했고, 빠른 개발과 유지보수를 위해 일부 기능에 웹뷰를 적용했습니다.

1*ZDTYx82v7AuPJh1LZBEZhg.png

“웹뷰는 느릴 수밖에 없어요.”

“로딩이 너무 느린것 같아요.”

프로젝트 초기, 이런 피드백을 자주 받았습니다. 첫 화면이 뜨기까지 4.2초. 사용자들은 흰 화면을 보며 앱이 멈춘 건 아닌지 걱정했습니다. 특히 3G 환경이나 저사양 기기에서 테스트할 때마다 로딩이 지연되는 모습을 보면서 “정말 웹뷰의 한계인가?”라는 의구심이 들기도 했습니다.

하지만 4주간의 최적화 작업 끝에, 초기 로딩을 3.7초로 62% 단축했고, 재방문 시 거의 즉각적인 3ms 로딩을 달성했습니다.


2. 문제 정의

2.1 초기 측정 결과 (4G Slow 환경)

  • 페이지 진입시 네트워크 속도를 낮춰본 결과 아래와 같은 충격적인 수치를 맞이했습니다.
  • 페이지 로드 속도가 느렸기 때문입니다…
0ms ─────────────────────> 4,200ms
[HTML] [JS 다운로드...] [JS 파싱] [Fetch] [렌더링]
 300ms    1,800ms     1,067ms  900ms   133ms

초기 측정 결과, FCP는 2.9초, LCP는 4.2초로 매우 느린 상태였습니다. 또한 최대 번들 크기가 469KB로 과도하게 컸고, 재방문 로드 시간 역시 570ms로 느린 편이었습니다.

2.2 핵심 병목 구간 발견

js 다운로드의 시간이 너무 오래걸리기에 빌드 결과물을 확인해보기로 했습니다.

번들 분석 결과, 469KB 단일 파일에 모든 페이지와 라이브러리가 포함되어 있었습니다.

1*drzyjAlBHS4xzbNx2pO8Ww.png

라이브러리 Raw 크기 Gzip 크기 사용 위치 문제점 framer-motion 128 kB 43 kB 대시보드 ⚠️ 초기 페이지에서 미사용 zod 90 kB 24 kB 로그인 ⚠️ Dashboard 사용자는 불필요 date-fns 85 kB 25 kB 대시보드 ⚠️ 모든 페이지 로드 react-hook-form 65 kB 20 kB 로그인 ⚠️ Dashboard 사용자는 불필요

문제의 핵심:

framer-motion, zod, hook-form 등 특정 페이지에서만 사용하는 라이브러리까지 초기 로딩에 포함되어 있었습니다.

재방문 시에도 304 응답으로 570ms 소요되어습니다.

사용자의 95%는 Dashboard만 사용하지만, Admin 페이지용 라이브러리도 다운로드했습니다.

304 →

“파일이 바뀌지 않았으니, 기존 캐시를 그대로 써도 됩니다.”

라는 서버의 응답 코드입니다.


3. 번들링 최적화

우리 팀은 TanStack Router와 Vite를 기반으로 만들어진 프로젝트입니다. 따라서 라우터와 번들러의 기능을 적극적으로 활용해 개선하고자 했습니다.

3.1 자동 최적화의 한계

Vite에는 기본적으로 자동 번들 최적화 기능이 내장되어 있습니다.

특히 Vite 7 버전에서는 Rollup 기반의 빌드 파이프라인을 활용해, 아래와 같은 전략으로 코드를 효율적으로 분할합니다.

Vite 7의 기본 전략 설정이 없으면 모든 코드를 하나의 index.js로 번들링합니다

Vite/Rollup은 매우 똑똑한 자동 최적화를 제공합니다. 하지만 “자동”이 항상 “최적”을 의미하지는 않습니다.

우리 프로젝트는 웹뷰 전용/비전용 페이지가 공존하지만, Vite의 기본 전략은 이를 구분하지 않아 특정 페이지 전용 라이브러리까지 한 번에 번들링되는 비효율이 있었습니다.

3.2 수동 청크 분리

Vite/Rollup은 자동 코드 분할 기능을 제공하지만, 우리가 원하는 라우트별·사용자별 최적 구조를 정확히 반영하긴 어렵습니다.

  • Rollup이 내부적으로 일부 모듈을 자동 분리하기도 하지만, 이는 예측하기 어렵고 우리가 원하는 방식이 아닐 수 있습니다.
  • 여러 개의 청크로 명확하게 분리하려면 manualChunks로 명시적인 전략을 지시해야 합니다.

그래서 Vite는

manualChunks

옵션을 제공해 개발자가 청크 구조를 직접 설계할 수 있도록 하고 있습니다.

3.3 최대 청크 분리 전략

200300KB 분리 기준 이번 최적화에서는 단일 청크가 **200300KB(gzip 전 기준)**를 넘는 경우,해당 청크를 별도로 분리하는 것을 목표로 설정했습니다. 왜 더 작게 나누지 않았나? 코드 스플리팅은 양날의 검입니다. React의

lazy

Suspense

는 동적 import를 활용해 컴포넌트를 필요할 때 로드하는 강력한 기능이지만,

너무 작게 쪼개면 오히려 느려집니다

.

각 청크마다 HTTP 요청이 발생

청크 분리 전략

번들 분석 결과, 469KB 파일에 불필요하게 합쳐진 라이브러리들을 발견했습니다. 각 라이브러리의 사용 위치를 파악하여 필요시에만 로드되도록 분리했습니다.

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // "framer-motion은 무조건 별도 청크로 분리해"
          if (id.includes('framer-motion')) {
            return 'lib-framer-motion';
          }
// "zod는 Login에서만 쓰이니까 별도 청크로"
          if (id.includes('zod')) {
            return 'lib-zod';
          }
          // "react-hook-form도 Login에서만"
          if (id.includes('react-hook-form')) {
            return 'lib-react-hook-form';
          }
          // "React 코어는 항상 필요하니 vendor로"
          if (id.includes('node_modules')) {
            return 'vendor-react';
          }
        }
      }
    }
  }
});

분리 기준은 단순했습니다. 가장 큰 청크에서 특정 페이지에만 사용되는 라이브러리를 찾아 별도 청크로 분리했습니다.

BEFORE: 모든 node_modules가 하나로 합쳐진 모습

1*drzyjAlBHS4xzbNx2pO8Ww.png

AFTER: node_modules가 수동으로 분리된 모습

1*FBB75uaTRceZRF7KVivkvw.png

결과 간단 요약

  • 대시보드 페이지 초기 로딩 청크: 468KB → 173.5KB (-62.9%)
  • 로그인 페이지 접근 시에만 zod, hook-form 로드 ( 로그인은 앱 페이지이기 때문 )
  • 대시보드 사용자는 불필요한 라이브러리 다운로드 안 함

3.4 Barrel File 제거

TanStack Router는 자동 코드 분할을 지원하지만, 초기 구조에서는 이 기능이 제대로 작동하지 않았습니다. 현재 페이지와 관계없는 다른 페이지들까지 함께 로드되는 문제가 발생했습니다.

1_1XZ2xo8W3AyS8lBevbPKBg.webp

Barrel File이란? 여러 모듈을 하나의

index.ts

파일에서 재export하여 import 경로를 깔끔하게 만드는 패턴입니다. 예를 들어

import { A, B, C } from './components'

처럼 한 곳에서 여러 컴포넌트를 가져올 수 있어 편리하지만, 번들링 시 의도치 않은 부작용이 발생할 수 있습니다.

문제의 원인

// pages/index.ts (Barrel File)
export { Dashboard } from './Dashboard';
export { Gallery } from './Gallery';
export { Inspection } from './Inspection';
// App.tsx
import { Dashboard, Gallery, Inspection } from './pages';
// → 모든 페이지가 하나의 모듈로 묶여서 import됨

Barrel File을 통해 모든 페이지를 하나의 파일에서 재export하고, 이를 한 번에 import하면 번들러는 “이 페이지들이 모두 함께 필요하다”고 판단합니다. 결과적으로 모든 페이지 컴포넌트가 메인 번들에 포함되어 자동 코드 분할이 실패했습니다.

해결 방법

// Barrel File 제거, 각 페이지를 직접 import
import { Dashboard } from './pages/Dashboard';
import { Gallery } from './pages/Gallery';
import { Inspection } from './pages/Inspection';

// 또는 TanStack Router의 lazy loading 방식 사용
import { lazyRouteComponent } from '@tanstack/react-router'
{
  path: '/gallery',
  component: lazyRouteComponent(() => import('./pages/Gallery'))
}
// TanStack Router는 route module을 기준으로 자동 스플릿을 도와주긴 하지만
// ➡︎ Barrel File, 상위 import 구조에 의해 쉽게 실패
// 필요시 명시적으로 lazyRouteComponent를 사용하자

각 페이지를 개별적으로 import하거나 lazy를 사용하면, 번들러가 “이 페이지는 해당 라우트에서만 필요하다”고 정확히 인식하여 자동 코드 분할이 정상 작동합니다.

결과 간단 요약

  • 메인 번들에서 불필요한 페이지 코드 제거
  • 각 페이지가 독립적인 청크로 분리
  • 초기 로딩 시간 단축

BEFORE: 모든 페이지가 메인 번들에 포함

1_2plkoWJ63XEuUzSr0IGKgQ.webp

AFTER: 각 페이지가 독립적인 청크로 분리

1_xlf6DqpJuxZpjlyjRWZ4bA-1.webp

3.5 불필요한 의존성 제거

번들 크기 감소를 위해 다음과 같은 작업도 진행했습니다:

  • 레거시 의존성 제거: 더 이상 사용하지 않지만 package.json에 남아있던 패키지 정리
  • 사용하지 않는 의존성 제거: import는 했지만 실제로는 쓰지 않는 라이브러리 제거
  • 더 가벼운 대안 검토: 일부 무거운 라이브러리를 더 작은 대안으로 교체

4. Content Hash 기반 브라우저 캐싱 전략 최적화

번들 크기를 아무리 작게 최적화해도, 사용자가 페이지에 방문할 때마다 이 리소스를 새로 다운로드한다면 의미가 없습니다.

특히 React, framer-motion 같은 핵심 라이브러리는 코드 변경 빈도가 매우 낮은 ‘안정적인(stable)’ 파일입니다. 따라서 한 번 다운로드한 리소스는 브라우저 캐시에 저장해두고, 최대한 오랫동안 재사용하여 네트워크 요청 자체를 없애는 것이 로딩 성능 최적화의 핵심이었습니다.

이 문제를 해결하기 위해 Content Hash 기법을 사용했습니다.

Content Hash란? 파일의 ‘내용물’을 기반으로 고유한 해시(hash)값을 생성하고, 이 값을 파일명에 포함시키는 기법입니다. 파일 내용이 1비트(bit)라도 변경되면 완전히 다른 해시값이 생성되어 파일명이 바뀌게 됩니다. 이를 통해 브라우저는 파일명만 보고도 “이 파일이 내가 가진 파일과 동일한지, 아니면 새로운 파일인지”를 즉시 판단할 수 있습니다.

Before: 파일명이 고정되어 캐시 관리가 어려움
vendor-react.js
lib-framer-motion.js
index.js
After: 파일 내용이 바뀔 때만 파일명이 변경됨
vendor-react-KMzISR1B.js      # React 코어 (거의 안 바뀜)
lib-framer-motion-CLLfQHnF.js # 애니메이션 라이브러리 (거의 안 바뀜)
index-B64UZnn0.js             # 앱 코드 (자주 바뀜)

4.1. 문제점: 304 비용

Content Hash를 적용해도, Cache-Control 설정이 미흡하면 브라우저는 캐시를 100% 신뢰하지 못합니다.

  • [Before] 기본 캐싱의 동작:
  1. 브라우저:vendor-react-KMzISR1B.js 파일 가지고 있긴 한데… 혹시 그 사이에 바뀐 거 있나요?” (서버에 확인 요청)
  2. 서버: “아니요, 안 바뀌었어요. 쓰세요.” (304 Not Modified 응답)
  3. 브라우저: 캐시에서 로드.

이 시나리오의 문제는, 파일 다운로드는 안 했지만 캐시 유효성을 검증하기 위한 네트워크 왕복 시간이 발생한다는 것입니다. 이 RTT는 적게는 50ms에서 길게는 수백 ms까지 걸리며, 로딩 성능에 명백한 병목이 됩니다.

4.2 해결책: immutable을 통한 명시적 캐싱 전략

우리는 이 불필요한 네트워크 왕복조차 제거하기 위해, vercel.json에 캐싱 정책을 ‘명시적으로(Explicitly)’ 정의했습니다.

{
  "headers": [
    {
      // 1. 불변 리소스 (Assets)
      // 내용이 바뀌면 어차피 파일명이 바뀌는 모든 리소스
      // "1년(31536000초)간 절대 불변(immutable)하니, 다시는 확인 요청도 보내지 마라"
      "source": "/(assets|font|img)/:path*",
      "headers": [{
        "key": "Cache-Control",
        "value": "public, max-age=31536000, immutable"
      }]
    },
    {
      // 2. 진입점 (HTML)
      // 앱의 '껍데기' 역할을 하므로, 항상 최신 상태인지 서버에 확인
      "source": "/index.html",
      "headers": [{
        "key": "Cache-Control",
        "value": "public, max-age=0, must-revalidate"
      }]
    }
  ]
}

immutable 설정의 핵심은 서버 요청 자체를 원천 차단하는 것입니다.

  • [After] immutable 캐싱의 동작:
  1. 브라우저:vendor-react-KMzISR1B.js 파일은 immutable이군. 1년 동안은 절대 안 바뀔 테니, 서버에 물어볼 필요도 없이 그냥 캐시에서 바로 로드해야지.”
  2. 결과: 네트워크 요청 없음. 200 OK (from disk cache) (로드 시간 0~3ms)

4.3 기대 효과 및 결론

이 명시적 캐싱 전략을 통해 4가지 핵심 가치를 확보했습니다.

  1. 네트워크 RTT 제거: 304 확인 요청을 원천 차단하여, 캐시된 리소스를 네트워크 지연 없이 즉시 로드합니다.
  2. 안전한 캐시 재사용: React처럼 안정적인 라이브러리는 몇 주, 몇 달간 캐시를 재사용하여 재방문자에게 최고의 로딩 속도를 제공합니다.
  3. 코드 분할 시너지: 실제로 자주 변경되는 것은 index-B64UZnn0.js 같은 앱 코드뿐입니다. 코드 분할(Code Splitting)과 Content Hash가 결합되어, 사용자는 ‘정말 바뀐’ 코드 청크(chunk)만 다운로드하게 됩니다.
  4. 예측 가능성 및 협업: Vercel의 기본 정책에 의존하는 대신, 캐싱 정책을 vercel.json 코드로 관리(Infrastructure as Code)합니다. 이를 통해 Git으로 변경 이력을 추적하고, 팀원 누구나 캐시 전략을 명확히 파악할 수 있어 디버깅이 용이하고 예측 가능한 운영이 가능해집니다.

5. CSS 최적화

5.1 비동기 로드

CSS는 기본적으로 렌더링을 블로킹하는 리소스입니다. 초기에는 media="print" 트릭으로 이 문제를 해결하려 했습니다.

<!-- ❌ 초기 시도: media="print" 트릭 -->
<link rel="stylesheet" href="style.css"
      media="print"
      onload="this.media='all'">

브라우저가 “프린트용 CSS”로 인식해 렌더링을 계속 진행하고, 로드 완료 시 실제 스타일을 적용하는 방식이었습니다. 하지만 이 방법은 비표준적이고 브라우저나 크롤러에서 예측 불가능한 동작을 할 수 있어, 표준 방식인 rel="preload"로 수정했습니다.

<!-- ✅ 개선: rel="preload" 표준 방식 -->
<link rel="preload"
      href="style.css"
      as="style"
      onload="this.onload=null;this.rel='stylesheet'">
<noscript>
  <link rel="stylesheet" href="style.css">
</noscript>

동작 원리:

  1. rel="preload"로 CSS 파일을 우선 다운로드 (렌더링 블로킹 없음)
  2. 다운로드 완료 시 onload 핸들러가 rel="stylesheet"로 변경하여 스타일 적용
  3. JavaScript 비활성 환경을 위한 <noscript> 폴백 제공

하지만 이 방식만으로는 FOUC(스타일 없는 콘텐츠 깜빡임) 문제가 발생할 수 있어, Critical CSS를 인라인으로 삽입하는 2단계 전략을 사용했습니다.

5.2 Critical CSS 인라인화

<!DOCTYPE html>
<html>
<head>
  <!-- 1단계: Above-the-fold 영역의 필수 스타일 -->
  <style>
    /* Critical CSS - 초기 렌더링에 필요한 최소 스타일 */
    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    .header {
      background: #1a1a1a;
      height: 60px;
    }
    .hero {
      min-height: 100vh;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }
</style>
<!-- 2단계: 나머지 CSS 비동기 로드 -->
  <link rel="preload"
        href="main.css"
        as="style"
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript>
    <link rel="stylesheet" href="main.css">
  </noscript>
</head>
<body>
  <!-- 컨텐츠 -->
</body>
</html>

최적화 효과:

  • 초기 렌더링 시간 단축 (Critical CSS 즉시 적용)
  • 메인 CSS 파일이 렌더링을 블로킹하지 않음
  • FOUC 현상 방지
  • 표준 방식으로 SEO 및 접근성 개선

Critical CSS 추출 도구:

  • Critical — Penthouse 기반 자동 추출
  • PurgeCSS — 미사용 CSS 제거
  • Chrome DevTools Coverage 탭 — 실제 사용되는 CSS 분석

5.3 CSS 압축 실패 사례

LightningCSS와 cssnano를 시도했으나, Tailwind가 이미 최적화되어 있어 오히려 0.76KB 증가했습니다. 이미 최적화된 것을 재최적화하려다 역효과를 경험한 사례입니다.

# 시도했던 압축 도구들
npm install -D lightningcss
npm install -D cssnano
# 결과 Before: 45.2 KB After:  45.96 KB (+0.76 KB)

CSS 코드 분할 :cssCodeSplit: true 설정으로 청크별로 CSS를 분리했습니다. 크기는 0.34KB 증가해 제거했습니다.

// vite.config.ts export default
defineConfig({   build: {     cssCodeSplit: true, // 청크별 CSS 분리   } });

6. 폰트 최적화

6.1 Preload vs FOUT

font-display: swap은 빠른 FCP를 제공하지만, 폰트 교체 시 레이아웃이 밀리는 CLS(Cumulative Layout Shift) 문제를 유발합니다. 반면 폰트 Preload는 처음부터 올바른 폰트로 렌더링하여 CLS를 방지합니다.

FOUT (Flash of Unstyled Text) 웹폰트가

로드되기 전 → 시스템 폰트로 먼저 표시 → 나중에 웹폰트로 바뀌며 깜빡임

글자는 빨리 보임(좋음) / 전환 시 레이아웃 흔들림(CLS ↑)

FOIT (Flash of Invisible Text) 웹폰트가

로드될 때까지 글자를 숨김 → 로드 후 한 번에 표시

레이아웃 안정적 / 글자 안 보여서 지연 체감

6.2 font-display 전략 비교

1*owlbvQ7zVCiiyaHKzdxOMg.png

현대 웹 성능 지표는 단순한 FCP보다 안정적인 LCP를 더 중요하게 평가합니다. 핵심 폰트 1~2개만 Preload하고 font-display: optional을 적용하여 CLS를 완전히 방지했습니다.

효과:

  • CLS 점수 개선
  • 폰트 로딩으로 인한 레이아웃 변화 제거
  • 시각적 안정성 향상

7. 성능 모니터링

7.1 Sentry 성능 모니터링 적용

최적화 작업을 마무리하며, 개선된 성능이 실제 사용자 환경에서도

유지되는지 지속적으로 확인할 필요가 있었습니다.

특히 LCP가 4초를 초과하는 상황을 실시간으로 감지하여 선제적으로 대응하고자 Sentry 성능 모니터링을 도입했습니다.

7.2 모니터링 효과

Sentry를 통해 다음과 같은 이점을 얻을 수 있었습니다:

1*KhUOJ1PFAUgwuBK21UycXg.png

  • 실시간 알림: LCP가 O초를 초과하면 즉시 알림을 받아 문제를 조기에 인지
  • 선제적 대응: 사용자 체감 속도가 떨어지기 전에 원인을 파악하고 대응 가능
  • 지속적인 관리: 새로운 기능 배포나 환경 변화에도 성능 저하를 빠르게 감
  • 사용자 세그먼트 분석: 특정 디바이스나 네트워크 환경에서의 성능 문제 파악

최적화는 한 번에 끝나는 작업이 아니라, 지속적으로 모니터링하고 개선해야 하는 과정입니다.

Sentry 성능 모니터링은 이러한 선제적 대응 체계를 구축하는 핵심 도구가 되었습니다.


8. 최종 결과

8.1 성과 요약

1*BsianEi7mzXuDJD4SwBVFA.png

8.2 번들 크기 변화

1*Yyq-vb4iREfhWFaFce0Yqg.png

80줄의 코드 변경으로 얻은 결과입니다.

다른것 보다 재방문 페이지부터는 90% 이상 빠릅니다.

대부분의 사용자는 여러 페이지를 방문하니까, 전체적으로는 압도적인 개선이었습니다.

특히 재방문 페이지부터는 90% 이상 빠릅니다.


9. 핵심

1. 최대 청크에서 라이브러리 분리 → 캐시 효율 83% 향상

가장 큰 번들 파일을 열어보니 용도가 다른 라이브러리들이 하나로 뭉쳐있었습니다. 이걸 용도별로 분리하니 변경 빈도에 따라 독립적으로 캐싱할 수 있게 되었습니다.

2. Barrel File 제거 → 자동 코드 분할 활성화

편의를 위해 만들었던 index.ts 재수출 파일들이 오히려 번들러를 혼란스럽게 만들고 있었습니다. 제거하고 직접 import 하도록 바꾸니 번들러가 의존성을 정확히 파악해서 Tree-shaking과 코드 분할이 의도대로 작동했습니다.

3. Content Hash 기반 캐싱 → 재방문 시간 570ms→3ms 단축

파일 내용을 기반으로 해시를 생성하니, 바뀌지 않은 파일은 완벽하게 캐싱되었습니다. 재방문 사용자의 로딩 시간이 190배 빨라졌습니다.

4. Critical CSS 인라인화 → FOUC 제거

초기 렌더링에 필요한 최소한의 CSS만 HTML에 인라인으로 삽입하고, 나머지는 비동기로 로드했습니다. 사용자는 스타일 없는 화면을 보지 않게 되었습니다.

❌ 이미 최적화된 CSS 재압축

“더 압축하면 더 작아지겠지?” 싶어서 LightningCSS와 cssnano를 시도했는데, 오히려 0.76KB 늘어났습니다. 이미 충분히 최적화된 걸 건드리면 역효과가 날 수 있다는 걸 배웠습니다.

측정 없이 감으로 하는 최적화는 독입니다.

❌ Framer Motion LazyMotion 적용

번들 크기를 줄이려고 LazyMotion을 시도했는데, 우리가 사용하는 핵심 기능들이 빠져 있었습니다. 번들 크기만 보고 덤볐다가 낭패를 봤습니다. 기능 요구사항을 먼저 확인하는 게 중요함을 느꼈습니다.

측정 없이 진행하는 최적화는 결국 도박과 같습니다.

CSS를 추가 압축했다가 오히려 용량이 증가했던 경험을 통해 뼈저리게 깨달았습니다.

성능 개선은 반드시 현재 상태(as-is)를 정확히 측정한 뒤,

그 결과를 바탕으로 개선 작업(to-be) 을 수행해야 합니다.

또한, 빌드 도구의 자동 최적화 기능만 믿기보다는

내부 동작 방식과 구조를 이해해야 프로젝트 특성에 맞게 설정을 조정할 수 있습니다.


자동 최적화의 동작원리를 의심하고 의심을 바탕으로 개선을 해나가는 것!!참고 자료https://rollupjs.org/configuration-options/#output-manualchunkshttps://ko.vite.dev/config/build-optionshttps://ko.legacy.reactjs.org/docs/code-splitting.html#code-splittinghttps://ko.vite.dev/guide/whyhttps://medium.com/@iboroinyang01/bundle-up-vite-or-webpack-c260915e0ff7https://yong-nyong.tistory.com/100https://medium.com/@akashsdas_dev/code-splitting-in-react-w-vite-eae8a9c39f6ehttps://velog.io/@hanmw110/프론트엔드-번들러-완벽-가이드 https://dev.to/tassiofront/barrel-files-and-why-you-should-stop-using-them-now-bc4 https://toss.tech/article/smart-web-service-cachehttps://vercel.com/docs/headers/cache-control-headers