๐Ÿ’ก 25. ๋ฒˆ๋“ค ์ตœ์ ํ™” & ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…: ์‹ค์ „ ์„ฑ๋Šฅ ๊ฐœ์„ 

๐Ÿ“‹ ๊ฐœ์š”

์‚ฌ์šฉ์ž๊ฐ€ ์ตœ์ดˆ ๋ฐฉ๋ฌธ ์‹œ ๋‚ด๋ ค๋ฐ›๋Š” JavaScript ๋ฒˆ๋“ค์„ ์ตœ์†Œํ™”ํ•˜๋Š” ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ „๋žต, React.lazy์™€ ๋™์  import, ๊ทธ๋ฆฌ๊ณ  Bundle Analyzer๋กœ ๋ฒˆ๋“ค ํฌ๊ธฐ ์‹œ๊ฐํ™”๊นŒ์ง€. ์‹ค๋ฌด ์„ฑ๋Šฅ ๊ฐœ์„ ์˜ ์ „ ๊ณผ์ •์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 8๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
[๋ฒˆ๋“ค์ด ๋ญ”๊ฐ€] โ†’ [์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์›๋ฆฌ] โ†’ [React.lazy ์‹ค์Šต] โ†’ [๋ฒˆ๋“ค ๋ถ„์„] โ†’ [Tree Shaking] โ†’ [Web Vitals]

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • React.lazy + Suspense ๋กœ ๋ผ์šฐํŠธ/์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค
  • Bundle Analyzer ๋กœ ์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋ฒˆ๋“ค์„ ๋ฌด๊ฒ๊ฒŒ ๋งŒ๋“œ๋Š”์ง€ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค
  • Tree Shaking์ด ์™œ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”์ง€ ์›์ธ์„ ์ง„๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ˆ˜(PM): "์˜์ฒ  ๋‹˜, Lighthouse ์ ์ˆ˜๊ฐ€ 42์ ์ด์—์š”. ์ฒซ ํ™”๋ฉด ๋กœ๋”ฉ์ด 8์ดˆ๋‚˜ ๊ฑธ๋ฆฐ๋‹ค๊ณ  ์‚ฌ์šฉ์ž ๋ฆฌ๋ทฐ๊ฐ€ ํญ๋ฐœํ•˜๊ณ  ์žˆ์–ด์š”. ๊ฒฝ์Ÿ์‚ฌ๋Š” 2์ดˆ์ธ๋ฐ!"
  • ์˜์ฒ (์‹ ์ž…): "๋„ค? ๊ธฐ๋Šฅ์€ ๋‹ค ์ž˜ ๋Œ์•„๊ฐ€๋Š”๋ฐ ์™œ ๋А๋ฆฌ์ฃ ? ์ฝ”๋“œ๊ฐ€ ์–ด๋””์„œ ๋ง‰ํžˆ๋Š” ๊ฑด์ง€..."
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "๊ธฐ๋Šฅ์ด ์ž˜ ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ๊ณผ ๋น ๋ฅด๊ฒŒ ๋กœ๋”ฉ๋˜๋Š” ๊ฑด ๋‹ฌ๋ผ์š”. ๋ฒˆ๋“ค์ด 4MB๋ผ๋Š” ๊ฒŒ ๋ฌธ์ œ์˜ˆ์š”. ์‚ฌ์šฉ์ž๊ฐ€ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ™ˆ์„ ๋ณด๋ ค๊ณ  ์—๋””ํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํฌํ•จํ•œ ์•ฑ ์ „์ฒด๋ฅผ ๋‚ด๋ ค๋ฐ›๊ณ  ์žˆ์–ด์š”. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ฉ์‹œ๋‹ค."

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€: ๋ฒˆ๋“ค์ด ๋ฌด๊ฑฐ์šฐ๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ๋– ๋‚œ๋‹ค

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๋ฒˆ๋“ค ํฌ๊ธฐ๊ฐ€ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ดํƒˆ๊ณผ ์–ด๋–ป๊ฒŒ ์—ฐ๊ฒฐ๋˜๋Š”์ง€ ์ดํ•ดํ•œ๋‹ค
  • LCP, FID, CLS ๋“ฑ Core Web Vitals ์ง€ํ‘œ์™€ ์„ฑ๋Šฅ์˜ ๊ด€๊ณ„๋ฅผ ์ดํ•ดํ•œ๋‹ค

Google ์—ฐ๊ตฌ ๊ฒฐ๊ณผ (๋ฌด์„œ์šด ์‚ฌ์‹ค๋“ค):

๋กœ๋”ฉ ์‹œ๊ฐ„์ดํƒˆ๋ฅ  ์ฆ๊ฐ€
1์ดˆ โ†’ 3์ดˆ32% ์ฆ๊ฐ€
1์ดˆ โ†’ 5์ดˆ90% ์ฆ๊ฐ€
1์ดˆ โ†’ 10์ดˆ123% ์ฆ๊ฐ€
ํ˜„์‹ค: ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ ์ดˆ๊ธฐ ๋ฒˆ๋“ค
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  main.bundle.js  [4.2 MB]        โ”‚
โ”‚                                  โ”‚
โ”‚  โ”œโ”€โ”€ React + ReactDOM  180KB     โ”‚
โ”‚  โ”œโ”€โ”€ moment.js  400KB  โ† ๐Ÿ˜ฑ     โ”‚
โ”‚  โ”œโ”€โ”€ lodash (์ „์ฒด)  500KB โ† ๐Ÿ˜ฑ  โ”‚
โ”‚  โ”œโ”€โ”€ ๋งˆํฌ๋‹ค์šด ์—๋””ํ„ฐ  1.2MB โ† ๐Ÿ’€ โ”‚
โ”‚  โ”œโ”€โ”€ ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ  800KB โ† ๐Ÿ˜ฑ โ”‚
โ”‚  โ””โ”€โ”€ ์šฐ๋ฆฌ ์•ฑ ์ฝ”๋“œ  120KB         โ”‚
โ”‚                                  โ”‚
โ”‚  โ†’ ํ™ˆ ํ™”๋ฉด์„ ๋ณด๋ ค๊ณ  ๋งˆํฌ๋‹ค์šด       โ”‚
โ”‚    ์—๋””ํ„ฐ์™€ ์ฐจํŠธ๋ฅผ ๋ชจ๋‘ ๋‚ด๋ ค๋ฐ›์Œ!  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

์ด ์ƒํ™ฉ์„ ํ•ด๊ฒฐํ•˜๋Š” ํ•ต์‹ฌ ๋„๊ตฌ๊ฐ€ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…(Code Splitting) ์ด์—์š”.

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"์‚ฌ์šฉ์ž๋Š” ์ง€๊ธˆ ๋‹น์žฅ ํ•„์š”ํ•œ ์ฝ”๋“œ๋งŒ ๋‚ด๋ ค๋ฐ›์•„์•ผ ํ•ด. ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํ™”๋ฉด์„ ๋ณด๋ ค๊ณ  ๋งˆํฌ๋‹ค์šด ์—๋””ํ„ฐ ์ฝ”๋“œ๋ฅผ ๋‚ด๋ ค๋ฐ›์„ ์ด์œ ๊ฐ€ ์—†์–ด."


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

๋„์„œ๊ด€์— 1๋งŒ ๊ถŒ์˜ ์ฑ…์ด ์žˆ์–ด. ์˜์ฒ ์ด ๋ฐฉ์‹์€ ๋„์„œ๊ด€์— ์ž…์žฅํ•˜๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ 1๋งŒ ๊ถŒ์„ ํ†ต์งธ๋กœ ์งŠ์–ด์ง€๊ฒŒ ํ•˜๋Š” ๊ฑฐ์•ผ. ์ฑ… ํ•œ ๊ถŒ ๋ณด๋Ÿฌ ์™”๋Š”๋ฐ 1๋งŒ ๊ถŒ์„ ๋“ค๊ณ  ์ž…์žฅํ•ด์•ผ ํ•ด. ๋‹น์—ฐํžˆ ๋А๋ฆฌ์ง€.

์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์€ ๋„์„œ๊ด€ ์ง์›์ด "์ง€๊ธˆ ์–ด๋–ค ์„น์…˜์— ๊ฐ€์‹œ๋‚˜์š”?"๋ฅผ ๋ฌผ์–ด๋ณด๊ณ  ๊ทธ ์„น์…˜ ์ฑ…๋“ค๋งŒ ๊ฐ€์ ธ๋‹ค์ฃผ๋Š” ๋ฐฉ์‹์ด์•ผ. ์†Œ์„ค ์„น์…˜์— ๊ฐ”์„ ๋•Œ ๊ณผํ•™ ์„น์…˜ ์ฑ…์„ ์งŠ์–ด์งˆ ํ•„์š”๊ฐ€ ์—†์ง€. ํ•„์š”ํ•œ ์ˆœ๊ฐ„์— ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๊ฐ€์ ธ์™€.


๐Ÿ“ฆ ๋ฒˆ๋“ค์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๋ฒˆ๋“ค์ด ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค์–ด์ง€๊ณ  ๋ธŒ๋ผ์šฐ์ €์— ์ „๋‹ฌ๋˜๋Š”์ง€ ์ดํ•ดํ•œ๋‹ค
  • ๋ฒˆ๋“ค์ด ํด์ˆ˜๋ก ์™œ ๋กœ๋”ฉ์ด ๋А๋ฆฐ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“– ์šฉ์–ด: ๋ฒˆ๋“ค(Bundle) โ€” Webpack, Vite ๊ฐ™์€ ๋นŒ๋“œ ๋„๊ตฌ๊ฐ€ ์ˆ˜๋ฐฑ ๊ฐœ์˜ .js ํŒŒ์ผ์„ ํ•˜๋‚˜(๋˜๋Š” ๋ช‡ ๊ฐœ)์˜ ํŒŒ์ผ๋กœ ๋ฌถ์€ ๊ฒƒ. ๋ธŒ๋ผ์šฐ์ €๋Š” ์ด ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ํŒŒ์‹ฑํ•œ ํ›„ ์‹คํ–‰ํ•ด์š”.

๊ฐœ๋ฐœ ์ฝ”๋“œ (์ˆ˜๋ฐฑ ๊ฐœ ํŒŒ์ผ)    โ†’    ๋นŒ๋“œ    โ†’    ๋ฒˆ๋“ค (๋ธŒ๋ผ์šฐ์ €์— ์ „๋‹ฌ)
โ”œโ”€โ”€ src/App.tsx                          โ”œโ”€โ”€ main.bundle.js
โ”œโ”€โ”€ src/components/...                   โ”œโ”€โ”€ vendor.bundle.js
โ”œโ”€โ”€ node_modules/react/...               โ””โ”€โ”€ ...
โ””โ”€โ”€ node_modules/lodash/...

๋ฒˆ๋“ค์ด ํด๋ฉด ๋А๋ฆฐ ์ด์œ  3๋‹จ๊ณ„:

โ‘  ๋‹ค์šด๋กœ๋“œ: ํŒŒ์ผ ํฌ๊ธฐ ํผ โ†’ ๋„คํŠธ์›Œํฌ ์ „์†ก ์˜ค๋ž˜ ๊ฑธ๋ฆผ
      โ†“
โ‘ก ํŒŒ์‹ฑ: JS ํŒŒ์‹ฑ = CPU ์ง‘์ค‘ ์ž‘์—… โ†’ ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ์—์„œ ํŠนํžˆ ๋А๋ฆผ
      โ†“
โ‘ข ์‹คํ–‰: ๋ชจ๋“  ์ฝ”๋“œ ์ดˆ๊ธฐํ™” โ†’ ์ฒซ ํ™”๋ฉด ํ‘œ์‹œ ์ง€์—ฐ (LCP ์ƒ์Šน)

๐Ÿ”— ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ
Core Web Vitals(LCP, INP, CLS)์˜ ๊ฐœ๋…์€ Next.js ์‹ฌํ™” โ€” 08๋ฒˆ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฌธ์„œ ์— ์ž์„ธํžˆ ๋‚˜์™€ ์žˆ์–ด. React ์•ฑ๋„ ๋™์ผํ•œ ์ง€ํ‘œ๋กœ ์ธก์ •ํ•ด.

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋ฒˆ๋“ค์€ ์•ฑ์˜ '์ˆ˜ํ•˜๋ฌผ'์ด์•ผ. ๋น„ํ–‰๊ธฐ(๋ธŒ๋ผ์šฐ์ €)์— ์ˆ˜ํ•˜๋ฌผ์ด ๋งŽ์„์ˆ˜๋ก ์ด๋ฅ™(์ฒซ ๋ Œ๋”๋ง)์ด ๋А๋ ค. ๊ผญ ํ•„์š”ํ•œ ์ง๋งŒ ์‹ฃ๋Š” ๊ฒŒ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์ด์•ผ."


โœ‚๏ธ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… โ€” ํ•„์š”ํ•  ๋•Œ๋งŒ ๋‚ด๋ ค๋ฐ›๊ธฐ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์ •์  import ์™€ ๋™์  import() ์˜ ์ฐจ์ด๋ฅผ ์ฝ”๋“œ๋กœ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์–ธ์ œ ๋™์  import ๋ฅผ ์จ์•ผ ํ•˜๋Š”์ง€ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

์ •์  import (ํ˜„์žฌ):

// โŒ ๋ชจ๋“  ๊ฒƒ์„ ์ฆ‰์‹œ ๋กœ๋“œ โ€” ํ™ˆ ํ™”๋ฉด์—์„œ๋„ ์—๋””ํ„ฐ ์ฝ”๋“œ๊ฐ€ ๋กœ๋“œ๋จ
import MarkdownEditor from './MarkdownEditor'; // 1.2MB
import ChartDashboard from './ChartDashboard'; // 800KB
 
function App() {
  return (
    <Router>
      <Route path="/" component={Home} />
      <Route path="/write" component={MarkdownEditor} />
      <Route path="/stats" component={ChartDashboard} />
    </Router>
  );
}

๋™์  import (์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…):

// โœ… ํ•„์š”ํ•œ ์‹œ์ ์— ๋กœ๋“œ โ€” ํ™ˆ ํ™”๋ฉด์—์„  ์—๋””ํ„ฐ ์ฝ”๋“œ๋ฅผ ๋‚ด๋ ค๋ฐ›์ง€ ์•Š์Œ
// import()๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ โ€” ํ•ด๋‹น ๋ชจ๋“ˆ์ด ํ•„์š”ํ•  ๋•Œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋ฐœ์ƒ
 
// /write ๊ฒฝ๋กœ์— ์ง„์ž…ํ•  ๋•Œ๋งŒ MarkdownEditor ๋ฒˆ๋“ค ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘
const loadEditor = () => import('./MarkdownEditor'); // Promise<module>
const loadChart = () => import('./ChartDashboard');
 
// ์‹ค์ œ ์‚ฌ์šฉ ์‹œ
loadEditor().then(module => {
  const MarkdownEditor = module.default;
  // ์ด์ œ MarkdownEditor ์‚ฌ์šฉ ๊ฐ€๋Šฅ
});

๋™์  import ์‚ฌ์šฉ ๊ธฐ์ค€:

์ƒํ™ฉ์ •์  import๋™์  import
ํ•ญ์ƒ ์ฒซ ํ™”๋ฉด์— ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธโœ…๊ณผํ•จ
ํŠน์ • ๋ผ์šฐํŠธ์—์„œ๋งŒ ์‚ฌ์šฉโŒโœ…
์‚ฌ์šฉ์ž ์•ก์…˜(๋ฒ„ํŠผ ํด๋ฆญ) ํ›„ ์‚ฌ์šฉโŒโœ…
๋ฌด๊ฑฐ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (์ฐจํŠธ, ์—๋””ํ„ฐ)โŒโœ…
์กฐ๊ฑด๋ถ€๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฌด๊ฑฐ์šด ์ปดํฌ๋„ŒํŠธโŒโœ…

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"import ModuleName from '...' ๋Š” ์•ฑ ์‹œ์ž‘ ์‹œ ์ฆ‰์‹œ ๋กœ๋“œ. import('...') ๋Š” ํ˜ธ์ถœ ์‹œ์ ์— ๋„คํŠธ์›Œํฌ ์š”์ฒญ โ†’ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์˜ ํ•ต์‹ฌ์ด์•ผ."


โš›๏ธ React.lazy + Suspense๋กœ ์ปดํฌ๋„ŒํŠธ ์Šคํ”Œ๋ฆฌํŒ… ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • React.lazy ์™€ Suspense ๋ฅผ ๊ฒฐํ•ฉํ•ด ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๋กœ๋”ฉ UI ์™€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•จ๊ป˜ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค

React.lazy ๋Š” ๋™์  import() ๋ฅผ React ์ปดํฌ๋„ŒํŠธ๋กœ ๊ฐ์‹ธ์ฃผ๋Š” ํ•จ์ˆ˜์˜ˆ์š”.

import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
 
// โœ… lazy๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ์–ธ โ€” ์ด ์‹œ์ ์—” ์•„๋ฌด๊ฒƒ๋„ ๋‹ค์šด๋กœ๋“œ ์•ˆ ๋จ
const MarkdownEditor = lazy(() => import('./pages/WritePage'));  // ์‹ค์ œ import๋Š” ๋‚˜์ค‘์—
const ChartDashboard = lazy(() => import('./pages/StatsPage'));
const AdminPanel = lazy(() => import('./pages/AdminPage'));
 
function App() {
  return (
    <Router>
      {/* Suspense: lazy ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋กœ๋”ฉ ์ค‘์ผ ๋•Œ fallback UI ํ‘œ์‹œ */}
      <Suspense fallback={<PageSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />         {/* ์ •์  import: ํ•ญ์ƒ ํ•„์š” */}
          <Route path="/write" element={<MarkdownEditor />} />  {/* /write ์ง„์ž… ์‹œ ๋‹ค์šด๋กœ๋“œ */}
          <Route path="/stats" element={<ChartDashboard />} />  {/* /stats ์ง„์ž… ์‹œ ๋‹ค์šด๋กœ๋“œ */}
          <Route path="/admin" element={<AdminPanel />} />      {/* /admin ์ง„์ž… ์‹œ ๋‹ค์šด๋กœ๋“œ */}
        </Routes>
      </Suspense>
    </Router>
  );
}

๋กœ๋”ฉ UI ์„ธ๋ถ„ํ™”:

// โœ… ๊ฐ ๋ผ์šฐํŠธ๋งˆ๋‹ค ๋‹ค๋ฅธ ๋กœ๋”ฉ UI ์ ์šฉ
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
 
        {/* ์—๋””ํ„ฐ ์ „์šฉ ๋กœ๋”ฉ UI */}
        <Route
          path="/write"
          element={
            <Suspense fallback={<EditorSkeleton />}>
              <MarkdownEditor />
            </Suspense>
          }
        />
 
        {/* ์ฐจํŠธ ์ „์šฉ ๋กœ๋”ฉ UI */}
        <Route
          path="/stats"
          element={
            <Suspense fallback={<ChartSkeleton />}>
              <ChartDashboard />
            </Suspense>
          }
        />
      </Routes>
    </Router>
  );
}

ErrorBoundary ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ (๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋Œ€์‘):

// lazy ์ปดํฌ๋„ŒํŠธ ๋กœ๋”ฉ ์‹คํŒจ(๋„คํŠธ์›Œํฌ ๋Š๊น€ ๋“ฑ)๋ฅผ ErrorBoundary๋กœ ์ฒ˜๋ฆฌ
function App() {
  return (
    <ErrorBoundary fallback={<p>ํŽ˜์ด์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์–ด์š”. ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ฃผ์„ธ์š”.</p>}>
      <Suspense fallback={<PageSpinner />}>
        <Routes>...</Routes>
      </Suspense>
    </ErrorBoundary>
  );
}

๊ฒฐ๊ณผ: ๋„คํŠธ์›Œํฌ ํƒญ์—์„œ ํ™•์ธ๋˜๋Š” ๋ณ€ํ™”:

// Before: ํ™ˆ ๋ฐฉ๋ฌธ ์‹œ
main.bundle.js   [4.2 MB] โ† ํ•œ ๋ฒˆ์— ๋ชจ๋‘ ๋‹ค์šด๋กœ๋“œ ๐Ÿ˜ฑ

// After: ํ™ˆ ๋ฐฉ๋ฌธ ์‹œ
main.bundle.js   [180 KB] โ† ํ•ต์‹ฌ๋งŒ

// /write ๋ฐฉ๋ฌธ ์‹œ (์ถ”๊ฐ€ ๋‹ค์šด๋กœ๋“œ)
write-page.js    [1.3 MB] โ† ํ•„์š”ํ•  ๋•Œ ๋‹ค์šด๋กœ๋“œ

// /stats ๋ฐฉ๋ฌธ ์‹œ (์ถ”๊ฐ€ ๋‹ค์šด๋กœ๋“œ)
stats-page.js    [820 KB] โ† ํ•„์š”ํ•  ๋•Œ ๋‹ค์šด๋กœ๋“œ

์‹ค์Šต ํ›„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

  • React.lazy(() => import('./Component')) ๋ฌธ๋ฒ•์„ ์™ธ์› ๋‹ค
  • Suspense ๊ฐ€ ์—†์œผ๋ฉด lazy ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ๋‹ค
  • ๋ผ์šฐํŠธ๋งˆ๋‹ค ๋ณ„๋„ Suspense ๋ฅผ ์“ฐ๋ฉด ๋กœ๋”ฉ UI๋ฅผ ์„ธ๋ถ„ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ๋‹ค

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"lazy(() => import('./Page')) + <Suspense fallback={<Spinner/>}> โ€” ์ด ๋‘ ์ค„ ์กฐํ•ฉ์ด React ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์˜ ์ „๋ถ€์•ผ. ๋ผ์šฐํŠธ๋งˆ๋‹ค ๊ฐ์‹ธ๋ฉด ๋ผ์šฐํŠธ ๊ธฐ๋ฐ˜ ์Šคํ”Œ๋ฆฌํŒ… ์™„์„ฑ."


๐Ÿ—บ๏ธ ๋ผ์šฐํŠธ ๊ธฐ๋ฐ˜ vs ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ์Šคํ”Œ๋ฆฌํŒ… ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์–ด๋–ค ์ˆ˜์ค€์—์„œ ์ฝ”๋“œ๋ฅผ ์ชผ๊ฐœ์•ผ ํšจ๊ณผ์ ์ธ์ง€ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

๋ผ์šฐํŠธ ๊ธฐ๋ฐ˜ ์Šคํ”Œ๋ฆฌํŒ… (๊ฐ€์žฅ ํšจ๊ณผ์ , ๊ธฐ๋ณธ):

// โœ… ๊ฐ ํŽ˜์ด์ง€๋ฅผ ๋ณ„๋„ ์ฒญํฌ(chunk)๋กœ ๋ถ„๋ฆฌ
const HomePage = lazy(() => import('./pages/HomePage'));    // chunk: home
const WritePage = lazy(() => import('./pages/WritePage'));  // chunk: write
const ProfilePage = lazy(() => import('./pages/Profile'));  // chunk: profile

์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ์Šคํ”Œ๋ฆฌํŒ… (๋ฌด๊ฑฐ์šด ์ปดํฌ๋„ŒํŠธ์— ์„ ํƒ์  ์ ์šฉ):

// โœ… ํŠน์ • ๋ฌด๊ฑฐ์šด ์ปดํฌ๋„ŒํŠธ๋งŒ ์ง€์—ฐ ๋กœ๋”ฉ
import { lazy, Suspense, useState } from 'react';
 
// ๋งˆํฌ๋‹ค์šด ์—๋””ํ„ฐ: 1.2MB โ€” ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๋•Œ๋งŒ ํ•„์š”
const MarkdownEditor = lazy(() => import('./MarkdownEditor'));
 
function PostForm() {
  const [isEditorOpen, setIsEditorOpen] = useState(false);
 
  return (
    <div>
      <button onClick={() => setIsEditorOpen(true)}>์—๋””ํ„ฐ ์—ด๊ธฐ</button>
 
      {isEditorOpen && (
        <Suspense fallback={<p>์—๋””ํ„ฐ ๋กœ๋”ฉ ์ค‘...</p>}>
          <MarkdownEditor />  {/* ๋ฒ„ํŠผ ํด๋ฆญ ํ›„์—์•ผ 1.2MB ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘ */}
        </Suspense>
      )}
    </div>
  );
}

์–ธ์ œ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ์Šคํ”Œ๋ฆฌํŒ…์„ ์“ธ๊นŒ?

// ์Šคํ”Œ๋ฆฌํŒ… ํšจ๊ณผ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
const RichTextEditor = lazy(() => import('react-quill'));  // 500KB ์ด์ƒ โ†’ ํšจ๊ณผ์ 
const PdfViewer = lazy(() => import('./PdfViewer'));       // ๋ฌด๊ฑฐ์šด PDF ๋ Œ๋”๋Ÿฌ
const VideoPlayer = lazy(() => import('./VideoPlayer'));   // ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด
 
// ์Šคํ”Œ๋ฆฌํŒ… ํšจ๊ณผ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์˜คํžˆ๋ ค ์†ํ•ด์ธ ๊ฒฝ์šฐ
const SmallButton = lazy(() => import('./Button')); // โŒ 2KB ์ปดํฌ๋„ŒํŠธ โ†’ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋” ํผ
// โ†’ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์˜ค๋ฒ„ํ—ค๋“œ > ํŒŒ์ผ ํฌ๊ธฐ ์ ˆ์•ฝ ํšจ๊ณผ

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋ผ์šฐํŠธ๋งˆ๋‹ค ์Šคํ”Œ๋ฆฌํŒ…์€ ๊ธฐ๋ณธ๊ฐ’์ด์•ผ. ์ปดํฌ๋„ŒํŠธ ์Šคํ”Œ๋ฆฌํŒ…์€ 50KB ์ด์ƒ์˜ ๋ฌด๊ฑฐ์šด ์ปดํฌ๋„ŒํŠธ์—๋งŒ ์ ์šฉํ•ด. ๋„ˆ๋ฌด ์ž˜๊ฒŒ ์ชผ๊ฐœ๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚˜ ์˜คํžˆ๋ ค ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด."


๐Ÿ” Bundle Analyzer๋กœ ๋ฒˆ๋“ค ํ•ด๋ถ€ํ•˜๊ธฐ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • Bundle Analyzer ๋ฅผ ์„ค์น˜ํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋ฒˆ๋“ค์„ ๊ฐ€์žฅ ๋ฌด๊ฒ๊ฒŒ ๋งŒ๋“œ๋Š”์ง€ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค

์„ค์น˜ & ์‹คํ–‰:

# Create React App (CRA) ์‚ฌ์šฉ ์‹œ
npm install --save-dev source-map-explorer
# package.json์— ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€:
# "analyze": "source-map-explorer 'build/static/js/*.js'"
npm run build && npm run analyze
 
# Vite ์‚ฌ์šฉ ์‹œ
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts์— ์ถ”๊ฐ€:
# plugins: [visualizer({ open: true })]
npm run build  # ๋นŒ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์‹œ๊ฐํ™” ํŒŒ์ผ ์ƒ์„ฑ
 
# Next.js ์‚ฌ์šฉ ์‹œ
npm install --save-dev @next/bundle-analyzer
# next.config.js์— ์„ค์ • ์ถ”๊ฐ€ ํ›„:
ANALYZE=true npm run build

Bundle Analyzer ํ™”๋ฉด ์ฝ๋Š” ๋ฒ•:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ๋ฒˆ๋“ค ์‹œ๊ฐํ™” (ํฌ๊ธฐ = ์‚ฌ๊ฐํ˜• ๋ฉด์ )                  โ”‚
โ”‚                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚               โ”‚  โ”‚React โ”‚  โ”‚              โ”‚ โ”‚
โ”‚  โ”‚  moment.js    โ”‚  โ”‚ DOM  โ”‚  โ”‚   lodash     โ”‚ โ”‚
โ”‚  โ”‚   (400KB)     โ”‚  โ”‚(180K)โ”‚  โ”‚  (500KB)     โ”‚ โ”‚
โ”‚  โ”‚               โ”‚  โ”‚      โ”‚  โ”‚              โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                 โ”‚
โ”‚  ๐Ÿ“ ๋ฉด์ ์ด ํฐ ๊ฒƒ = ๋ฒˆ๋“ค์—์„œ ์ฐจ์ง€ํ•˜๋Š” ๋น„์ค‘์ด ํผ     โ”‚
โ”‚  ๐Ÿ“ ํฐ ์‚ฌ๊ฐํ˜•๋ถ€ํ„ฐ ๊ฒฝ๋Ÿ‰ํ™” ๋Œ€์ƒ                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ํ”ํ•œ "๋ฒˆ๋“ค ๋ฒ”์ธ" ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๋Œ€์•ˆ:

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌํฌ๊ธฐ๋ฌธ์ œ๋Œ€์•ˆ
moment.js~400KB์ „์ฒด ๋กœ์ผ€์ผ ํฌํ•จdate-fns (ํ•„์š”ํ•œ ํ•จ์ˆ˜๋งŒ) ๋˜๋Š” dayjs (2KB)
lodash (์ „์ฒด)~500KBimport _ from 'lodash' ๋กœ ์ „์ฒด ๋กœ๋“œimport { debounce } from 'lodash-es' ๋˜๋Š” ์ง์ ‘ ๊ตฌํ˜„
react-icons (์ „์ฒด)๊ฐ€๋ณ€์ import * as Icons from 'react-icons/fa'import { FaUser } from 'react-icons/fa' (๊ฐœ๋ณ„ import)
xlsx~800KB์Šคํ”„๋ ˆ๋“œ์‹œํŠธ ์ „์ฒด ๊ธฐ๋Šฅ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ ํ›„ ๋‹ค์šด๋กœ๋“œ ๋งํฌ ์ œ๊ณต

moment.js โ†’ dayjs ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ˆ์‹œ:

// โŒ moment.js: 400KB
import moment from 'moment';
const formatted = moment(date).format('YYYY๋…„ MM์›” DD์ผ');
 
// โœ… dayjs: 2KB (200๋ฐฐ ์ฐจ์ด!)
import dayjs from 'dayjs';
import 'dayjs/locale/ko'; // ํ•œ๊ตญ์–ด ๋กœ์ผ€์ผ๋งŒ ์ถ”๊ฐ€ (์„ ํƒ์ )
dayjs.locale('ko');
const formatted = dayjs(date).format('YYYY๋…„ MM์›” DD์ผ');
 
// โœ… date-fns: ํ•„์š”ํ•œ ํ•จ์ˆ˜๋งŒ ํŠธ๋ฆฌ์…ฐ์ดํ‚น
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
const formatted = format(date, 'yyyy๋…„ MM์›” dd์ผ', { locale: ko });

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"Bundle Analyzer ๋ฅผ ์ฒ˜์Œ ์ผœ๋ฉด 'moment.js์™€ lodash ์ „์ฒด' ๊ฐ€ ๋ฒˆ๋“ค์˜ ์ ˆ๋ฐ˜ ์ด์ƒ์„ ์ฐจ์ง€ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. ์ด ๋‘˜์˜ ๊ฒฝ๋Ÿ‰ ๋Œ€์•ˆ์œผ๋กœ๋งŒ ๋ฐ”๊ฟ”๋„ 30~50% ๋ฒˆ๋“ค ๊ฐ์†Œ๋Š” ๊ธฐ๋ณธ์ด์•ผ."


๐ŸŒฒ Tree Shaking โ€” ์•ˆ ์“ฐ๋Š” ์ฝ”๋“œ ์ž๋™ ์ œ๊ฑฐ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • Tree Shaking์ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์ดํ•ดํ•œ๋‹ค
  • Tree Shaking ์ด ๋˜์ง€ ์•Š๋Š” ํ”ํ•œ ์‹ค์ˆ˜๋ฅผ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“– ์šฉ์–ด: Tree Shaking โ€” ๋‚˜๋ฌด๋ฅผ ํ”๋“ค๋ฉด ์ฃฝ์€ ๋‚˜๋ญ‡์žŽ์ด ๋–จ์–ด์ง€๋“ฏ, ๋ฒˆ๋“ค์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ œ๊ฑฐํ•˜๋Š” ์ตœ์ ํ™” ๊ธฐ๋ฒ•. Webpack, Vite๊ฐ€ ๋นŒ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์ˆ˜ํ–‰ํ•ด์š”.

Tree Shaking ์ž‘๋™ ์กฐ๊ฑด:

// โœ… Tree Shaking์ด ๋˜๋Š” named export
import { debounce } from 'lodash-es'; // debounce๋งŒ ๋ฒˆ๋“ค์— ํฌํ•จ
// โ†’ ๋‚˜๋จธ์ง€ lodash ํ•จ์ˆ˜ ์ˆ˜๋ฐฑ ๊ฐœ๋Š” ์ œ๊ฑฐ๋จ
 
// โŒ Tree Shaking์ด ์•ˆ ๋˜๋Š” default export (์ „์ฒด import)
import _ from 'lodash';         // lodash ์ „์ฒด ๋ฒˆ๋“ค์— ํฌํ•จ!
const debounced = _.debounce(); // debounce ํ•˜๋‚˜๋งŒ ์จ๋„ lodash ์ „์ฒด๊ฐ€ ๋“ค์–ด์˜ด

Tree Shaking์ด ์•ˆ ๋˜๋Š” ํ”ํ•œ ์‹ค์ˆ˜:

// โŒ 1. CommonJS ๋ชจ๋“ˆ์€ Tree Shaking ๋ถˆ๊ฐ€
const { debounce } = require('lodash'); // require๋Š” Tree Shaking ๋ถˆ๊ฐ€!
// โ†’ ESM(import/export)๋งŒ Tree Shaking ๊ฐ€๋Šฅ
 
// โŒ 2. ์™€์ผ๋“œ์นด๋“œ import
import * as Icons from 'react-icons/fa'; // ๋ชจ๋“  ์•„์ด์ฝ˜ ํฌํ•จ!
import { FaUser, FaHeart } from 'react-icons/fa'; // โœ… ๊ฐœ๋ณ„ import
 
// โŒ 3. ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์žˆ๋Š” ๋ชจ๋“ˆ
import 'some-css-library'; // CSS ํŒŒ์ผ์€ Tree Shaking ๋Œ€์ƒ ์•„๋‹˜
import './polyfills';      // ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ๋„ Tree Shaking ๋Œ€์ƒ ์•„๋‹˜
 
// โœ… ์˜ฌ๋ฐ”๋ฅธ ๊ฐœ๋ณ„ import ํŒจํ„ด
import { format, parseISO } from 'date-fns'; // ๋‘ ํ•จ์ˆ˜๋งŒ ๋ฒˆ๋“ค์— ํฌํ•จ

sideEffects ์„ค์ •์œผ๋กœ Tree Shaking ๊ฐ•ํ™” (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐœ๋ฐœ ์‹œ):

// package.json
{
  "sideEffects": false  // ๋ชจ๋“  ํŒŒ์ผ์ด ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ์—†์Œ โ†’ ๋ฒˆ๋“ค๋Ÿฌ๊ฐ€ ์ ๊ทน์ ์œผ๋กœ ์ œ๊ฑฐ
  // ๋˜๋Š”
  "sideEffects": ["*.css", "./src/polyfills.js"]  // ํŠน์ • ํŒŒ์ผ๋งŒ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ์žˆ์Œ
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"Tree Shaking์€ ESM(named export)์—์„œ๋งŒ ์ž‘๋™ํ•ด. import _ from 'lodash' ๋Œ€์‹  import { debounce } from 'lodash-es' โ€” ์ด ํ•œ ์ค„์˜ ์ฐจ์ด๊ฐ€ ์ˆ˜๋ฐฑKB ์ฐจ์ด๋ฅผ ๋งŒ๋“ค์–ด."


๐Ÿ“Š ์ด๋ฏธ์ง€ ์ตœ์ ํ™”์™€ Web Vitals ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • LCP ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ „๋žต์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค
  • Lighthouse ์ ์ˆ˜๋ฅผ ์‹ค์ œ๋กœ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‹ค

Lighthouse๋กœ ์„ฑ๋Šฅ ์ธก์ •:

# Chrome DevTools โ†’ Lighthouse ํƒญ โ†’ Generate Report
# ๋˜๋Š” CLI
npm install -g lighthouse
lighthouse https://your-site.com --view

LCP ๊ฐœ์„  ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

// โœ… 1. ์ด๋ฏธ์ง€ ์ง€์—ฐ ๋กœ๋”ฉ (์Šคํฌ๋กค ํ•˜๋‹จ ์ด๋ฏธ์ง€)
<img src="photo.jpg" loading="lazy" alt="๊ฒŒ์‹œ๊ธ€ ์ด๋ฏธ์ง€" />
 
// โœ… 2. ์ฒซ ํ™”๋ฉด ์ด๋ฏธ์ง€๋Š” preload ํžŒํŠธ
<link rel="preload" as="image" href="/hero-image.webp" />
 
// โœ… 3. ํ˜„๋Œ€์  ์ด๋ฏธ์ง€ ํฌ๋งท ์‚ฌ์šฉ
// WebP๋Š” JPEG ๋Œ€๋น„ 25-35% ์ž‘์Œ
// AVIF๋Š” WebP ๋Œ€๋น„ 20% ๋” ์ž‘์Œ
<picture>
  <source srcSet="hero.avif" type="image/avif" />
  <source srcSet="hero.webp" type="image/webp" />
  <img src="hero.jpg" alt="ํžˆ์–ด๋กœ ์ด๋ฏธ์ง€" /> {/* ํด๋ฐฑ */}
</picture>
 
// โœ… 4. ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๋ช…์‹œ (CLS ๋ฐฉ์ง€)
<img src="photo.jpg" width={400} height={300} alt="..." />
// width/height ์—†์œผ๋ฉด ์ด๋ฏธ์ง€ ๋กœ๋“œ ์ „ ํฌ๊ธฐ๋ฅผ ๋ชจ๋ฆ„ โ†’ ๋ ˆ์ด์•„์›ƒ ์ด๋™(CLS) ๋ฐœ์ƒ!

React์—์„œ ์„ฑ๋Šฅ ์ธก์ • (Profiler API):

import { Profiler } from 'react';
 
function onRenderCallback(
  id: string,           // Profiler์˜ id prop
  phase: 'mount' | 'update', // ์ฒซ ๋งˆ์šดํŠธ์ธ์ง€ ์—…๋ฐ์ดํŠธ์ธ์ง€
  actualDuration: number,    // ์‹ค์ œ ๋ Œ๋” ์‹œ๊ฐ„ (ms)
  baseDuration: number,      // ์ตœ์ ํ™” ์—†๋Š” ์˜ˆ์ƒ ๋ Œ๋” ์‹œ๊ฐ„
) {
  // ๋ Œ๋” ์‹œ๊ฐ„์ด 16ms(60fps) ์ดˆ๊ณผ ์‹œ ์ตœ์ ํ™” ํ•„์š”
  if (actualDuration > 16) {
    console.warn(`[${id}] ๋ Œ๋” ์ตœ์ ํ™” ํ•„์š”: ${actualDuration.toFixed(2)}ms`);
  }
}
 
// ์ธก์ •ํ•˜๊ณ  ์‹ถ์€ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ๋ฅผ Profiler๋กœ ๊ฐ์‹ธ๊ธฐ
function App() {
  return (
    <Profiler id="PostFeed" onRender={onRenderCallback}>
      <PostFeed />
    </Profiler>
  );
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"LCP๋Š” ์ฒซ ํ™”๋ฉด์˜ ๊ฐ€์žฅ ํฐ ์ด๋ฏธ์ง€/ํ…์ŠคํŠธ ๋ Œ๋” ์‹œ๊ฐ„์ด์•ผ. WebP ํฌ๋งท + ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๋ช…์‹œ + preload ํžŒํŠธ ์„ธ ๊ฐ€์ง€๋งŒ์œผ๋กœ๋„ LCP๋ฅผ ํฌ๊ฒŒ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด."


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ


โŒ A React component suspended while rendering, but no fallback UI was specified

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

Error: A React component suspended while rendering, but no fallback UI was specified.

์›์ธ: lazy ๋กœ ๋ถˆ๋Ÿฌ์˜จ ์ปดํฌ๋„ŒํŠธ๋ฅผ Suspense ๋กœ ๊ฐ์‹ธ์ง€ ์•Š์•˜์–ด์š”.

ํ•ด๊ฒฐ์ฑ…:

// โœ… lazy ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐ˜๋“œ์‹œ Suspense๋กœ ๊ฐ์‹ธ์•ผ ํ•จ
const LazyComponent = lazy(() => import('./LazyComponent'));
 
function App() {
  return (
    <Suspense fallback={<div>๋กœ๋”ฉ ์ค‘...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

โŒ React.lazy๊ฐ€ default export๋งŒ ์ง€์›ํ•จ

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

// named export ์ปดํฌ๋„ŒํŠธ๋ฅผ lazy๋กœ import ์‹œ๋„
const { MyComponent } = lazy(() => import('./MyComponents')); // โŒ

ํ•ด๊ฒฐ์ฑ…:

// ๋ฐฉ๋ฒ• 1: ํ•ด๋‹น ํŒŒ์ผ์—์„œ default export ์ถ”๊ฐ€
// MyComponent.tsx
export default function MyComponent() { ... }
 
// ๋ฐฉ๋ฒ• 2: ๋ž˜ํผ๋ฅผ ํ†ตํ•ด named โ†’ default ๋ณ€ํ™˜
const MyComponent = lazy(() =>
  import('./MyComponents').then(module => ({ default: module.MyComponent }))
);

โŒ Tree Shaking์ด ์ ์šฉ๋๋Š”๋ฐ๋„ ๋ฒˆ๋“ค ํฌ๊ธฐ๊ฐ€ ์•ˆ ์ค„์–ด๋“ฆ

์›์ธ ์ง„๋‹จ:

# 1. ํ•ด๋‹น ํŒจํ‚ค์ง€๊ฐ€ ESM์„ ์ง€์›ํ•˜๋Š”์ง€ ํ™•์ธ
cat node_modules/lodash/package.json | grep '"module"'
# "module" ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด CommonJS โ†’ Tree Shaking ์•ˆ ๋จ
 
# 2. lodash-es ๋Œ€์‹  lodash ์“ฐ๊ณ  ์žˆ์ง€ ์•Š์€์ง€ ํ™•์ธ
# lodash๋Š” CommonJS, lodash-es๋Š” ESM

ํ•ด๊ฒฐ์ฑ…:

// lodash (CommonJS) โ†’ lodash-es (ESM)๋กœ ๋ณ€๊ฒฝ
// Before
import { debounce } from 'lodash';
 
// After
import { debounce } from 'lodash-es';
// ๋˜๋Š” ์ง์ ‘ ๊ตฌํ˜„ (debounce๋Š” ๋ช‡ ์ค„์ด๋ฉด ๊ฐ€๋Šฅ)

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๐Ÿ“‹ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ํŒจํ„ด

ํŒจํ„ด์ฝ”๋“œ์ ์šฉ ์‹œ๊ธฐ
๋ผ์šฐํŠธ ์Šคํ”Œ๋ฆฌํŒ…lazy(() => import('./Page'))๊ธฐ๋ณธ๊ฐ’, ๋ชจ๋“  ๋ผ์šฐํŠธ
์ปดํฌ๋„ŒํŠธ ์Šคํ”Œ๋ฆฌํŒ…lazy(() => import('./HeavyComp'))50KB+ ๋ฌด๊ฑฐ์šด ์ปดํฌ๋„ŒํŠธ
์กฐ๊ฑด๋ถ€ ์Šคํ”Œ๋ฆฌํŒ…isOpen && <Suspense><LazyComp/></Suspense>๋ฒ„ํŠผ ํด๋ฆญ ํ›„ ๋‚˜ํƒ€๋‚˜๋Š” ๋ฌด๊ฑฐ์šด UI

๐Ÿ“‹ ๋ฒˆ๋“ค ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํ•ญ๋ชฉํ™•์ธ์˜ˆ์ƒ ํšจ๊ณผ
moment.js โ†’ dayjs ๊ต์ฒด[ ]-400KB
lodash ์ „์ฒด โ†’ ๊ฐœ๋ณ„ import[ ]-400KB
๋ผ์šฐํŠธ๋ณ„ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…[ ]์ดˆ๊ธฐ ๋กœ๋”ฉ -50%
์ด๋ฏธ์ง€ WebP ํฌ๋งท ์ ์šฉ[ ]์ด๋ฏธ์ง€ ํฌ๊ธฐ -30%
์ด๋ฏธ์ง€์— width/height ๋ช…์‹œ[ ]CLS 0์ 
Bundle Analyzer ์ •๊ธฐ ์‹คํ–‰[ ]๋ฒˆ๋“ค ๋ชจ๋‹ˆํ„ฐ๋ง

โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ

โŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ์ด์œ 
import _ from 'lodash'import { debounce } from 'lodash-es'์ „์ฒด ๋ฒˆ๋“ค ํฌํ•จ
import * as Icons from 'react-icons'import { FaUser } from 'react-icons/fa'๋ชจ๋“  ์•„์ด์ฝ˜ ํฌํ•จ
lazy ์—†์ด ๋ฌด๊ฑฐ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ง์ ‘ importlazy(() => import('./Heavy'))์ดˆ๊ธฐ ๋ฒˆ๋“ค ๋น„๋Œ€
Suspense ์—†์ด lazy ์‚ฌ์šฉ<Suspense fallback={...}> ๋กœ ๊ฐ์‹ธ๊ธฐ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ
2KB ์ปดํฌ๋„ŒํŠธ์— ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์ •์  import ์œ ์ง€์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋” ํผ

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

๊ธฐ๋Šฅ๋งŒ ์ž˜ ๊ฐœ๋ฐœํ•˜๋ฉด ๋์ธ ์ค„ ์•Œ์•˜๋˜ ๋‚ด ์˜ค๋งŒํ•จ์ด, ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ 4MB๋ผ๋Š” ์ฒ˜์ฐธํ•œ ์ˆซ์ž๋กœ ๋Œ์•„์™”์—ˆ๋‹ค. ๊ฒŒ์‹œ๊ธ€ ํ•˜๋‚˜ ๋ณด๋ ค๋Š”๋ฐ ์—๋””ํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊นŒ์ง€ ํ†ต์งธ๋กœ ๋‹ค์šด๋ฐ›๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค๋‹ˆ... ์œ ์ €๋“ค์ด ์™œ ์ฒซ ํ™”๋ฉด ๋กœ๋”ฉ์ด ๋А๋ฆฌ๋‹ค๊ณ  ํ™”๋ฅผ ๋ƒˆ๋Š”์ง€ ์ด์ œ ์•Œ๊ฒ ๋‹ค.

๐Ÿ’ก "์œ ์ €์˜ ์ง(๋ฒˆ๋“ค)์„ ๋œ์–ด์ฃผ๋Š” ์ž๊ฐ€ ์ง„์งœ ํ”„๋ก ํŠธ์—”๋“œ ์žฅ์ธ์ด๋‹ค. ํ•„์š”ํ•œ ์ˆœ๊ฐ„์—๋งŒ ๋™์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” Code Splitting์€ ์„ ํƒ์ด ์•„๋‹Œ ํ•„์ˆ˜!"

React.lazy๋ž‘ Suspense ์„ค์ • ์กฐ๊ธˆ๋งŒ ํ–ˆ์„ ๋ฟ์ธ๋ฐ ์•ฑ ์šฉ๋Ÿ‰์ด ๋š๋š ๋–จ์–ด์ง€๋Š” ๊ฑธ Bundle Analyzer๋กœ ์ง์ ‘ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•˜๋‹ˆ ์†์ด ๋‹ค ์‹œ์›ํ•˜๋‹ค. lodash ์ „์ฒด๋ฅผ ๋‹ค ๋ถˆ๋Ÿฌ์˜ค๋˜ ๋ฏธ๋ จํ•œ ์ฝ”๋“œ๋„ ๋‹น์žฅ ๋‹ค named export๋กœ ๋ฐ”๊ฟจ๋‹ค. ์„ฑ๋Šฅ ์ตœ์ ํ™”, ๋‚จ์˜ ์ผ์ธ ์ค„ ์•Œ์•˜๋Š”๋ฐ ์ด์   ์ž์‹ ๊ฐ์ด ๋ถ™์—ˆ๋‹ค.


๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ํšจ๊ณผ๊ฐ€ ๊ฐ€์žฅ ํฐ ๊ฒƒ์€?

  • A) 1KB ์งœ๋ฆฌ ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ lazy ๋กœ ๋™์  ๋กœ๋“œ
  • B) ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€์˜ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ๋ฅผ lazy ๋กœ ๋™์  ๋กœ๋“œ
  • C) ๋งˆํฌ๋‹ค์šด ์—๋””ํ„ฐ(1.2MB)๋ฅผ ์—๋””ํ„ฐ ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ์—๋งŒ ๋™์  ๋กœ๋“œ
  • D) React ์ž์ฒด๋ฅผ ๋™์  ๋กœ๋“œ

โœ… ์ •๋‹ต: C

  • A: 1KB โ†’ ์˜ค๋ฒ„ํ—ค๋“œ > ์ ˆ์•ฝ ํšจ๊ณผ, ์—ญํšจ๊ณผ
  • B: ๋ ˆ์ด์•„์›ƒ์€ ๋ชจ๋“  ํŽ˜์ด์ง€์— ๊ณตํ†ต โ†’ ์Šคํ”Œ๋ฆฌํŒ… ํšจ๊ณผ ์—†์Œ
  • C: 1.2MB์˜ ๋ฌด๊ฑฐ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•„์š”ํ•œ ํŽ˜์ด์ง€์—์„œ๋งŒ ๋กœ๋“œ โ†’ ๊ฐ€์žฅ ํฐ ํšจ๊ณผ โœ…
  • D: React๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ์ „์ œ โ†’ ๋™์  ๋กœ๋“œ ๋ถˆ๊ฐ€๋Šฅ

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์€ ํฌ๊ณ  ์„ ํƒ์ ์ธ ๊ฒƒ์— ์ ์šฉํ• ์ˆ˜๋ก ํšจ๊ณผ๊ฐ€ ์ปค. ์ž‘๊ณ  ๊ณตํ†ต์ ์ธ ์ฝ”๋“œ์—๋Š” ์˜คํžˆ๋ ค ์†ํ•ด."


Q2. ์•„๋ž˜ ๋นˆ์นธ์„ ์ฑ„์›Œ๋ณด์ž.

// ์—๋””ํ„ฐ ํŽ˜์ด์ง€๋ฅผ ์ง€์—ฐ ๋กœ๋”ฉํ•˜๋˜, ๋กœ๋”ฉ ์ค‘์—” <Spinner/>๋ฅผ ํ‘œ์‹œํ•ด์•ผ ํ•ด
 
const EditorPage = ________(() => import('./EditorPage')); // 1๋ฒˆ
 
function App() {
  return (
    <________ fallback={<Spinner />}> // 2๋ฒˆ
      <EditorPage />
    </________>
  );
}

โœ… ์ •๋‹ต: 1๋ฒˆ: lazy, 2๋ฒˆ: Suspense

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "lazy + Suspense ๋Š” ํ•ญ์ƒ ๋ถ™์–ด๋‹ค๋…€. lazy๊ฐ€ ์„ ์–ธ, Suspense๊ฐ€ ํ‘œํ˜„์ด์•ผ."


Q3. ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

Tree Shaking์ด import _ from 'lodash' ์—์„œ๋Š” ์™œ ์•ˆ ๋˜๊ณ , import { debounce } from 'lodash-es' ์—์„œ๋Š” ๋˜๋Š”์ง€ ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"Tree Shaking์€ '์•ˆ ์“ฐ๋Š” ์ฝ”๋“œ๋ฅผ ์ž๋ฅด๋Š” ๊ฒƒ'์ธ๋ฐ, ์ž๋ฅด๋ ค๋ฉด ์–ด๋–ค ์ฝ”๋“œ๊ฐ€ ์–ด๋””์— ์žˆ๋Š”์ง€ ์ •์ ์œผ๋กœ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด. import _ from 'lodash' ๋Š” _ ๊ฐ์ฒด ํ•˜๋‚˜์— ๋ชจ๋“  ํ•จ์ˆ˜๊ฐ€ ๋‹ด๊ฒจ์žˆ๊ณ , _.debounce() ์ฒ˜๋Ÿผ ์“ฐ๋ฉด ๋นŒ๋“œ ๋„๊ตฌ๊ฐ€ _ ์ค‘์— ๋ญ˜ ์“ฐ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์–ด์„œ ์ „๋ถ€ ๋ฒˆ๋“ค์— ํฌํ•จํ•ด. import { debounce } from 'lodash-es' ๋Š” debounce ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค๊ณ  ๋ช…์‹œ์ ์œผ๋กœ ์„ ์–ธํ•˜๋‹ˆ๊นŒ ๋นŒ๋“œ ๋„๊ตฌ๊ฐ€ ๋‚˜๋จธ์ง€๋Š” ์•ˆ์ „ํ•˜๊ฒŒ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์–ด. ESM์˜ named export๊ฐ€ ์ •์  ๋ถ„์„์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ค˜."


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ