๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘จ‍๐Ÿ’ป web.dev/translates

๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์ •๋ง ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ• ๊นŒ์š”?

by HandHand 2025. 11. 20.

 

 

 

์›๋ฌธ: https://www.developerway.com/posts/react-server-components-performance

 

 

๋™์ผํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ CSR, SSR, RSC๋ฅผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋น„๊ตํ•˜๋ฉฐ, ์ดˆ๊ธฐ ๋กœ๋“œ ์„ฑ๋Šฅ๊ณผ ํด๋ผ์ด์–ธํŠธ vs ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ฐ์ดํ„ฐ ํŒจ์นญ(Streaming + Suspense ํฌํ•จ)์˜ ์˜ํ–ฅ์„ ์ค‘์‹ฌ์œผ๋กœ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

 

๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ๋“ค์–ด๋ณด์…จ๋‚˜์š”? ์•„๋งˆ ๋“ค์–ด๋ณด์…จ์„ ๊ฒ๋‹ˆ๋‹ค. ์ง€๋‚œ ๋ช‡ ๋…„๊ฐ„ ๋ฆฌ์•กํŠธ ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ๊ฑฐ์˜ ๋ชจ๋“  ํ™”์ œ๊ฐ€ ๊ทธ๊ฒƒ์ด์—ˆ์ฃ . ํ•˜์ง€๋งŒ ์ œ ์ƒ๊ฐ์— ์ด๊ฑด ๊ฐ€์žฅ ์˜คํ•ด๋ฐ›์€ ๊ฐœ๋… ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค.

 

์†”์งํžˆ ๋ง์”€๋“œ๋ฆฌ๋ฉด, ์ €๋„ ํ•œ๋™์•ˆ ๊ทธ ๊ฐœ๋…์„ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ œ ์‚ฌ๊ณ ๋ฐฉ์‹์ด ์‹ค์šฉ์ ์ธ ํŽธ์ด๋ผ, ๋„ˆ๋ฌด ๊ฐœ๋…์ ์ธ ์ด์•ผ๊ธฐ๋กœ ๋А๊ปด์กŒ๊ฑฐ๋“ ์š”. ๊ฒŒ๋‹ค๊ฐ€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋“ฑ์žฅํ•˜๊ธฐ ํ›จ์”ฌ ์ „๋ถ€ํ„ฐ, Next.js๋‚˜ getServerSideProps ๊ฐ™์€ API๋ฅผ ์‚ฌ์šฉํ•ด ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์—ˆ์ž–์•„์š”.
๊ทธ๋ ‡๋‹ค๋ฉด ๋„๋Œ€์ฒด ๋ญ๊ฐ€ ๋‹ค๋ฅธ ๊ฑธ๊นŒ์š”?

 

์ด ์ฐจ์ด๋ฅผ ์ œ๋Œ€๋กœ ์ดํ•ดํ•œ ๊ฑด, ์—ฌ๋Ÿฌ ํŒจํ„ด์„ ๊ตฌํ˜„ ๊ด€์ ์—์„œ ๋น„๊ตํ•ด ๋ณด๊ณ , ๊ฐ ๋ Œ๋”๋ง ๋ฐฉ์‹์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ํŒจ์นญ ๋˜๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ๊ฐ ๋ฐฉ์‹์ด ์„ฑ๋Šฅ์— ์–ด๋–ค ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š”์ง€๋ฅผ ์ถ”์ ํ•ด ๋ณธ ํ›„์˜€์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ ์ด๋ฒˆ ๊ธ€์—์„œ ๋ฐ”๋กœ ๊ทธ ๋ถ€๋ถ„์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.
CSR (ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง), SSR (์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง), ๊ทธ๋ฆฌ๊ณ  RSC (๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)๊ฐ€ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„๋˜๋Š”์ง€, ๊ฐ ๋ฐฉ์‹์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ์–ด๋–ป๊ฒŒ ์˜ค๊ฐ€๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  CSR → SSR → RSC๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•  ๋•Œ์˜ ์„ฑ๋Šฅ ๋ณ€ํ™”๋ฅผ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

 

์ด๋ฅผ ์œ„ํ•ด ์ œ๊ฐ€ ์ง์ ‘ ๊ฐ„๋‹จํ•˜์ง€๋งŒ ์‹ค์ œ์— ๊ฐ€๊นŒ์šด ๋ฉ€ํ‹ฐ ํŽ˜์ด์ง€ ์•ฑ์„ ๋งŒ๋“ค์–ด ์‹คํ—˜ํ–ˆ์Šต๋‹ˆ๋‹ค. GitHub์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์œผ๋‹ˆ, ์›ํ•˜์‹ ๋‹ค๋ฉด ๋™์ผํ•œ ์‹คํ—˜์„ ์ง์ ‘ ์žฌํ˜„ํ•ด ๋ณด์…”๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

 

์ด ๊ธ€์€ ์—ฌ๋Ÿฌ๋ถ„์ด ์ด๋ฏธ Initial Load, CSR, SSR, Chrome Performance ํƒญ์˜ ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…๊ณผ ์„ฑ๋Šฅ ๋ถ„์„ ๊ทธ๋ž˜ํ”„๋ฅผ ์ฝ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
๋งŒ์•ฝ ๋ณต์Šต์ด ํ•„์š”ํ•˜์‹œ๋‹ค๋ฉด, ์ œ๊ฐ€ ์ถ”์ฒœํ•˜๋Š” ๊ธ€๋“ค์„ ์•„๋ž˜ ์ˆœ์„œ๋Œ€๋กœ ๋จผ์ € ์ฝ์–ด๋ณด์‹œ๋Š” ๊ฑธ ๊ถŒ์žฅ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

  1. Initial load performance for React developers: investigative deep dive
  2. Client-Side Rendering in Flame Graphs
  3. SSR Deep Dive for React Developers

 

์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

๊ฐ€๋ น, ์ƒํ˜ธ์ž‘์šฉ์ ์ด๊ณ  ์‹œ๊ฐ์ ์œผ๋กœ ์•„๋ฆ„๋‹ค์šด ์›น์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ด…์‹œ๋‹ค. ๊ทธ ์›น์‚ฌ์ดํŠธ์˜ ํ•œ ํŽ˜์ด์ง€๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋ฅผ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์ด ํŽ˜์ด์ง€์˜ ์ผ๋ถ€ ๋ฐ์ดํ„ฐ๋Š” ๋™์ ์œผ๋กœ REST ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜ค๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ์™ผ์ชฝ์˜ Sidebar ํ•ญ๋ชฉ๋“ค์€ /api/sidebar ์—”๋“œํฌ์ธํŠธ์—์„œ ๊ฐ€์ ธ์˜ค๊ณ , ์˜ค๋ฅธ์ชฝ์˜ ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ๋Š” /api/messages ์—”๋“œํฌ์ธํŠธ์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

 

/api/sidebar๋Š” ๊ฝค ๋น ๋ฅด๊ฒŒ ์‘๋‹ตํ•˜๋ฉฐ, ์•ฝ 100ms ์ •๋„ ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ /api/messages ์—”๋“œํฌ์ธํŠธ๋Š” 1s ์ •๋„ ๊ฑธ๋ฆฌ๋Š”๋ฐ, ์ด๋Š” ๋ฐฑ์—”๋“œ ์ตœ์ ํ™”๋ฅผ ๊นœ๋นกํ•œ ํƒ“์ด์ฃ . ์ด๋Ÿฐ ์ •๋„์˜ ์ง€์—ฐ์€ ๊ทœ๋ชจ๊ฐ€ ํฌ๊ฑฐ๋‚˜ ์˜ค๋ž˜๋œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ˜„์‹ค์ ์ธ ์ˆ˜์น˜๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด ๊ธ€์„ ๋”ฐ๋ผ ์ง์ ‘ ์ธก์ •ํ•ด๋ณด๊ณ  ์‹ถ์œผ์‹œ๋‹ค๋ฉด GitHub์— ๊ณต๊ฐœ๋œ ํ”„๋กœ์ ํŠธ๋ฅผ ํด๋ก (clone)ํ•œ ๋’ค์— ์˜์กด์„ฑ์„ ์„ค์น˜ํ•˜๊ณ , ๊ฐ ์„น์…˜ ๋์— ๋‚˜์˜ค๋Š” "์žฌํ˜„ ๋ฐฉ๋ฒ•" ๋‹จ๊ณ„๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋”ฐ๋ผ๊ฐ€์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

์šฐ๋ฆฌ๊ฐ€ ์ธก์ •ํ•  ํ•ญ๋ชฉ ์ •์˜ํ•˜๊ธฐ

์„ฑ๋Šฅ๊ณผ ๊ด€๋ จํ•˜์—ฌ ์ธก์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์š”์†Œ๋Š” ์…€ ์ˆ˜ ์—†์ด ๋งŽ์Šต๋‹ˆ๋‹ค. "์ด ์›น์‚ฌ์ดํŠธ์˜ ์„ฑ๋Šฅ์ด ์ข‹๋‹ค" ๋˜๋Š” "๋‚˜์˜๋‹ค"๋ผ๊ณ  ๋งํ•˜๊ธฐ ์ „์— "์„ฑ๋Šฅ"์ด ๋ฌด์—‡์ธ์ง€, "์ข‹์Œ"๊ณผ "๋‚˜์จ"์ด ์–ด๋–ค ๊ธฐ์ค€์ธ์ง€ ๋ช…ํ™•ํžˆ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ด๋ฒˆ ์‹คํ—˜์—์„œ๋Š” ๋‹ค์–‘ํ•œ ๋ Œ๋”๋ง ๋ฐฉ์‹๊ณผ ๋ฐ์ดํ„ฐ ํŒจ์นญ ๊ธฐ๋ฒ• ๊ฐ„์˜ ๋กœ๋“œ ์„ฑ๋Šฅ ์ฐจ์ด๋ฅผ ํ™•์ธํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ธฐ๋ฒ•์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด, “๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ ์‹ค์ œ๋กœ ๊ฐ€์น˜๊ฐ€ ์žˆ์„๊นŒ?”๋ผ๋Š” ์งˆ๋ฌธ์— ๋‹ตํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค.

 

์ธก์ •์—๋Š” Chrome DevTools์˜ Performance ํƒญ์„ ์‚ฌ์šฉํ•  ์˜ˆ์ •์ด๋ฉฐ, CPU๋Š” 6๋ฐฐ ๋А๋ฆฌ๊ฒŒ ์„ค์ •ํ•˜๊ณ  Network๋Š” Slow 4G ํ™˜๊ฒฝ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜น์‹œ ์•„์ง ์ด๊ฒƒ๋“ค์— ์ต์ˆ™ํ•˜์ง€ ์•Š์œผ์‹œ๋‹ค๋ฉด, Initial load performance for React developers: investigative deep dive์—์„œ ์˜ค๋Š˜ ์‚ฌ์šฉํ•  ๋ถ€๋ถ„๋“ค์„ ๊ฐ„๋žตํžˆ ์ •๋ฆฌํ•ด ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ธก์ • ๋Œ€์ƒ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ฒ˜์Œ์œผ๋กœ ๋‹ค์šด๋กœ๋“œ๋˜๋Š” ์ฒซ ๋ฐฉ๋ฌธ์ž์™€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ผ๋ฐ˜์ ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ์—์„œ ์ œ๊ณต๋˜๋Š” ์žฌ๋ฐฉ๋ฌธ์ž ๋ชจ๋‘์ž…๋‹ˆ๋‹ค.

 

์ธก์ •ํ•  ๊ตฌ์ฒด์ ์ธ ํ•ญ๋ชฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Largest Contentful Paint (LCP)๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์ด๋“œ๋ฐ”์™€ ๋ฉ”์‹œ์ง€์˜ "์Šค์ผˆ๋ ˆํ†ค"์œผ๋กœ ๋ Œ๋”๋ง ๋œ ํŽ˜์ด์ง€๋ฅผ ๋ณด๋Š” ์‹œ๊ฐ„๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.
  2. "์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ ํ‘œ์‹œ" ์‹œ๊ฐ„์€ ์„ค๋ช…์ด ํ•„์š” ์—†์„ ์ •๋„๋กœ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ์ด ์—”๋“œํฌ์ธํŠธ์—์„œ ๊ฐ€์ ธ์™€ ํŽ˜์ด์ง€์— ๋ Œ๋”๋ง ๋˜๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค.
  3. "๋ฉ”์‹œ์ง€ ํ‘œ์‹œ" ์‹œ๊ฐ„์€ ์œ„์™€ ๋™์ผํ•˜์ง€๋งŒ ๋ฉ”์‹œ์ง€์—๋งŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.
  4. "ํŽ˜์ด์ง€ ์ƒํ˜ธ ์ž‘์šฉ" ์‹œ๊ฐ„์€ ํ—ค๋”์˜ ํ† ๊ธ€์ด ์ž‘๋™ํ•˜๊ธฐ ์‹œ์ž‘ํ•˜๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค(์ด ๋ถ€๋ถ„์˜ ์ค‘์š”์„ฑ์€ ๋‚˜์ค‘์— ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค).

 

 

๊ฐ ์ธก์ •์€ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ฐ˜๋ณตํ•˜๊ณ , ์ค‘๊ฐ„๊ฐ’์„ ์‚ฌ์šฉํ•ด ์ด์ƒ์น˜๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๊ณง HTTP/1์˜ ๋™์‹œ ์—ฐ๊ฒฐ ์ œํ•œ ๋ฌธ์ œ๋ฅผ ๊ฒฝํ—˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋กœ์ปฌ Node ๊ธฐ๋ฐ˜ ์„œ๋ฒ„(Next.js ํฌํ•จ)๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ HTTP/1์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Chrome์—์„œ๋Š” ๋™์‹œ ์—ฐ๊ฒฐ ์ œํ•œ์ด 6์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค! HTTP/1์€ ๊ฐ™์€ ๋„๋ฉ”์ธ์—์„œ 6๊ฐœ ์ด์ƒ์˜ ๋ฆฌ์†Œ์Šค(์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ๋“ฑ)๋ฅผ ๋™์‹œ์— ๋‹ค์šด๋กœ๋“œํ•˜๋ฉด, ๋‚˜๋จธ์ง€๋Š” "๋Œ€๊ธฐ์—ด"์— ๋“ค์–ด๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

 

ํ•˜์ง€๋งŒ ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋Œ€๋ถ€๋ถ„ ํŒŒ์ผ์„ CDN์„ ํ†ตํ•ด ์ œ๊ณตํ•˜๋ฉฐ, ์š”์ฆ˜์€ HTTP/2 ๋˜๋Š” HTTP/3์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๋ชจ๋“  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์ด ๋ณ‘๋ ฌ๋กœ ๋‹ค์šด๋กœ๋“œ๋˜๋ฏ€๋กœ, ๋กœ์ปฌ ํ…Œ์ŠคํŠธ์—์„œ๋„ CDN๊ณผ ์œ ์‚ฌํ•œ ๋™์ž‘์„ ์žฌํ˜„ํ•ด ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ์ €๋Š” ์ด๋ฅผ ์œ„ํ•ด Caddy๋ฅผ ์ด์šฉํ•œ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ์œ ์‚ฌํ•œ ๋„๊ตฌ๋ผ๋ฉด ๋ฌด์—‡์ด๋“  ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

 

์ด์ œ ๋ชจ๋“  ์ค€๋น„๊ฐ€ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์‹คํ—˜์„ ์‹œ์ž‘ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์ธก์ •

๋จผ์ € ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์˜ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•ด ๋ด…์‹œ๋‹ค. ์—ฌ๋Ÿฌ๋ถ„์ด ํƒœ์–ด๋‚œ ์—ฐ๋„์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒ ์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์€ ๋งŽ์€ ๋ฆฌ์•กํŠธ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๊ธฐ๋ณธ์ ์ธ ์›น ๊ฒฝํ—˜์ด์—ˆ๊ฑฐ๋‚˜, ์ง€๊ธˆ๋„ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์—ฌ๋Ÿฌ๋ถ„์ด Webpack ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ฑฐ๋‚˜, Vite + router ์กฐํ•ฉ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ SSR(์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง)์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๊ทธ๊ฑด ๋ฐ”๋กœ CSR(ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง)์„ ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๊ตฌํ˜„ ๊ด€์ ์—์„œ, ์—ฌ๋Ÿฌ๋ถ„์˜ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ /inbox URL์„ ์š”์ฒญํ•˜๋ฉด, ์„œ๋ฒ„๋Š” ๋‹ค์Œ HTML์„ ์‘๋‹ตํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module" src="/assets/index-C3kWgjO3.js"></script>
    <link rel="stylesheet" href="/assets/index-C26Og_lN.css" />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

 

head ํƒœ๊ทธ์—๋Š” script์™€ link ์š”์†Œ๊ฐ€ ์žˆ๊ณ  body ํƒœ๊ทธ์—๋Š” ๋นˆ div๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๊ฒŒ ์ „๋ถ€์ž…๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด ๋นˆ div์—์„œ ์˜ˆ์ƒํ–ˆ๋“ฏ์ด ๋นˆ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

 

์ด ๋นˆ div๋ฅผ ์•„๋ฆ„๋‹ค์šด ํŽ˜์ด์ง€๋กœ ๋ณ€ํ™˜ํ•˜๋ ค๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ(๋“ค)์„ ๋‹ค์šด๋กœ๋“œํ•˜์—ฌ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ(๋“ค)์—๋Š” ๋ฆฌ์•กํŠธ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•˜๋Š” ๋ชจ๋“  ๋‚ด์šฉ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

 

// ์ด๊ณณ์ด ์•„๋ฆ„๋‹ค์šด ์•ฑ์˜ ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค.
export default function App() {
  return (
    <SomeLayout>
      <Sidebar />
      <MainContent />
    </SomeLayout>
  )
}

 

๊ฒŒ๋‹ค๊ฐ€ ์ด๋Ÿฐ ๊ฒƒ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

// ๋‹จ์ˆœํ™”๋ฅผ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ API์ž…๋‹ˆ๋‹ค.
const DOMElements = renderToDOM(<App />)
const root = document.getElementById('root')
root.appendChild(DOMElements)

 

๋ฆฌ์•กํŠธ๋Š” ์ง„์ž…์  App ์ปดํฌ๋„ŒํŠธ๋ฅผ DOM ๋…ธ๋“œ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ id๋กœ ๋นˆ div๋ฅผ ์ฐพ์•„ ์ƒ์„ฑ๋œ ์š”์†Œ๋ฅผ ๋นˆ div์— ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.

 

์ „์ฒด ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋งˆ์นจ๋‚ด ๋ณด์ž…๋‹ˆ๋‹ค.

 

์ดˆ๊ธฐ ๋ถ€ํ•˜์— ๋Œ€ํ•œ ์„ฑ๋Šฅ์„ ๊ธฐ๋กํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

 

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ์‚ฌ์šฉ์ž๋Š” ์—ฌ์ „ํžˆ ๋นˆ ํ™”๋ฉด์„ ์‘์‹œํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒƒ์ด ๋‹ค์šด๋กœ๋“œ๋˜๊ณ  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ปดํŒŒ์ผ ๋ฐ ์‹คํ–‰๋œ ํ›„์—์•ผ UI๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  LCP ๋ฉ”ํŠธ๋ฆญ์ด ๊ธฐ๋ก๋˜๋ฉฐ, fetch ์š”์ฒญ๊ณผ ๊ฐ™์€ ๋ถ€์ˆ˜ ํšจ๊ณผ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

๋ฌผ๋ก  ์‹ค์ œ๋กœ๋Š” ํ›จ์”ฌ ๋” ๋ณต์žกํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ(๋•Œ๋กœ๋Š” ์ฒด์ด๋‹ ๋˜์–ด์žˆ์Œ), CSS ํŒŒ์ผ๊ณผ Main ์„น์…˜์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ž‘์—… ๋“ฑ์ด ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์˜ ์‹ค์ œ ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ์„ ๊ธฐ๋กํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

 

์‚ฌ์ด๋“œ๋ฐ”์™€ ๋ฉ”์‹œ์ง€ ํ•ญ๋ชฉ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋‚ด๋ถ€์—์„œ ํŠธ๋ฆฌ๊ฑฐ ๋ฉ๋‹ˆ๋‹ค.

 

useEffect(() => {
  const fetchMessages = async () => {
    const response = await fetch('/api/messages')
    const data = await response.json()
  }

  fetchMessages()
}, [])

 

์˜ˆ๋ฅผ ๋“ค์–ด Tanstack Query์™€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ํŒจ์นญ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

const { isPending, error, data } = useQuery({
  queryKey: ['messages'],
  queryFn: () => fetch('/api/messages').then((res) => res.json()),
})

 

ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ ์ด๊ฒƒ์€ ๊ทธ๋ฆฌ ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ ๋ฐ์ดํ„ฐ ํŒจ์นญ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์‹œ์ž‘๋˜๋ ค๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜๊ณ  ์ปดํŒŒ์ผ๋˜์–ด ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์บ์‹œ ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ์ดˆ๊ธฐ ๋กœ๋“œ ์ˆ˜์น˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X) ๋ฉ”์‹œ์ง€ (์บ์‹œ X)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s 4.7s 5.1s

 

4.1s๋ฅผ ๊ธฐ๋‹ค๋ ค์•ผ ํ™”๋ฉด์— ๋ฌด์–ธ๊ฐ€ ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค๋‹ˆ! ๋ˆ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ Œ๋”๋ง ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์€ ์•„์ด๋””์–ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์„๊นŒ์š”?

 

๊ฐœ๋ฐœ ๊ฒฝํ—˜๊ณผ ํ•™์Šต ๊ณก์„ ์— ๊ด€๋ จ๋œ ๋ชจ๋“  ๊ฒƒ๋“ค(์ด๊ฒƒ๋“ค๋งŒ ํ•ด๋„ ๋งค์šฐ ์ค‘์š”ํ•œ ์ด์Šˆ์ด์ง€๋งŒ)์„ ์ œ์™ธํ•˜๊ณ ๋„, ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์—๋Š” ๋” "์ „ํ†ต์ ์ธ" ์›น์‚ฌ์ดํŠธ ๋ฐฉ์‹๊ณผ ๋น„๊ตํ•˜์—ฌ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ฒซ์งธ๋Š” ์„ฑ๋Šฅ์ž…๋‹ˆ๋‹ค! ๋ชจ๋“  ๊ฒƒ์ด ํด๋ผ์ด์–ธํŠธ ์ธก์— ์žˆ๊ณ  ์„œ๋ฒ„์™€ ์˜ค๊ณ  ๊ฐ€๋Š” ํ†ต์‹ ์ด ์—†์„ ๋•Œ, ํŽ˜์ด์ง€ ๊ฐ„ ์ „ํ™˜ ์†๋„๋Š” ๋ฏฟ์„ ์ˆ˜ ์—†์„ ์ •๋„๋กœ ๋น ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ, Inbox ํŽ˜์ด์ง€์—์„œ Settings ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋ฐ๋Š” ๋‹จ 80ms๋งŒ์ด ์†Œ์š”๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ฑฐ์˜ ์ฆ‰๊ฐ์ ์ธ ์ˆ˜์ค€์— ๊ฐ€๊น์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๋‘˜์งธ๋Š” ๋น„์šฉ์ž…๋‹ˆ๋‹ค. ํ„ฐ๋ฌด๋‹ˆ์—†์ด ์ €๋ ดํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ๋ถ„์€ ์ •๋ง ๋ณต์žกํ•˜๊ณ , ๊ณ ๋„๋กœ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋ฉฐ, ํ’๋ถ€ํ•œ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ Cloudflare CDN ๊ฐ™์€ ๊ณณ์— ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋ฐฑ๋งŒ ๋ช…์˜ ์›”๊ฐ„ ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ์–ด๋„ ๋ฌด๋ฃŒ ํ”Œ๋žœ(free plan)์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์ฃ . ์ด๋Š” ์ทจ๋ฏธ ํ”„๋กœ์ ํŠธ, ํ•™์ƒ ํ”„๋กœ์ ํŠธ, ๋˜๋Š” ์ž ์žฌ์  ์‚ฌ์šฉ์ž๋Š” ๋งŽ์ง€๋งŒ ๋น„์šฉ์ด ์ค‘์š”ํ•œ ์š”์†Œ์ธ ๋ชจ๋“  ์ข…๋ฅ˜์˜ ํ”„๋กœ์ ํŠธ์— ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค.

 

๊ฒŒ๋‹ค๊ฐ€ ์„œ๋ฒ„๊ฐ€ ์—†๊ณ , ์œ ์ง€ ๋ณด์ˆ˜๊ฐ€ ํ•„์š” ์—†์œผ๋ฉฐ, CPU๋‚˜ ๋ฉ”๋ชจ๋ฆฌ ๋ชจ๋‹ˆํ„ฐ๋ง๋„ ํ•  ํ•„์š”๊ฐ€ ์—†๊ณ , ํ™•์žฅ์„ฑ ๋ฌธ์ œ๋„ ์—†๋‹ค๋Š” ๊ฒƒ์€ ์•„์ฃผ ์ข‹์€ ๋ณด๋„ˆ์Šค์ž…๋‹ˆ๋‹ค. ๋งˆ๋‹คํ•  ์ด์œ ๊ฐ€ ์žˆ์„๊นŒ์š”?

 

๋˜ํ•œ, 4s๊ฐ€ ๋„˜๋Š” ๋กœ๋”ฉ ์‹œ๊ฐ„(loading times)๋„ ๊ฒ‰๋ณด๊ธฐ๋งŒํผ ๋”์ฐํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ๊ฐ€์žฅ ์ฒ˜์Œ ๋ฐฉ๋ฌธํ•  ๋•Œ๋งŒ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ๋žœ๋”ฉ ํŽ˜์ด์ง€ ๊ฐ™์€ ๊ณณ์—์„œ๋Š” ์ด๊ฒƒ์ด ์šฉ๋‚ฉํ•  ์ˆ˜ ์—†๋Š” ์ˆ˜์ค€์ด๊ฒ ์ง€๋งŒ, ์‚ฌ์šฉ์ž๊ฐ€ ์›น์‚ฌ์ดํŠธ๋ฅผ ์ž์ฃผ ๋ฐฉ๋ฌธํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜๋Š” SaaS์˜ ๊ฒฝ์šฐ, 4s๋Š” ๋ฐฐํฌ๋‹น ๋‹จ ํ•œ ๋ฒˆ๋งŒ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ํ›„์—๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์— ์˜ํ•ด ๋‹ค์šด๋กœ๋“œ๋˜๊ณ  ์บ์‹œ ๋˜์–ด, ๋‘ ๋ฒˆ์งธ ๋ฐ ์ดํ›„ ๋กœ๋”ฉ ์ˆ˜์น˜๋Š” ์ƒ๋‹นํžˆ ๊ฐ์†Œํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X) ๋ฉ”์‹œ์ง€ (์บ์‹œ X) LCP (JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s 4.7s 5.1s 800ms 1.5s 2s

 

800ms๋Š” ํ›จ์”ฌ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š๋‚˜์š”?

 

์˜ค๋Š˜ ์ œ๊ฐ€ ๋งˆ์ง€๋ง‰์œผ๋กœ ๊ด€์‹ฌ์„ ๊ฐ€์งˆ ์ˆ˜์น˜๋Š” ํ† ๊ธ€ ๋ฒ„ํŠผ์ด ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•ด์ง€๋Š” ์‹œ์ ์ž…๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ๋น„๋กœ์†Œ ๋ชจ๋“  ๊ฒƒ์ด ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด ์‹œ๊ฐ„์€ LCP์™€ ์ผ์น˜ํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๋”ฐ๋ผ์„œ ์ „์ฒด ํ…Œ์ด๋ธ”์€ ๋‹ค์Œ๊ณผ ๊ฐ™์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms

 

์‹คํ—˜์„ ์žฌํ˜„ํ•˜๋Š” ๋‹จ๊ณ„

  1. ์ €์žฅ์†Œ๋ฅผ ํด๋ก ํ•˜๊ณ  npm install๋กœ ๋ชจ๋“  ์˜์กด์„ฑ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฐฑ์—”๋“œ API๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: npm run start --workspace=backend-api
  3. ํ”„๋ŸฐํŠธ์—”๋“œ๋ฅผ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค: npm run build --workspace=client-fetch-frontend
  4. ํ”„๋ŸฐํŠธ์—”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: npm run start --workspace=client-fetch-frontend
  5. HTTP/2๋ฅผ ์œ„ํ•œ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: caddy reverse-proxy --to :3000
  6. ์›น์‚ฌ์ดํŠธ๋ฅผ https://localhost/inbox ์ฃผ์†Œ๋กœ ์—ฝ๋‹ˆ๋‹ค.
  7. ์ธก์ •ํ•˜์„ธ์š”!

 

์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์ธก์ • (๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ฏธํฌํ•จ)

์‚ฌ์šฉ์ž๋“ค์ด ๋นˆ ํŽ˜์ด์ง€๋ฅผ ๋„ˆ๋ฌด ์˜ค๋žซ๋™์•ˆ ์ณ๋‹ค๋ด์•ผ ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์€ ์–ด๋А ์‹œ์ ๋ถ€ํ„ฐ ์‚ฌ๋žŒ๋“ค์„ ์งœ์ฆ๋‚˜๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋น„๋ก ์ด๊ฒƒ์ด ์ฒซ ๋ฐฉ๋ฌธ์—์„œ๋งŒ ๋ฐœ์ƒํ•˜๋Š” ์ผ์ด๋ผ ํ•˜๋”๋ผ๋„ ๋ง์ด์ฃ . ๊ฒŒ๋‹ค๊ฐ€, SEO ๊ด€์ ์—์„œ๋„ ์ตœ์„ ์˜ ํ•ด๊ฒฐ์ฑ…์€ ์•„๋‹ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‹น์‹œ์—๋Š” ์ธํ„ฐ๋„ท ์†๋„๊ฐ€ ๋” ๋А๋ ธ๊ณ , ์‚ฌ์šฉ์ž๋“ค์ด ์ตœ์‹  ๋งฅ๋ถ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€๋„ ์•Š์•˜์ฃ .

 

๊ทธ๋ž˜์„œ ์‚ฌ๋žŒ๋“ค์€ ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ๊ธฐ ์œ„ํ•ด ๋จธ๋ฆฌ๋ฅผ ๋งž๋Œ€๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ ์ƒํƒœ๊ณ„๋ฅผ ํฌ๊ธฐํ•˜๊ธฐ์—๋Š” ๋„ˆ๋ฌด๋‚˜ ํŽธ๋ฆฌํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด์ฃ .

 

์šฐ๋ฆฌ๋Š” ์ „์ฒด ๋ฆฌ์•กํŠธ ์•ฑ์ด ์ตœ์ข…์ ์œผ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋ฅผ ๋ ๊ฒŒ ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

// ๋‹จ์ˆœํ™”๋ฅผ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ API์ž…๋‹ˆ๋‹ค.
const DOMElements = renderToDOM(<App />)

 

ํ•˜์ง€๋งŒ DOM ๋…ธ๋“œ ๋Œ€์‹  ๋ฆฌ์•กํŠธ๊ฐ€ ์•ฑ์˜ HTML์„ ์ƒ์„ฑํ•œ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”?

 

const HTMLString = renderToString(<App />)

 

์„œ๋ฒ„๊ฐ€ ๋นˆ div ๋Œ€์‹  ๋ธŒ๋ผ์šฐ์ €์— ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ์‹ค์ œ ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ์š”.

 

// HTMLString์€ ๋‹ค์Œ ๋ฌธ์ž์—ด์„ ๋‹ด๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
<div class="...">
  <div class="...">...</div>
  ...
</div>

์ด๋ก ์ƒ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ์œ„ํ•œ ๋งค์šฐ ๊ฐ„๋‹จํ•œ ์„œ๋ฒ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// ๋„ค, ๊ธฐ๋ณธ์ ์ธ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ Œ๋”๋ง์„ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฒƒ์€ ์ด๊ฒƒ์ด ์ „๋ถ€์ž…๋‹ˆ๋‹ค.
export const serveStatic = async (c) => {
  const html = fs.readFileSync('index.html').toString()

  return c.body(html, 200)
}

 

๋‹จ์ˆœํ•˜๊ฒŒ ์ƒ๊ฐํ•ด ๋ณธ๋‹ค๋ฉด ๋‹จ์ง€ ํ•œ ๊ฐ€์ง€ ์ถ”๊ฐ€ ๋‹จ๊ณ„๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๋ฐ”๋กœ html ๋ณ€์ˆ˜ ๋‚ด์˜ ๋ฌธ์ž์—ด์„ ์ฐพ์•„์„œ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

// SSR์ด ์ ์šฉ๋œ ๋™์ผํ•œ ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค.
export const serveStatic = async (c) => {
  const html = fs.readFileSync('index.html').toString() // HTML ๋ฌธ์ž์—ด ์ถ”์ถœ
  const HTMLString = renderToString(<App />)
  // ์„œ๋ฒ„ ์‘๋‹ต์— ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
  const htmlWithSSR = html.replace('<div id="root"></div>', HTMLString)

  return c.body(htmlWithSSR, 200)
}

 

์ด์ œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์ด, ์ „์ฒด UI๊ฐ€ ๋ฐ”๋กœ ์ฒ˜์Œ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

 

๋ฆฌ์•กํŠธ์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR) ๋ฐ ์ •์  ์‚ฌ์ดํŠธ ์ƒ์„ฑ(SSG) ์‹œ๋Œ€์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด renderToString์€ ์‹ค์ œ๋กœ ๋ฆฌ์•กํŠธ์—์„œ ์ง€์›ํ•˜๋Š” ์‹ค์ œ API์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ฐ”๋กœ ์ผ๋ถ€ ๋ฆฌ์•กํŠธ SSG/SSR ํ”„๋ ˆ์ž„์›Œํฌ์˜ ํ•ต์‹ฌ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ ์ œ๊ฐ€ ์ด ๋ฐฉ์‹์„ ํ˜„์žฌ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ํ”„๋กœ์ ํŠธ์— ๊ทธ๋Œ€๋กœ ์ ์šฉํ•œ๋‹ค๋ฉด, ์ด ํ”„๋กœ์ ํŠธ๋Š” ์„œ๋ฒ„ ์ธก ๋ Œ๋”๋ง ํ”„๋กœ์ ํŠธ๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ์€ ์•ฝ๊ฐ„ ๋‹ฌ๋ผ์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. LCP ์ˆ˜์น˜๋Š” ์™ผ์ชฝ์œผ๋กœ, ์ฆ‰ HTML๊ณผ CSS๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋œ ์งํ›„๋กœ ์ด๋™ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ „์ฒด HTML์ด ์ดˆ๊ธฐ ์„œ๋ฒ„ ์‘๋‹ต์œผ๋กœ ์ „์†ก๋˜๊ณ , ๋ชจ๋“  ๊ฒƒ์ด ์ฆ‰์‹œ ๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

 

์—ฌ๊ธฐ์„œ ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ฒซ์งธ, ๋ณด์‹œ๋‹ค์‹œํ”ผ LCP ์ˆ˜์น˜(ํŽ˜์ด์ง€์˜ "์Šค์ผˆ๋ ˆํ†ค"์ด ๋ณด์ด๋Š” ์‹œ์ )๋Š” ๊ทน์ ์œผ๋กœ ๊ฐœ์„ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค (์ด๊ฒƒ์€ ์ž ์‹œ ํ›„์— ์ธก์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค).

 

ํ•˜์ง€๋งŒ, ์šฐ๋ฆฌ๋Š” ์—ฌ์ „ํžˆ ๋™์ผํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ •ํ™•ํžˆ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ , ์ปดํŒŒ์ผํ•˜๊ณ , ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ํŽ˜์ด์ง€๋Š” ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์šฐ๋ฆฌ๊ฐ€ ๊ตฌํ˜„ํ•œ ๋ชจ๋“  ๋“œ๋กญ๋‹ค์šด, ํ•„ํ„ฐ, ์ •๋ ฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋“ฑ์ด ์ž‘๋™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ์—๋„ ์ „์ฒด ํŽ˜์ด์ง€๋Š” ์ด๋ฏธ ๋ณด์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค!

 

ํŽ˜์ด์ง€๋Š” ์ด๋ฏธ ๋ณด์ด์ง€๋งŒ, ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋‹ค์šด๋กœ๋“œ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ทธ ๊ฐ„๊ทน์€ ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ํŽ˜์ด์ง€๊ฐ€ ๊ณ ์žฅ ๋‚œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์ œ๊ฐ€ "ํŽ˜์ด์ง€๊ฐ€ ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•ด์ง€๋Š”" ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๊ณ  ์‹ถ์—ˆ๋˜ ์ด์œ ์ž…๋‹ˆ๋‹ค. ์ด ์‹œ๊ฐ„ ๋™์•ˆ์—๋Š” ํ—ค๋”์— ์žˆ๋Š” ๋ฉ‹์ง„ ํ† ๊ธ€ ๋ฒ„ํŠผ์ด ์ž‘๋™ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

๋˜ํ•œ, ์œ„ ํ”„๋กœํŒŒ์ผ์—์„œ๋Š” LCP ์ˆ˜์น˜๋งŒ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. "์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ"๊ณผ "๋ฉ”์‹œ์ง€ ๋ชฉ๋ก"์€ ๊ตฌ์กฐ์ ์œผ๋กœ ๋งํ•˜๋ฉด ์ •ํ™•ํžˆ ๊ฐ™์€ ์œ„์น˜์— ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ฝ”๋“œ๋ฅผ ๋‹จ ํ•˜๋‚˜๋„ ๋ฐ”๊พธ์ง€ ์•Š์•˜์œผ๋ฉฐ, ์—ฌ์ „ํžˆ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ ์ฝ”๋“œ์˜ ๊นŠ์€ ์–ด๋”˜๊ฐ€์—๋Š” ์—ฌ์ „ํžˆ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

const Sidebar = () => {
  useEffect(() => {
    const fetchSidebarData = async () => {
      const response = await fetch('/api/sidebar')
      const data = await response.json()
      setSidebarData(data)
    }

    fetchSidebarData()
  }, [])
}

 

useEffect๋Š” ๋น„๋™๊ธฐ์ ์ธ ๋ถ€์ˆ˜ ํšจ๊ณผ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์•ฑ์ด ๋ธŒ๋ผ์šฐ์ €์— ์ œ๋Œ€๋กœ ๋งˆ์šดํŠธ ๋˜์—ˆ์„ ๋•Œ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฆฌ์•กํŠธ๊ฐ€ ์ž‘๋™ํ•  ๋•Œ๋งŒ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. renderToString์€ ์ด๋ฅผ ๋ฌด๊ด€ํ•œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•˜๊ณ  ๊ฑด๋„ˆ๋›ฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๊ฒฐ๊ณผ์ ์œผ๋กœ, ์šฐ๋ฆฌ๊ฐ€ ์„œ๋ฒ„์—์„œ ํŽ˜์ด์ง€๋ฅผ ์‚ฌ์ „ ๋ Œ๋”๋งํ–ˆ๋‹ค๋Š” ์‚ฌ์‹ค์€ ์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ๊ณผ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๋Š” ์‹œ๊ฐ„์— ๋‹จ ํ•˜๋‚˜์˜ ์˜ํ–ฅ๋„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค!

 

์ด์ œ ์ด๋ก ์ ์ธ ๋…ผ์˜ ๋Œ€์‹  ์‹ค์ œ๋กœ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ์ธก์ •ํ•ด ๋ณด๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ˆ˜์น˜๋“ค์„ ๋ณด๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms

 

๋ณด์‹œ๋‹ค์‹œํ”ผ, ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ LCP ๊ฐ’์€ ์‹ค์ œ๋กœ ๊ธ‰๊ฒฉํ•˜๊ฒŒ ๋–จ์–ด์กŒ์Šต๋‹ˆ๋‹ค. 4.1s์—์„œ 1.61s๋กœ ๋ง์ด์ฃ ! ์ด๋Š” ์ด๋ก ์ ์ธ ์ถ”๋ก ๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ํ† ๊ธ€์ด ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•ด์ง€๋Š” ์‹œ๊ฐ„์€ ์ถ”๋ก ์—์„œ ์˜ˆ์ƒํ–ˆ๋˜ ๋Œ€๋กœ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ 2์ดˆ ์ด์ƒ ๋™์•ˆ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๊ฑฐ์˜ ๋ง๊ฐ€์ง„ ์ƒํƒœ์ธ ๊ฒƒ์ž…๋‹ˆ๋‹ค!

 

"์ƒํ˜ธ์ž‘์šฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ" ์ด ๊ตฌ๊ฐ„์€, ์„œ๋ฒ„๋ฅผ ์šด์˜ํ•˜๋Š” ๋น„์šฉ๊ณผ ๋”๋ถˆ์–ด ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์—์„œ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์œผ๋กœ ์ „ํ™˜ํ•  ๋•Œ LCP ๊ฐœ์„ ์„ ์œ„ํ•ด ๊ฐ์ˆ˜ํ•ด์•ผ ํ•˜๋Š” ๋Œ€๊ฐ€์ž…๋‹ˆ๋‹ค. ์™„์ „ํžˆ ์—†์•จ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ์—†์ง€๋งŒ, ์ฒซ ์‹คํ–‰ ์‹œ ๋‹ค์šด๋กœ๋“œํ•ด์•ผ ํ•˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ์–‘์„ ์ค„์ž„์œผ๋กœ์จ ๊ทธ ์˜ํ–ฅ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์‹คํ—˜์„ ์žฌํ˜„ํ•˜๋Š” ๋‹จ๊ณ„

  1. ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋•Œ์™€ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ผํ•œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด๋˜, ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  2. src/frontend/client-fetch/server/index.ts ํŒŒ์ผ๋กœ ์ด๋™ํ•˜์—ฌ return c.html(simpleSSR(c, html)); ๋ผ์ธ์˜ ์ฃผ์„ ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

 

์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์ธก์ • (๋ฐ์ดํ„ฐ ํŒจ์นญ ํฌํ•จ)

"์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ" ๊ตฌ๊ฐ„์„ ์ œ์™ธํ•˜๋”๋ผ๋„, ์ด์ „ ์‹คํ—˜์—๋Š” ๋˜ ๋‹ค๋ฅธ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ๊ณผ ๋ฉ”์‹œ์ง€ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์‹œ์ ์— ์•„๋ฌด๋Ÿฐ ๋ณ€ํ™”๋„ ์—†์—ˆ๋‹ค๋Š” ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฏธ ์„œ๋ฒ„ ์˜์—ญ์— ๋“ค์–ด์„ฐ๋‹ค๋ฉด, ์™œ ์—ฌ๊ธฐ์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์—†์„๊นŒ์š”? ๋ถ„๋ช…ํžˆ ๋” ๋น ๋ฅผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ ์–ด๋„ ์ง€์—ฐ ์‹œ๊ฐ„๊ณผ ๋Œ€์—ญํญ ๋ฉด์—์„œ๋Š” ํ›จ์”ฌ ๊ฐœ์„ ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

 

์ •๋‹ต์€, ์šฐ๋ฆฌ๋Š” ๋‹น์—ฐํžˆ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ํ•˜์ง€๋งŒ, ์šฐ๋ฆฌ๊ฐ€ ์ด์ „์— ํ–ˆ๋˜ ๋‹จ์ˆœํ•œ ์‚ฌ์ „ ๋ Œ๋”๋ง์— ๋น„ํ•˜๋ฉด ๊ตฌํ˜„ ๊ด€์ ์—์„œ ํ›จ์”ฌ ๋” ๋งŽ์€ ์ž‘์—…์ด ํ•„์š”ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฒซ์งธ, ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๊ทธ๊ณณ์—์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

// SSR ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง ์ถ”๊ฐ€
export const serveStatic = async (c) => {
  const html = fs.readFileSync("index.html").toString();

  // ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง
  const sidebarPromise = fetch(`/api/sidebar`).then((res) => res.json());
    const messagesPromise = fetch(`/api/messages`).then((res) => res.json());

    const [sidebar, messages] = await Promise.all([
      sidebarPromise,
      messagesPromise,
    ]);

    ... // ๋‚˜๋จธ์ง€๋Š” ๋™์ผํ•ฉ๋‹ˆ๋‹ค.
};

 

๊ทธ๋‹ค์Œ์—๋Š” ๋ฆฌ์•กํŠธ๊ฐ€ ๋‚˜๋จธ์ง€ UI๋ฅผ ๋ Œ๋”๋ง ํ•  ๋•Œ ํ•ญ๋ชฉ๋“ค์„ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๋„๋ก, ์–ด๋–ป๊ฒŒ๋“  ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹คํ–‰ํžˆ๋„, ๋ณธ์งˆ์ ์œผ๋กœ App ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹จ์ˆœํ•œ ํ•จ์ˆ˜์— ๋ถˆ๊ณผํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋‹ค๋ฅธ ์ผ๋ฐ˜์ ์ธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ์ธ์ž๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ด๊ฒƒ์„ props๋ผ๊ณ  ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋„ค, renderToString์„ ์‹คํ–‰ํ•  ๋•Œ App ์ปดํฌ๋„ŒํŠธ์— ์˜›๋‚  ๋ฐฉ์‹์˜ props๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!

 

// SSR ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง ์ถ”๊ฐ€
export const serveStatic = async (c) => {
  const html = fs.readFileSync('index.html').toString() // ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง
  const sidebarPromise = fetch(`/api/sidebar`).then((res) => res.json())
  const messagesPromise = fetch(`/api/messages`).then((res) => res.json())

  const [sidebar, messages] = await Promise.all([sidebarPromise, messagesPromise])

  // ํŒจ์นญํ•œ ๋ฐ์ดํ„ฐ๋ฅผ props๋กœ ์ „๋‹ฌ
  const HTMLString = renderToString(<App messages={messages} sidebar={sidebar} />)
}

 

๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ์˜ App ์ปดํฌ๋„ŒํŠธ๋Š” props๋ฅผ ๋ฐ›๋„๋ก ์ˆ˜์ •๋˜์–ด์•ผ ํ•˜๋ฉฐ, ์ผ๋ฐ˜์ ์ธ prop drilling ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด props๋“ค์„ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

// ์ด๊ณณ์ด ์•„๋ฆ„๋‹ค์šด ์•ฑ์˜ ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค.
export default function App({ sidebar, messages }) {
  return (
    <SomeLayout>
      <Sidebar data={sidebar} />
      <MainContent data={messages} />
    </SomeLayout>
  )
}

 

์ด๋ก ์ ์œผ๋กœ๋Š” ์ด๊ฒƒ๋งŒ์œผ๋กœ๋„ ์ด๋ฏธ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ์—ฌ๊ธฐ์„œ ๋ช‡ ๊ฐ€์ง€๋ฅผ ๋” ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ฒซ์งธ๋Š” "ํ•˜์ด๋“œ๋ ˆ์ด์…˜(hydration)"์ž…๋‹ˆ๋‹ค. SSR ์˜์—ญ์—์„œ "ํ•˜์ด๋“œ๋ ˆ์ด์…˜"์ด๋ž€, ๋ฆฌ์•กํŠธ๊ฐ€ ์„œ๋ฒ„์—์„œ ๋ณด๋‚ธ ๊ธฐ์กด HTML์„ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋ถ€์ฐฉํ•˜๋Š” ๊ณผ์ •์„ ๋งํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ด๋“œ๋ ˆ์ด์…˜์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋ ค๋ฉด, ์„œ๋ฒ„์—์„œ ์˜ค๋Š” HTML์ด ํด๋ผ์ด์–ธํŠธ ์ธก์˜ HTML๊ณผ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Š” ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์•„์ง ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด์ฃ . ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง„ ์ชฝ์€ ์˜ค์ง ์„œ๋ฒ„๋ฟ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ HTML์„ ์ „์†กํ•  ๋•Œ ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•ด์•ผ, ๋ฆฌ์•กํŠธ ์ดˆ๊ธฐํ™” ์‹œ์ ์— ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋กœ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•์€ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์Šคํฌ๋ฆฝํŠธ ํƒœ๊ทธ ํ˜•ํƒœ๋กœ HTML์— ์‚ฝ์ž…ํ•˜๊ณ , ์ด๋ฅผ window์— ๊ฐ์ฒด๋กœ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

const htmlWithData = `
    <script>window.__SSR_DATA__ = ${JSON.stringify({
    sidebar,
    messages,
  })}</script>
    ${HTMLString}`

 

์ด์ œ ํ”„๋ŸฐํŠธ์—”๋“œ์—์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ window.__SSR_DATA__.sidebar ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์ง€๋ฉฐ, ์šฐ๋ฆฌ๋Š” ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์™€์„œ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

export default function App({ messages, sidebar }) {
  // renderToString๋กœ ์ง์ ‘ ์ „๋‹ฌํ•œ๋‹ค๋ฉด props๋Š” ์—ฌ๊ธฐ์— ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค.
  const sidebarData = typeof window === 'undefined' ? sidebar : window.__SSR_DATA__?.sidebar
  const messagesData = typeof window === 'undefined' ? messages : window.__SSR_DATA__?.messages

  return (
    <SomeLayout>
      <Sidebar data={sidebarData} />
      <MainContent data={messagesData} />
    </SomeLayout>
  )
}

 

๊ทธ๋ฆฌ๊ณ  ์ด๊ฒƒ์€ ์‹ค์ œ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค! ์„ฑ๋Šฅ ๋„ํ‘œ๋Š” ๋‹ค์‹œ ํ•œ๋ฒˆ ๋ณ€ํ™”ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

 

์ด์ œ ์ด์ „์—๋Š” ๋™์ ์ด์—ˆ๋˜ ํ•ญ๋ชฉ๋“ค์„ ํฌํ•จํ•˜์—ฌ ์ „์ฒด ํŽ˜์ด์ง€๊ฐ€ CSS ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋˜๋Š” ์ฆ‰์‹œ ๋ณด์ด๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ ํ›„์—๋Š” ์šฐ๋ฆฌ๋Š” ์ด์ „๊ณผ ์™„์ „ํžˆ ๋™์ผํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๋ฉฐ, ๊ทธ ํ›„์—์•ผ ํŽ˜์ด์ง€๊ฐ€ ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

 

์ด์ œ ์ˆ˜์น˜๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.16s / 1.24s 2.16s / 1.24s 2.16s / 1.24s 4.6s / 1.4s 2.44s / 150ms

 

LCP ๊ฐ’์€ ์•ˆํƒ€๊น๊ฒŒ๋„ ์ €ํ•˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋†€๋ผ์šด ์ผ์ด ์•„๋‹™๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ๋ฆฌ์•กํŠธ ๋ถ€๋ถ„์„ ์‚ฌ์ „ ๋ Œ๋”๋ง ํ•˜๊ธฐ ์ „์—, ์ด์ œ๋Š” ๋ฐ์ดํ„ฐ ํŒจ์นญ์˜ Promise๊ฐ€ ์™„๋ฃŒ๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

// SSR ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง ์ถ”๊ฐ€
export const serveStatic = async (c) => {

  const sidebarPromise = fetch(`/api/sidebar`).then((res) => res.json());
    const statisticsPromise = fetch(`/api/statistics`).then((res) => res.json());

    // ๋‘ ์š”์ฒญ ๋ชจ๋‘๋ฅผ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.
    const [sidebar, statistics] = await Promise.all([
      sidebarPromise,
      statisticsPromise,
    ]);

    ... // ๋‚˜๋จธ์ง€ ์„œ๋ฒ„ ์ฝ”๋“œ ์ƒ๋žต
};

 

๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ๋Š” ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ Œ๋”๋ง ํ•˜๊ธฐ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ๊ทธ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ๋ฐ˜๋“œ์‹œ ๊ธฐ๋‹ค๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์‚ฌ์ด๋“œ๋ฐ”์™€ ๋ฉ”์‹œ์ง€ ํ•ญ๋ชฉ์€ ์ด์ œ ํ›จ์”ฌ ๋” ๋น ๋ฅด๊ฒŒ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. 5.1s ๋Œ€์‹  2.16s ๋งŒ์— ๋ง์ด์ฃ . ๋”ฐ๋ผ์„œ ๋งŒ์•ฝ ์ „์ฒด ํŽ˜์ด์ง€ ๋ทฐ์— ๋น„ํ•ด LCP ์ˆ˜์น˜๊ฐ€ ๊ทธ๋ฆฌ ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด, ์ด๊ฒƒ์€ ๊ฐœ์„ ์ด๋ผ๊ณ  ๋ถˆ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜๋Š”, ์šฐ๋ฆฌ๋Š” ์ตœ์†Œํ•œ์˜ ์„ฑ๋Šฅ ์ €ํ•˜๋งŒ์œผ๋กœ ์‚ฌ์ด๋“œ๋ฐ”๋งŒ ๋ฏธ๋ฆฌ ํŒจ์นญ ํ•˜๊ณ  (์ด API ์—”๋“œํฌ์ธํŠธ๋Š” ๊ฝค ๋น ๋ฅด๊ธฐ ๋•Œ๋ฌธ), ๋ฉ”์‹œ์ง€ ๋ถ€๋ถ„์€ ํด๋ผ์ด์–ธํŠธ ์ธก์— ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐ€์žฅ ์ข‹์€ ๊ฒƒ์ด ๋ฌด์—‡์ธ์ง€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ดํ•ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋‚ด๋ฆฌ๋Š” ์ œํ’ˆ ๊ฒฐ์ •(product decision)์ด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์‹คํ—˜์„ ์žฌํ˜„ํ•˜๋Š” ๋‹จ๊ณ„

  1. ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋•Œ์™€ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ผํ•œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด๋˜, ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  2. src/frontend/client-fetch/server/index.ts ํŒŒ์ผ๋กœ ์ด๋™ํ•˜์—ฌ simpleSSRWithHydration(c, html) ๋ผ์ธ์˜ ์ฃผ์„ ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

 

Next.js Pages ("๊ตฌํ˜•" Next.js) ์ธก์ •

ํ˜„์‹ค์ ์œผ๋กœ, ์ด์ „ ์„น์…˜์˜ ์ฝ”๋“œ๋Š” ๋‹น์—ฐํžˆ ํ›จ์”ฌ ๋” ๋ณต์žกํ•ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฒซ์งธ, ์ด๊ฒƒ์€ ๋ฉ€ํ‹ฐ ํŽ˜์ด์ง€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ํŽ˜์ด์ง€์—์„œ๋Š” ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์ด ํ•„์š”ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€, ์šฐ๋ฆฌ๋Š” ๊ฐ ํŽ˜์ด์ง€๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ์‚ฌ์ „ ๋ Œ๋”๋งํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ /login URL์„ ๋กœ๋“œํ•  ๋•Œ ๋ฉ”์ธ ํŽ˜์ด์ง€์˜ HTML์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์€ ๋ง์ด ๋˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๋”ฐ๋ผ์„œ ์ด์ œ ์šฐ๋ฆฌ๋Š” ์„œ๋ฒ„์— ์–ด๋–ค ํ˜•ํƒœ์˜ ๋ผ์šฐํŒ…์„ ๋„์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ตœ์†Œํ•œ ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค ๋‹ค๋ฅธ ์ง„์ž…์ ์„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

๊ธฐ๋ณธ์ ์œผ๋กœ ์šฐ๋ฆฌ๋Š” ์šฐ๋ฆฌ๋งŒ์˜ SSR ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋งŒ๋“ค๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ ์ด์ œ ๊ธฐ์กด์˜ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ „ํ™˜ํ•ด๋„ ์ „ํ˜€ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†๋Š”, "๊ตฌํ˜•" Next.js ๊ฒฝํ—˜์ธ Next.js Pages Router๋ฅผ ์‚ฌ์šฉํ•ด ๋ด…์‹œ๋‹ค. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํฌํ•จ๋œ ๋ฒ„์ „์œผ๋กœ ์ „ํ™˜ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ปค์Šคํ…€ SSR ๊ตฌํ˜„์„ Next.js Pages Router๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๊ธฐ ์œ„ํ•ด, ํŒจ์นญ ๋กœ์ง์„ getServerSideProps ์•ˆ์œผ๋กœ ์˜ฎ๊ธฐ๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์„œ๋ฒ„์—์„œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ ํ•˜๊ธฐ ์œ„ํ•œ ๊ตฌํ˜• Next.js API์ž…๋‹ˆ๋‹ค. props drilling์„ ํฌํ•จํ•œ ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์€ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค! Next.js๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ–ˆ๋˜ renderToString ํ˜ธ์ถœ๊ณผ ์ฐพ์•„ ๋ฐ”๊พธ๊ธฐ(find-and-replace) ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•ด ์ค๋‹ˆ๋‹ค.

์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์ฝ”๋“œ๋Š” ์ด์ œ ๋‹ค์Œ๊ณผ ๊ฐ™์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

export const getServerSideProps = async () => {
  const sidebarPromise = fetch(`/api/sidebar`).then((res) => res.json())
  const messagesPromise = fetch(`/api/messages`).then((res) => res.json())

  const [sidebar, messages] = await Promise.all([sidebarPromise, messagesPromise])

  // props๋ฅผ ํ†ตํ•ด์„œ ํŽ˜์ด์ง€์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  return { props: { messages, sidebar } }
}

 

์•„๋‹ˆ๋ฉด ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ด ์ฝ”๋“œ๋ฅผ ๋‹จ์ˆœํžˆ ์ฃผ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ์„ ํฌํ•จํ•˜์—ฌ ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์€ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ ํŒจ์นญ์˜ ์œ ๋ฌด์— ๊ด€๊ณ„์—†์ด, ์œ„์—์„œ ๋‹ค๋ฃฌ SSR ์„น์…˜๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ธก์ •ํ•˜๊ณ  ๊ธฐ๋กํ•œ ๊ทธ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.16s / 1.24s 2.16s / 1.24s 2.16s / 1.24s 4.6s / 1.4s 2.44s / 150ms
Next.js Pages (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.76s / 800ms 3.7s / 1.5s 4.2s / 2s 3.1s / 900ms 1.34s / 100ms
Next.js Pages (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.15s / 1.15s 2.15s / 1.15s 2.15s / 1.15s 3.5s / 1.25s 1.35s / 100ms

 

๋ณด์‹œ๋‹ค์‹œํ”ผ, ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ LCP ๊ฐ’์€ "์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ" ์‚ฌ๋ก€์— ๋Œ€ํ•œ ์ €์˜ ์ปค์Šคํ…€ ๊ตฌํ˜„๋ณด๋‹ค ์˜คํžˆ๋ ค ์•ฝ๊ฐ„ ๋” ๋‚˜์ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์—, ์‚ฌ์ด๋“œ๋ฐ”์™€ ๋ฉ”์‹œ์ง€๋Š” ์ด ์‚ฌ๋ก€์—์„œ 1s ๋” ์ผ์ฐ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. "์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ" ์‚ฌ๋ก€์—์„œ๋Š” LCP, Sidebar, Messages ์ˆ˜์น˜๊ฐ€ ์ €์˜ ์ปค์Šคํ…€ ๊ตฌํ˜„๊ณผ Next.js ๊ฐ„์— ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ"์˜ ๊ฐ„๊ทน์€ Next.js์—์„œ 1s ์ •๋„ ๋” ์งง์Šต๋‹ˆ๋‹ค.

 

์ด๊ฒƒ์€ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์ด ๋‹ค๋ฅด๊ฒŒ ์ˆ˜ํ–‰๋  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ƒํ™ฉ์„ ๋งค์šฐ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ์œ ์Šค์ผ€์ด์Šค์ž…๋‹ˆ๋‹ค. Next.js๋Š” ์ €์˜ ์ปค์Šคํ…€ ๊ตฌํ˜„๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋งŽ์€ ์ฒญํฌ๋กœ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ถ„ํ• ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ, ์ œ๊ฐ€ ์ดˆ๊ธฐ ๋กœ๋“œ๋ฅผ ์ธก์ •ํ–ˆ์„ ๋•Œ ํ›จ์”ฌ ๋” ๋งŽ์€ ๋ณ‘๋ ฌ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ๋“ค์ด CSS๋กœ๋ถ€ํ„ฐ ์•ฝ๊ฐ„์˜ ๋Œ€์—ญํญ์„ "ํ›”์ณ๊ฐ€๊ฒŒ" ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” CSS ๋‹ค์šด๋กœ๋“œ์— ๋” ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋งŒ๋“ค๊ณ , LCP ๊ฐ’์„ ์•ฝ๊ฐ„ ์ €ํ•˜์‹œํ‚ค๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋‚ณ์Šต๋‹ˆ๋‹ค.

 

๋ฐ˜๋ฉด, ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์„ ๋ณ‘๋ ฌ๋กœ ๋‹ค์šด๋กœ๋“œํ•˜๋ฉด ์ „์ฒด์ ์œผ๋กœ ์•ฝ 1s ์ •๋„ ๋” ๋น ๋ฅด๊ฒŒ ๋กœ๋“œ๋˜์–ด, ํŽ˜์ด์ง€๊ฐ€ ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๊ฐ€ ๋˜๋Š” ์‹œ๊ฐ„์ด ํ›จ์”ฌ ๋‹จ์ถ•๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ๊ณผ์ ์œผ๋กœ, "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ" ๊ฐ„๊ทน์„ ํ›จ์”ฌ ๋” ์งง๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

 

์‹คํ—˜์„ ์žฌํ˜„ํ•˜๋Š” ๋‹จ๊ณ„

  1. ์ €์žฅ์†Œ๋ฅผ ํด๋ก ํ•˜๊ณ  npm install๋กœ ๋ชจ๋“  ์˜์กด์„ฑ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฐฑ์—”๋“œ API๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: npm run start --workspace=backend-api
  3. frontend/utils/link.tsx ํŒŒ์ผ๋กœ ์ด๋™ํ•ด์„œ Next.js Link๋ฅผ ์ฃผ์„ ํ•ด์ œํ•˜๊ณ  ์ปค์Šคํ…€ ๊ตฌํ˜„์„ ์ฃผ์„ ์ฒ˜๋ฆฌ ํ•ฉ๋‹ˆ๋‹ค.
  4. src/frontend/next-pages/pages/inbox.tsx ํŒŒ์ผ๋กœ ์ด๋™ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ํŒจ์นญ ์‚ฌ๋ก€ ๊ฐ„ ์ „ํ™˜์„ ์œ„ํ•ด getServerSideProps๋ฅผ ์ฃผ์„ ์ฒ˜๋ฆฌ/ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.
  5. ํ”„๋ŸฐํŠธ์—”๋“œ๋ฅผ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค: npm run build --workspace=next-pages
  6. ํ”„๋ŸฐํŠธ์—”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: npm run start --workspace=next-pages
  7. HTTP2/3๋ฅผ ์œ„ํ•œ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: caddy reverse-proxy --to :3000
  8. ์›น์‚ฌ์ดํŠธ๋ฅผ https://localhost/inbox ์ฃผ์†Œ๋กœ ์—ฝ๋‹ˆ๋‹ค.

 

๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์†Œ๊ฐœ

์ข‹์Šต๋‹ˆ๋‹ค. ์ด์ „ ์„น์…˜์„ ์š”์•ฝํ•˜์ž๋ฉด, ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ํ•˜๊ณ  ์‚ฌ์ „ ๋ Œ๋”๋ง ํ•˜๋Š” ๊ฒƒ์€ ์ดˆ๊ธฐ ๋กœ๋“œ ์„ฑ๋Šฅ ์ˆ˜์น˜์— ์ •๋ง๋กœ ํฐ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค.

 

SSR์˜ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน"์ž…๋‹ˆ๋‹ค. ์ฆ‰, ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ๋ณด์ด์ง€๋งŒ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์—ฌ์ „ํžˆ ๋‹ค์šด๋กœ๋“œ ๋ฐ ์ดˆ๊ธฐํ™”๋˜๊ณ  ์žˆ๋Š” ์‹œ๊ฐ„์ด์ฃ . ์ด ๊ฐ„๊ทน์„ ๋‹จ์ถ•์‹œํ‚ค๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€ ์ƒํ˜ธ์ž‘์šฉ์— ํ•„์š”ํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ์–‘์„ ์ค„์ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ด๋ฏธ Next.js Pages๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๋ฉด์„œ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์„ ์ˆ˜ํ–‰ํ–ˆ๊ณ , ์ด ์˜์—ญ์—์„œ 1s ์ด์ƒ์˜ ๊ฐœ์„ ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์™ธ์— ์—ฌ๊ธฐ์„œ ๋” ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ์ด ์žˆ์„๊นŒ์š”? ์šฐ๋ฆฌ๋Š” ๊ณผ์—ฐ ๊ทธ ๋ชจ๋“  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ๋Š” ํ•œ ๊ฑธ๊นŒ์š”?

 

SSR์˜ ๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ๋Š” ๋ฐ์ดํ„ฐ ํŒจ์นญ์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํ™ฉ์—์„œ, ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜ํƒ€๋‚˜๋Š” ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฏธ๋ฆฌ ํŒจ์นญ ํ•˜๋ ค ํ•œ๋‹ค๋ฉด, ์ด๋Š” ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ๊ฐ„๊ณผ ์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ์ด ๋‚˜ํƒ€๋‚˜๋Š” ์‹œ๊ฐ„ ๋ชจ๋‘์— ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์น˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

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

 

 

ํ•˜์ง€๋งŒ ๋งŒ์•ฝ ์šฐ๋ฆฌ ์„œ๋ฒ„๊ฐ€ ๋” ๋˜‘๋˜‘ํ•ด์งˆ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ํŒจ์นญ ์š”์ฒญ๋“ค์€ Promise์ด๋ฉฐ, ๋น„๋™๊ธฐ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ๊ธฐ์ˆ ์ ์œผ๋กœ, ์šฐ๋ฆฌ๋Š” ๋‹ค๋ฅธ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ธฐ ์œ„ํ•ด ๊ทธ๋“ค์„ ๊ธฐ๋‹ค๋ฆด ํ•„์š”๊ฐ€ ์ „ํ˜€ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”?

  1. ๊ทธ ํŒจ์น˜ Promise๋“ค์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  2. ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š” ์—†๋Š” ๋ฆฌ์•กํŠธ ์š”์†Œ๋“ค์˜ ๋ Œ๋”๋ง์„ ์‹œ์ž‘ํ•˜๊ณ , ์ค€๋น„๊ฐ€ ๋˜๋ฉด ์ฆ‰์‹œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
  3. ์‚ฌ์ด๋“œ๋ฐ” ํ•ญ๋ชฉ์˜ Promise๊ฐ€ ๋ฐ˜ํ™˜๋˜๊ณ  ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด, ์‚ฌ์ด๋“œ๋ฐ” ๋ถ€๋ถ„์„ ๋ Œ๋”๋ง ํ•˜๊ณ , ์„œ๋ฒ„ ํŽ˜์ด์ง€์— ์ฃผ์ž…ํ•œ ๋‹ค์Œ, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
  4. ๋ฉ”์‹œ์ง€ ํ•ญ๋ชฉ์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

๊ธฐ๋ณธ์ ์œผ๋กœ, ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์—์„œ ๊ฐ€์ง€๊ณ  ์žˆ๋˜ ๋ฐ์ดํ„ฐ ํŒจ์นญ์˜ ์ •ํ™•ํžˆ ๋™์ผํ•œ ๊ตฌ์กฐ๋ฅผ ์„œ๋ฒ„์—์„œ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

 

์ด๊ฒƒ์ด ์ด๋ก ์ ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด, ๋ฏธ์นœ ๋“ฏ์ด ๋น ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ SSR ์†๋„๋กœ ํ”Œ๋ ˆ์ด์Šคํ™€๋”๊ฐ€ ์žˆ๋Š” ์ดˆ๊ธฐ ๋ Œ๋”๋ง ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜๊ณ  ์‹คํ–‰๋˜๊ธฐ ํ›จ์”ฌ ์ด์ „์— ์‚ฌ์ด๋“œ๋ฐ”์™€ ๋ฉ”์‹œ์ง€ ํ•ญ๋ชฉ๋“ค์„ ๋ณผ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ๋ฆฌ์•กํŠธ๊ฐ€ ๋‹จ์ˆœํ•˜๊ณ  ๋™๊ธฐ์ ์ธ renderToString์„ ํฌ๊ธฐํ•˜๊ณ , ๋ Œ๋”๋ง ํ”„๋กœ์„ธ์Šค๋ฅผ ์ฒญํฌ ๋‹จ์œ„๋กœ ์žฌ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์ฒญํฌ๋“ค์„ ๋ Œ๋”๋ง ๋œ ๊ตฌ์กฐ์— ์–ด๋–ป๊ฒŒ๋“  ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ์ด ์ฒญํฌ๋“ค์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋…๋ฆฝ์ ์œผ๋กœ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ด๊ฒƒ์€ ์ƒ๋‹นํžˆ ์–ด๋ ค์šด ์ž‘์—…์ž…๋‹ˆ๋‹ค! ๊ทธ๋ฆฌ๊ณ  ์ด๋ฏธ ์™„๋ฃŒ๋œ ์ž‘์—…์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ด ์„ค๋ช…์ด ๋ฐ”๋กœ ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ์ŠคํŠธ๋ฆฌ๋ฐ์ด ์กฐํ™”๋กญ๊ฒŒ ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹์„ ๋ฌ˜์‚ฌํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

์ด ๋ชจ๋“  ๊ฒƒ์ด ์–ด๋–ป๊ฒŒ ๋งž๋ฌผ๋ ค ์ž‘๋™ํ•˜๋Š”์ง€ ์ดํ•ดํ•˜๋ ค๋ฉด, ์šฐ๋ฆฌ๋Š” ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ๊ฐœ๋…์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ

์šฐ์„ , ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

 

์ผ๋ฐ˜์ ์ธ ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” ํŽ˜์ด์ง€์— HTML ํƒœ๊ทธ๋ฅผ ๋ฐฐ์น˜ํ•˜๋Š” ์—ญํ• ๋งŒ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๋‹นํžˆ ๋งŽ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด ํ”„๋กœ์ ํŠธ์˜ ์‚ฌ์ด๋“œ๋ฐ” ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณด์ž…๋‹ˆ๋‹ค.

 

export const TopbarForSidebarContentLayout = () => {
  return (
    <div className="lg:bg-blinkNeutral50 lg:dark:bg-blinkNeutral800">
      <nav
        aria-label="Main Navigation"
        className="h-auto lg:h-16 px-6 flex items-center justify-between absolute top-3 lg:top-0 right-0 lg:right-0 left-12 lg:left-0 lg:relative"
      >
        <div className="text-3xl blink-text-primary italic font-blink-title">
          <a href="#">My Dashboards</a>
        </div>
        <div className="gap-3 hidden lg:flex">
        ... // ๋‚˜๋จธ์ง€ ์ฝ”๋“œ ์ƒ๋žต

 

๋ณด์‹œ๋‹ค์‹œํ”ผ, ๋‹จ์ˆœํžˆ div์™€ link ํƒœ๊ทธ๋“ค์˜ ๋ฌถ์Œ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ชจ๋“  ๊ฒƒ์€ ์—ฌ์ „ํžˆ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์ด๋ฉฐ, ๋ฐ”๋กœ ์ด ์ฝ”๋“œ๊ฐ€ "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน"์„ ์•ผ๊ธฐํ•˜๋Š” ๋ชจ๋“  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์—๋Š” ์–ด๋–ค ์ƒํ˜ธ์ž‘์šฉ๋„ ์—†์Šต๋‹ˆ๋‹ค! ์ด ์ปดํฌ๋„ŒํŠธ์—์„œ ์šฐ๋ฆฌ๊ฐ€ ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ์œ ์ผํ•œ ๊ฒƒ์€ div, link, ๊ทธ๋ฆฌ๊ณ  ๋‹ค๋ฅธ ํƒœ๊ทธ๋“ค๋ฟ์ž…๋‹ˆ๋‹ค. ์ฆ‰, HTML ํ˜•ํƒœ์˜ ๋ ˆ์ด์•„์›ƒ์ธ ๊ฑฐ์ฃ .

 

์ด ์ฝ”๋“œ๊ฐ€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜๋Š” ์œ ์ผํ•œ ์ด์œ ๋Š” ๋ฆฌ์•กํŠธ๊ฐ€ Virtual DOM์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ ์ด ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. Virtual DOM์ด๋ž€ ํŽ˜์ด์ง€์— ๋ Œ๋”๋ง ๋˜๋Š” ๋ชจ๋“  ๊ฒƒ์˜ ๊ณ„์ธต์  ํ‘œํ˜„์ž…๋‹ˆ๋‹ค.

 

์—ฌ๋Ÿฌ๋ถ„์ด ๋ฆฌ์•กํŠธ์—์„œ <TopbarForSidebarContentLayout />์ฒ˜๋Ÿผ ์ปดํฌ๋„ŒํŠธ๋ฅผ "๋ Œ๋”๋ง"ํ•  ๋•Œ๋งˆ๋‹ค, Element๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฉ‹์ง„ HTML ์œ ์‚ฌ ๊ตฌ๋ฌธ์˜ ๋ฐ‘๋ฐ”ํƒ•์—๋Š” ๋‹จ์ˆœํžˆ ์—ฌ๋Ÿฌ ์†์„ฑ์„ ๊ฐ€์ง„ ๊ฐ์ฒด๊ฐ€ ์žˆ์œผ๋ฉฐ, ๊ทธ์ค‘ ํ•˜๋‚˜๊ฐ€ "type"์ž…๋‹ˆ๋‹ค. "type"์€ ๋ฌธ์ž์—ด ์ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด ๊ฒฝ์šฐ DOM ์š”์†Œ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ํ˜น์€ ํ•จ์ˆ˜์ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด ๊ฒฝ์šฐ ๋ฆฌ์•กํŠธ๋Š” ๊ทธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ์š”์†Œ๋“ค์˜ ๋ฌถ์Œ์„ ์ถ”์ถœํ•˜์—ฌ, ๊ทธ๊ฒƒ๋“ค์„ ํ†ตํ•ฉ๋œ ํŠธ๋ฆฌ๋กœ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

 

// TopbarForSidebarContentLayout ์š”์†Œ๋“ค
{
    "type": "div",
    "props": {
        "children": [
            {
                "type": "nav",
                "props": {
                    "className": "...",
                    ...
                }
            }
        ]
    }
}

 

ํ˜„์žฌ SSR ๊ตฌํ˜„์—์„œ, ์ด๊ฒƒ์ด Next.js Pages์ด๋“  ์ €์˜ ์ปค์Šคํ…€ ๊ตฌํ˜„์ด๋“  ๊ด€๊ณ„์—†์ด, ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ์ด ํŠธ๋ฆฌ๋ฅผ ์ถ”์ถœํ•˜๋Š” ํ”„๋กœ์„ธ์Šค๋Š” ๋‘ ๋ฒˆ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ๋Š” ์„œ๋ฒ„์—์„œ ์‚ฌ์ „ ๋ Œ๋”๋ง์„ ์ˆ˜ํ–‰ํ•  ๋•Œ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‘ ๋ฒˆ์งธ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฆฌ์•กํŠธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ๋•Œ ์™„์ „ํžˆ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ทธ๋Ÿด ํ•„์š”๊ฐ€ ์—†๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ๋งŒ์•ฝ ์šฐ๋ฆฌ๊ฐ€ ์„œ๋ฒ„์—์„œ ๊ทธ ํŠธ๋ฆฌ๋ฅผ ์ฒ˜์Œ ์ƒ์„ฑํ–ˆ์„ ๋•Œ, ๊ทธ๊ฒƒ์„ ๋ณด์กดํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ๋ฆฌ์•กํŠธ๊ฐ€ ๊ทธ ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ Virtual DOM ํŠธ๋ฆฌ๋ฅผ ๋‹ค์‹œ ์ƒ์„ฑํ•  ์ˆ˜๋งŒ ์žˆ๋‹ค๋ฉด, ์šฐ๋ฆฌ๋Š” ์ผ์„์ด์กฐ์˜ ํšจ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์šฐ๋ฆฌ๋Š” ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์— ๋ณด๋‚ผ ํ•„์š”๊ฐ€ ์—†์–ด์ง€๋ฏ€๋กœ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ํฌ๊ธฐ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์šฐ๋ฆฌ๋Š” ๊ทธ ๋ชจ๋“  ํ•จ์ˆ˜๋“ค์„ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๊ณ  ๊ทธ ๋ฐ˜ํ™˜ ๊ฐ’๋“ค์„ ํŠธ๋ฆฌ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—…์„ ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง€๋ฏ€๋กœ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ปดํŒŒ์ผํ•˜๊ณ  ์‹คํ–‰ํ•˜๋Š” ๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์–ด๋–ป๊ฒŒ ๋ณด๋‚ผ๊นŒ์š”? ์šฐ๋ฆฌ๋Š” ์ด๋ฏธ ์ด์ „์— SSR๋กœ ํŒจ์นญ ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ์ด๋ฏธ ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค! ๋ฐ”๋กœ <script> ํƒœ๊ทธ ์•ˆ์— ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•˜๊ณ , ์ด๋ฅผ window ๊ฐ์ฒด์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

// ์„œ๋ฒ„ ์‘๋‹ต์—์„œ ๋‹ค์Œ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
const htmlWithData = `
    <script>window.__REACT_ELEMENTS__ = ${JSON.stringify({
      "type": "div",
      "props": { "children": [...] }
    })}</script>
    ${HTMLString}`;

 

์šฐ๋ฆฌ๊ฐ€ ๋ฐฉ๊ธˆ ์ด๋ก ์ ์ธ ๊ฐ€๋Šฅ์„ฑ์œผ๋กœ ๋ฐœ๋ช…ํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ ์ œ๊ฐ€ ์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” Next.js App Router๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•œ๋‹ค๋ฉด, ์„œ๋ฒ„์—์„œ ์ œ๊ณต๋˜๋Š” HTML์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ๋ณด๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

<script>
  self.__next_f.push([1,"6:[\"$\",\"div\",null,{\"className\":\"w-full h-full flex flex-col lg:flex-row\",\"children\":[\"$\",\"div\",null,{\"className\":\"flex flex-1 h-full overflow-y-auto flex..."
</script>

 

์ด๊ฒƒ์€ ์•ฝ๊ฐ„ ์ˆ˜์ •๋˜์—ˆ์ง€๋งŒ ์—ฌ์ „ํžˆ ์•Œ์•„๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋“ค์˜ ํŠธ๋ฆฌ์ด๋ฉฐ, ํŽ˜์ด์ง€์— ๋ฌด์—‡์ด ๋ Œ๋”๋ง ๋˜์–ด์•ผ ํ•˜๋Š”์ง€๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์—ฌ๋Ÿฌ๋ถ„์˜ Next.js App Router ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด, Chrome์˜ Elements ํƒญ์—์„œ ๋งจ ์•„๋ž˜๋ฅผ ์‚ดํŽด๋ณด์„ธ์š”.<script> ํƒœ๊ทธ ์ค‘ ํ•˜๋‚˜์—์„œ ์œ„์™€ ๋™์ผํ•œ ๊ตฌ์กฐ๋ฅผ ๋ณด๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ด๊ฒƒ์ด ๋ฐ”๋กœ ์—ฌ๋Ÿฌ๋ถ„์ด ๋ฌธ์„œ์—์„œ์กฐ์ฐจ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„๊ฐ€ ํ•„์š” ์—†๋‹ค๋Š” ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. ์ •๋ง๋กœ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค! ์—ฌ๋Ÿฌ๋ถ„์€ ๊ทธ ๊ตฌ์กฐ๋ฅผ ๋นŒ๋“œ ํƒ€์ž„์— ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ "์‹คํ–‰ ์ค‘์ธ" ์„œ๋ฒ„์ผ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฐธ๊ณ ๋กœ ์ด๊ฒƒ์„ RSC payload๋ผ๊ณ  ๋ถ€๋ฆ…๋‹ˆ๋‹ค.

 

์ด๋ก ์ ์œผ๋กœ, ์ด๊ฒƒ์ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ์•Œ์•„์•ผ ํ•  ์ „๋ถ€์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ๋“ค์€ "์„œ๋ฒ„" ์ธก์—์„œ ๋ฏธ๋ฆฌ ์‹คํ–‰๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋ฉฐ, ์ด๋“ค์˜ ์ฝ”๋“œ์™€ ์ด๋“ค์ด ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์„œ๋ฒ„ ์ธก์— ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ค์ง ์ƒ์„ฑ๋œ RSC payload, ์ฆ‰ ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ์ด์ƒํ•œ ๊ตฌ์กฐ๋งŒ์ด ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ํ•จ๊ป˜ ์ž์ฃผ ์ ‘ํ•˜๊ฒŒ ๋˜๋Š” ์ด์ ๋“ค ์ค‘ ํ•˜๋‚˜๋Š” ๋ฒˆ๋“ค ํฌ๊ธฐ์˜ ๊ฐ์†Œ์ž…๋‹ˆ๋‹ค. ์ด๋ก ์ ์œผ๋กœ, ๋ชจ๋“  ์ฝ”๋“œ์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„œ๋ฒ„์— ๋‚จ์•„ ์žˆ๊ณ , ์ตœ์ข… ๊ตฌ์กฐ๋งŒ ํด๋ผ์ด์–ธํŠธ์— ์ „์†ก๋œ๋‹ค๋ฉด, ๋‹ค์šด๋กœ๋“œ๋˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ์–‘์€ ๋ˆˆ์— ๋„๊ฒŒ ์ค„์–ด๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ๋Š” ๋„ˆ๋ฌด ๋งŽ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ฏธ์น˜๋Š” ์˜ํ–ฅ์„ ์ด๋ฏธ ์•Œ๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ์ด๋Š” ์ข‹์€ ์•„์ด๋””์–ด์ฒ˜๋Ÿผ ๋“ค๋ฆฝ๋‹ˆ๋‹ค.

 

์‹ค์ œ ์„ฑ๋Šฅ์€ ์กฐ๊ธˆ ์žˆ๋‹ค๊ฐ€ ์ธก์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ์ผ๋ฐ˜์ ์œผ๋กœ ํ•จ๊ป˜ ๋‹ค๋ค„์ง€๋Š” ๋‘ ๋ฒˆ์งธ ์ค‘์š”ํ•œ ๊ฐœ๋…์€ ๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ๋ถ„์˜ ์˜ˆ์ƒ๋Œ€๋กœ ์ผ๋ฐ˜์ ์ธ ์ปดํฌ๋„ŒํŠธ์— async ํ‚ค์›Œ๋“œ๊ฐ€ ๋ถ™์€ ๊ฒƒ๋ฟ์ž…๋‹ˆ๋‹ค. ์ด์ œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ์œ„ํ•œ ์ฝ”๋“œ๊ฐ€ ์™„์ „ํžˆ ์œ ํšจํ•œ ์ฝ”๋“œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

 

const PrimarySidebar = async () => {
  const sidebarResponse = await fetch("/api/sidebar");
  const sidebarData = await sidebarResponse.json();

  return <div>{sidebarData.map(...)}</div>
};

 

์ด๊ฒƒ์€ ์˜ค์ง ์„œ๋ฒ„์—์„œ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค. ์ ์–ด๋„ ์ด ๊ธ€์„ ์ž‘์„ฑํ•˜๋Š” ์‹œ์ ์—๋Š” ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. ๋ Œ๋”๋ง ํ”„๋กœ์„ธ์Šค ์ค‘์— ๋ฆฌ์•กํŠธ๋Š” ๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ , Promise๊ฐ€ ๋ฐ˜ํ™˜๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ๋‹ค์Œ, ๊ทธ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ RSC payload๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด payload๋ฅผ RSC ๊ตฌ์กฐ ๋‚ด์—์„œ ์žˆ์–ด์•ผ ํ•  ๊ณณ์œผ๋กœ ์ „์†กํ•˜๊ณ , ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

 

์ŠคํŠธ๋ฆฌ๋ฐ

์ŠคํŠธ๋ฆฌ๋ฐ์€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ์ผ๋ฐ˜์ ์œผ๋กœ ํ•จ๊ป˜ ๋‹ค๋ค„์ง€๋Š” ์„ธ ๋ฒˆ์งธ๋กœ ์ค‘์š”ํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์•ž์„œ ์ด๋ก ์ ์ธ ์—ฐ์Šต์œผ๋กœ ์„ค๋ช…ํ–ˆ๋˜ ๊ฒƒ์„ ์ •ํ™•ํžˆ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

 

์ผ๋ฐ˜์ ์ธ SSR ๊ตฌํ˜„์—์„œ, ์„œ๋ฒ„๋Š” ๋จผ์ € ์ „์†กํ•  ๋ชจ๋“  HTML์„ ๋ฌธ์ž์—ด๋กœ ์ƒ์„ฑํ•œ ๋‹ค์Œ, ์ด๋ฅผ ํ•˜๋‚˜์˜ ํฐ ์ฒญํฌ๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

 

๋ฐ˜๋ฉด, "์ŠคํŠธ๋ฆฌ๋ฐ" SSR ๊ตฌํ˜„์—์„œ๋Š”, ์„œ๋ฒ„๊ฐ€ ๋จผ์ € Node.js Stream์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ, ๋ฆฌ์•กํŠธ์˜ renderToPipeableStream API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ์•กํŠธ ์•ฑ์„ Node ์ŠคํŠธ๋ฆผ์„ ํ†ตํ•ด ์ฒญํฌ ๋‹จ์œ„๋กœ ๋ Œ๋”๋ง ํ•ฉ๋‹ˆ๋‹ค.

 

์ฐธ๊ณ ๋กœ, ์ด ํ”„๋กœ์„ธ์Šค์—์„œ ์ฒญํฌ์˜ ๊ฒฝ๊ณ„๋Š” ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋‚˜ ๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ Suspense๋กœ ๊ฐ์‹ธ์ง„ ์ปดํฌ๋„ŒํŠธ๋“ค์ž…๋‹ˆ๋‹ค. ์ด ์ ์€ ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ˆ ๊ธฐ์–ตํ•ด ๋‘์‹ญ์‹œ์˜ค. ์šฐ๋ฆฌ๊ฐ€ ์ธก์ •์„ ์‹œ์ž‘ํ•  ๋•Œ ๊ทธ ์ค‘์š”์„ฑ์„ ๋ณด๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ €์˜ ๊ฒฝ์šฐ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋กœ ๊ตฌ์„ฑ๋œ ์•ฑ์— ๋Œ€ํ•œ ์ด ๊ธฐ๋Šฅ์˜ ์‹ค์ œ ๊ตฌํ˜„์€ ์ •๋ง๋กœ ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋„ ์ด๋ฅผ ์ž˜ ๋‹ค๋ฃจ์ง€ ๋ชปํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถ€๋„๋Ÿฌ์šธ ์ •๋„๋กœ ์˜ค๋žœ ์‹œ๊ฐ„์„ ๋“ค์—ฌ์„œ ์ž‘๋™ํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ, ๊ฒฐ๊ตญ์—๋Š” ์ •๋ง ๋ณต์žกํ•œ ํŒŒ์ผ ์—ฌ๋Ÿฌ ๊ฐœ์— ๋ฐ˜์ฏค ๋ง๊ฐ€์ง„ ์ƒํƒœ์˜€์„ ๊ฒ๋‹ˆ๋‹ค. ์ด๋Š” "์ผ๋ฐ˜์ ์ธ" SSR์—์„œ์™€ ๊ฐ™์€ ๋‹จ์ˆœํ•œ renderToString์ด ์•„๋‹™๋‹ˆ๋‹ค.

 

๋”ฐ๋ผ์„œ ์—ฌ๊ธฐ์„œ๋Š” ๊ณง๋ฐ”๋กœ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์‰ฝ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ Next.js App Router๋Š” ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๋ฐ ์ŠคํŠธ๋ฆฌ๋ฐ์˜ ์‚ฌ์‹ค์ƒ ๋™์˜์–ด์ด๋ฉฐ, ์ตœ๊ทผ ๋ช‡๋ช‡ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค๋„ ์ด๋ฅผ ์ง€์›ํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, React Router๋Š” ์ตœ๊ทผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ์‹คํ—˜์ ์ธ ์ง€์›์„ ์ถœ์‹œํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ถ„์•ผ์—์„œ๋Š” ์—ฌ์ „ํžˆ Next.js๊ฐ€ ์ง€๋ฐฐ์ ์ž…๋‹ˆ๋‹ค.

 

Next.js App Router ์ธก์ • (Lift-and-Shift ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„)

Next.js App Router ํ™˜๊ฒฝ์—์„œ ๊ธฐ์กด ์ธก์ • ๊ฒฐ๊ณผ์™€์˜ ์˜๋ฏธ ์žˆ๋Š” ๋น„๊ต๋ฅผ ํ•˜๋ ค๋ฉด, ์–ด๋–ป๊ฒŒ๋“  ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ์˜ํ–ฅ์„ ๊ฒฉ๋ฆฌํ•˜๊ณ  ๊ธฐ์ค€์„ ์„ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด Next.js์˜ ์žฅ์ ์ด์ž ๋‹จ์ ์€ ์—„์ฒญ๋‚œ ์–‘์˜ ๋‹ค์–‘ํ•œ ์ตœ์ ํ™”, ์บ์‹ฑ, ๊ฐ€์ •, ๋ณ€ํ™˜ ๋“ฑ ์ •๋ง๋กœ ๋งŽ์€ ์ผ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค๋Š” ์ ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ ์ œ๊ฐ€ ์•ฑ ์ „์ฒด๋ฅผ ๋‹น์žฅ ์žฌ์ž‘์„ฑํ•œ๋‹ค๋ฉด, ๊ทธ ๋น„๊ต๋Š” ๊ณต์ •ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ด์ ์ด๋‚˜ ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ํ”„๋ ˆ์ž„์›Œํฌ ์ž์ฒด์˜ ๊ณ ์œ ํ•œ ์ž‘์—… ๋•Œ๋ฌธ์ธ์ง€, ์•„๋‹ˆ๋ฉด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ/์ŠคํŠธ๋ฆฌ๋ฐ์ด ํ›Œ๋ฅญํ•˜๊ฑฐ๋‚˜ ๋”์ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์ธ์ง€ ๊ตฌ๋ถ„ํ•  ๋ฐฉ๋ฒ•์ด ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ธฐ์กด ์•ฑ์„ Next.js Pages์—์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๋Š” ๊ฐ€๋Šฅํ•œ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ƒ์ƒํ•œ๋‹ค๋ฉด, ์˜๋ฏธ ์žˆ๋Š” ๋ฌด์–ธ๊ฐ€๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋‹ˆ ํ•ฉ๋ฆฌ์ ์œผ๋กœ ์ ‘๊ทผํ•ด ๋ด…์‹œ๋‹ค. ์ €์—๊ฒŒ๋Š” Next.js Pages์™€ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฐ์ดํ„ฐ ํŒจ์นญ์œผ๋กœ ๊ตฌํ˜„๋œ ๊ธฐ์กด์˜ ํฐ ์•ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์•ฑ์€ ํ˜„์žฌ๊นŒ์ง€ ํ…Œ์ด๋ธ”์—์„œ ๋‘ ๋ฒˆ์งธ ํ–‰์— ํ•ด๋‹นํ•˜๋Š”, ๊ฐ€์žฅ ์ž‘์€ LCP๋ฅผ ๊ฐ€์ง„ ์•ฑ์ž…๋‹ˆ๋‹ค. ์ €๋Š” ์ด ์•ฑ์„ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ๋ฉ˜ํ„ธ ๋ชจ๋ธ๊ณผ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ฐฉ์‹์„ ๊ฐ€์ง„ ์ƒˆ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ €๋Š” ๋ฌด์—‡์„ ํ•ด์•ผ ํ• ๊นŒ์š”?

 

์ฒซ์งธ, ๊ฐ€๋Šฅํ•œ ํ•œ ๊ธฐ๋Šฅ ๋ณ€๊ฒฝ ์—†์ด ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธฐ๊ณ (Lift-and-Shift) ์•ฑ์ด ์ž‘๋™ํ•˜๋ฉฐ ์•„๋ฌด๊ฒƒ๋„ ๋ฌธ์ œ์—†๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹คํ—˜์˜ ๋งฅ๋ฝ์—์„œ ์ด๊ฒƒ์€ ๋ผ์šฐํŒ…์„ ์•ฝ๊ฐ„ ์žฌ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ์ง„์ž… ํŒŒ์ผ์— use client๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐ•์ œํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด App Router๊ฐ€ ๋ชจ๋“  ๊ณณ์—์„œ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘๋™ํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๊ฒฐ๊ณผ์ ์œผ๋กœ, ์ด๊ฒƒ์€ ์ƒˆ ํ”„๋ ˆ์ž„์›Œํฌ ์ž์ฒด์˜ ์˜ํ–ฅ์„ ๊ฒฉ๋ฆฌํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์ด์ ์ด๋‚˜ ์„ฑ๋Šฅ ์ €ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ ์ž์ฒด ๋•Œ๋ฌธ์ผ ๊ฒƒ์ด๋ฉฐ, ์ด๋Š” ์•„์ง ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ŠคํŠธ๋ฆฌ๋ฐ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค! ๋ถ€์ˆ˜์ ์ธ ์ด์ ์œผ๋กœ, ์ด๊ฒƒ์€ ํฐ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด "๊ตฌํ˜•" Next.js์—์„œ "์‹ ํ˜•" Next.js๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•  ๊ฐ€์น˜๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋„ ๋ณด์—ฌ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ, ์•ฑ์€ Next.js Pages์—์„œ ์‚ฌ์ „ ๋ Œ๋”๋ง๋˜์—ˆ์„ ๋•Œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ํ–ˆ์„ ๋•Œ์ฒ˜๋Ÿผ ์ •ํ™•ํžˆ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ธก์ • ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.16s / 1.24s 2.16s / 1.24s 2.16s / 1.24s 4.6s / 1.4s 2.44s / 150ms
Next.js Pages (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.76s / 800ms 3.7s / 1.5s 4.2s / 2s 3.1s / 900ms 1.34s / 100ms
Next.js Pages (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.15s / 1.15s 2.15s / 1.15s 2.15s / 1.15s 3.5s / 1.25s 1.35s / 100ms
Next.js App router (Lift-and-Shift) 1.28s / 650ms 4.4s / 1.5s 4.9s / 2s 3.8s / 900ms 2.52s / 250ms

 

์—ฌ๊ธฐ์„œ LCP ๊ฐ’์€ 1.28s๋กœ ๋–จ์–ด์กŒ๋Š”๋ฐ, ์ด๋Š” ์ง€๊ธˆ๊นŒ์ง€์˜ ๊ฐ’ ์ค‘์—์„œ ๊ฐ€์žฅ ์ž‘์€ ๊ฐ’์ž…๋‹ˆ๋‹ค. Next.js Pages์™€ ๋น„๊ตํ•˜์—ฌ ์•ฝ 500ms์˜ ๊ฐœ์„ ์„ ๋ณด์˜€๋Š”๋ฐ, ์ด๋Š” ์—„์ฒญ๋‚œ ๋ฐœ์ „์ž…๋‹ˆ๋‹ค! ๐ŸŽ‰ ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์€ ์•ฝ 700ms ์ •๋„ ์•…ํ™”๋œ ๊ฒƒ์œผ๋กœ ๋ณด์ด๋Š”๋ฐ, ์ด๊ฒƒ ์—ญ์‹œ ํฌ์ง€๋งŒ ๋ถ€์ •์ ์ธ ๋ฐฉํ–ฅ์—์„œ์˜ ๋ณ€ํ™”์ž…๋‹ˆ๋‹ค ๐Ÿฅบ.

 

์™œ ์ด๋Ÿฐ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™”๋Š”์ง€ ์กฐ์‚ฌํ•˜๋Š” ๊ฒƒ์€ ์—„์ฒญ๋‚˜๊ฒŒ ์žฌ๋ฏธ์žˆ์„ ์ˆ˜ ์žˆ๊ณ , ์—ฌ๋Ÿฌ๋ถ„์ด ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ์„ ์–ผ๋งˆ๋‚˜ ์ž˜ ์ฝ๋Š”์ง€์— ๋Œ€ํ•œ ์ข‹์€ ํ…Œ์ŠคํŠธ๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ €๋Š” ์—ฌ๋Ÿฌ๋ถ„์ด ์ง์ ‘ ์‹œ๋„ํ•ด ๋ณด๊ธฐ๋ฅผ ๊ฐ•๋ ฅํžˆ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค ๐Ÿ˜‰. ๋งŒ์•ฝ ์—ฌ๋Ÿฌ๋ถ„์ด ์ด๋ฅผ ์žฌํ˜„ํ•˜๋ ค๋Š”๋ฐ ์ด๋Ÿฐ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, ๋ธŒ๋ผ์šฐ์ € ๋•Œ๋ฌธ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Chrome์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

 

๊ทธ ํ•ด๋‹ต์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

์ฒซ์งธ, Next.js App Router๋Š” CSS๊ฐ€ ๋กœ๋“œ๋œ ํ›„์—์•ผ ๋ชจ๋“  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ง€์—ฐ์‹œํ‚ค๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. Pages ๋ฒ„์ „์€ ๊ทธ๋ ‡์ง€ ์•Š์•˜๊ณ  CSS์™€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ณ‘๋ ฌ๋กœ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋กœ๋”ฉ์ด ์•ฝ๊ฐ„์˜ ๋Œ€์—ญํญ์„ "ํ›”์ณ๊ฐ”๊ณ ", Pages์—์„œ๋Š” CSS๊ฐ€ ๋” ๋А๋ฆฌ๊ฒŒ ๋กœ๋“œ๋˜์–ด LCP ๊ฐ’์„ ์ง€์—ฐ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด LCP์—์„œ 500ms๋ฅผ ์–ป์€ ์ด์œ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์—์„œ ์•ฝ 700ms๋ฅผ ์†ํ•ด ๋ณธ ์ด์œ ์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ •๋ณด๋Š” Network ์„น์…˜์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

๊ฒŒ๋‹ค๊ฐ€, App Router๋Š” Pages ๋ฒ„์ „๋ณด๋‹ค ์ตœ์†Œ 100ms ์ •๋„ ๋” ๋งŽ์€ ์ž‘์—…๋Ÿ‰์„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋งค์šฐ ๋ฐ”์˜๊ฒŒ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ด๋Š” LCP ์™ธ์˜ ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์„ ๋”์šฑ ์ง€์—ฐ์‹œํ‚ต๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰ 100ms๋Š” ์—ฌ๊ธฐ์ €๊ธฐ์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌด์ž‘์œ„์ ์ธ ๋ณ€๋™์ผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ข…ํ•ฉ์ ์œผ๋กœ ๋ดค์„ ๋•Œ, ๊ทธ ํšจ๊ณผ๋Š” ๋‹ค์†Œ ๋ฏธ๋ฏธํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋งˆ๋„ ์ถ”๊ฐ€์ ์ธ ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๋ฉด ๋” ๋‚˜์•„์งˆ ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ ๋‹ค์Œ ๋‹จ๊ณ„๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ฐ€๋Šฅํ•œ ํ•œ ๋งŽ์ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์•ฑ์€ ์—ฌ๊ธฐ์ €๊ธฐ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ์•ฑ ์ „์ฒด๋ฅผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ํ•ฉ๋ฆฌ์ ์œผ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์ œ๊ฐ€ ์ „๋žต์ ์ธ ์œ„์น˜์— use client๋ฅผ ์ œ๊ฑฐํ–ˆ์„ ๋•Œ (์žฌํ˜„ ๋‹จ๊ณ„ ์ฐธ๊ณ ), ๊ทธ ํšจ๊ณผ๋Š” ํฅ๋ฏธ๋กœ์› ์Šต๋‹ˆ๋‹ค.

 

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ํฌ๊ธฐ๋Š” ์ค„์–ด๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ข‹์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ํŽ˜์ด์ง€์—์„œ๋Š” ์•„์ฃผ ์กฐ๊ธˆ๋งŒ ์ค„์—ˆ๊ณ  (ํ™ˆ ํŽ˜์ด์ง€๋Š” ๋‹จ 2% ์ •๋„), ์ผ๋ถ€ ํŽ˜์ด์ง€์—์„œ๋Š” ํฌ๊ฒŒ ์ค„์—ˆ์Šต๋‹ˆ๋‹ค (๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋Š” KB ๊ฐ’์—์„œ B ๋‹จ์œ„๋กœ, ๊ฑฐ์˜ 0์— ๊ฐ€๊นŒ์›Œ์กŒ์Šต๋‹ˆ๋‹ค). ๊ทธ๋Ÿฌ๋‚˜ ๋Œ€๋ถ€๋ถ„์˜ ๊ณต์œ  ์ฒญํฌ๋Š” ์ „ํ˜€ ๋ณ€ํ•˜์ง€ ์•Š์•˜๊ณ , Inbox ํŽ˜์ด์ง€์—์„œ ์ €์—๊ฒŒ ์ค‘์š”ํ•œ ๋ชจ๋“  ์ง€ํ‘œ์— ๋Œ€ํ•œ ์„ฑ๋Šฅ ์˜ํ–ฅ์€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋”ฐ๋ผ์„œ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ์žฌ์ž‘์„ฑํ•˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ทธ ์ž์ฒด๋งŒ์œผ๋กœ๋Š” ์ €์˜ ์•ฑ์—์„œ ์–ด๋– ํ•œ ์„ฑ๋Šฅ ์˜ํ–ฅ๋„ ๋ฏธ์น˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ €๋Š” use client๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ๊ณ ๋ฏผ ์—†์ด ์—ฌ๊ธฐ์ €๊ธฐ ๋ฌด์ž‘์œ„๋กœ ๋ถ™์–ด ์žˆ๊ณ  ๊ฒฐ๊ตญ ๋Œ€๋ถ€๋ถ„์˜ ํŽ˜์ด์ง€์˜ ์ตœ์ƒ๋‹จ๊นŒ์ง€ ๊ฑฐ์Šฌ๋Ÿฌ ์˜ฌ๋ผ๊ฐ€๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์‹ค์ œ ๋ณต์žกํ•œ ์•ฑ์—์„œ๋„ ๋™์ผํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

์‹คํ—˜์„ ์žฌํ˜„ํ•˜๋Š” ๋‹จ๊ณ„

  1. ๋ฐฑ์—”๋“œ API๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: npm run start --workspace=backend-api
  2. frontend/utils/link.tsx ํŒŒ์ผ๋กœ ์ด๋™ํ•ด์„œ Next.js Link๋ฅผ ์ฃผ์„ ํ•ด์ œํ•˜๊ณ  ์ปค์Šคํ…€ ๊ตฌํ˜„์„ ์ฃผ์„ ์ฒ˜๋ฆฌ ํ•ฉ๋‹ˆ๋‹ค.
  3. src/frontend/next-app-router/src/app ํด๋”๋กœ ์ด๋™ํ•˜์—ฌ, ๋ชจ๋“  page ํŒŒ์ผ ์ƒ๋‹จ์— use client๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ชจ๋“  ๊ฒƒ์„ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ฐ•์ œ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„, ํ•ฉ๋ฆฌ์ ์ธ ์–‘์˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋˜๋Œ๋ฆฌ๋ ค๋ฉด (์•„์ง ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ์ œ์™ธ), ๋ชจ๋“  use client๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
  4. ํ”„๋ŸฐํŠธ์—”๋“œ๋ฅผ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค: npm run build --workspace=next-app-router
  5. ํ”„๋ŸฐํŠธ์—”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: npm run start --workspace=next-app-router
  6. HTTP2/3๋ฅผ ์œ„ํ•œ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: caddy reverse-proxy --to :3000
  7. ์›น์‚ฌ์ดํŠธ๋ฅผ https://localhost/inbox ์ฃผ์†Œ๋กœ ์—ฝ๋‹ˆ๋‹ค.

 

Next.js App Router ์ธก์ • (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ฐ์ดํ„ฐ ํŒจ์นญ ํฌํ•จ)

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์˜ ๋‹ค์Œ ๋‹จ๊ณ„๋Š” ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ฐฉ์‹์„ ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ ์žฌ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค์Œ ์ฝ”๋“œ ๋Œ€์‹ ์—,

 

const Sidebar = () => {
  useEffect(() => {
    const fetchSidebarData = async () => {
      const response = await fetch('/api/sidebar')
      const data = await response.json()
      setSidebarData(data)
    }

    fetchSidebarData()
  }, [])
}

 

์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

const Sidebar = async () => {
  const response = await fetch("/api/sidebar");
  const data = await response.json();

  return <div>{data.map(...)}</div>
};

 

์‹ค์ œ๋กœ๋Š” ์ด ๊ณผ์ •์ด ์กฐ๊ธˆ ๋” ๋ณต์žกํ–ˆ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ๋ชจ๋“  ์‚ฌ์šฉ์ฒ˜๋ฅผ ์ถ”์ ํ•˜์—ฌ ์ƒ์œ„ ์ฒด์ธ์— ์žˆ๋Š” ๋ชจ๋“  ๋‹จ์ผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ธ์ง€, ์ฆ‰ ํŒŒ์ผ ์ƒ๋‹จ์— use client๊ฐ€ ์—†๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด, ์ด ์ฝ”๋“œ๊ฐ€ ๋ฌดํ•œ ๋ฃจํ”„๋ฅผ ์ผ์œผํ‚ค๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋Š” ์•ฝ๊ฐ„์˜ ์ฐฝ์˜์ ์ธ ์‚ฌ๊ณ ์™€ ๋ฆฌํŒฉํ† ๋ง์„ ์š”๊ตฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๋งˆ์นจ๋‚ด ์ €๋Š” ๊ฒฐ๊ตญ ์„ฑ๊ณตํ–ˆ๊ณ  ์ธก์ •ํ•  ์ค€๋น„๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๋‹ค์Œ์€ ๊ทธ ์ˆ˜์น˜๋“ค์ž…๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.16s / 1.24s 2.16s / 1.24s 2.16s / 1.24s 4.6s / 1.4s 2.44s / 150ms
Next.js Pages (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.76s / 800ms 3.7s / 1.5s 4.2s / 2s 3.1s / 900ms 1.34s / 100ms
Next.js Pages (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.15s / 1.15s 2.15s / 1.15s 2.15s / 1.15s 3.5s / 1.25s 1.35s / 100ms
Next.js App router (Lift-and-Shift) 1.28s / 650ms 4.4s / 1.5s 4.9s / 2s 3.8s / 900ms 2.52s / 250ms
Next.js App router (์„œ๋ฒ„ ํŒจ์นญ) 1.78s / 1.2s 1.78s / 1.2s 1.78s / 1.2s 4.2s / 1.3s 2.42s / 100ms

 

๊ฒฐ๊ณผ๋Š” ๋งค์šฐ ํฅ๋ฏธ๋กญ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚ฌ๋Š”์ง€ ์•„์‹œ๊ฒ ๋‚˜์š”?

 

๋ณด์‹œ๋‹ค์‹œํ”ผ, LCP ๊ฐ’์€ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธด(Lift-and-Shift) ๋ฒ„์ „๊ณผ ๋น„๊ตํ•˜์—ฌ ์•ฝ 500ms ์ •๋„ ์•…ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์ด๋“œ๋ฐ” ๋ฐ ๋ฉ”์‹œ์ง€ ํ•ญ๋ชฉ ์ˆ˜์น˜๋Š” ์ผ์น˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์‹ค, ์ด ํŒจํ„ด์€ ์ด์ „์— ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ์ด ์ ์šฉ๋œ Next.js Pages์˜ ํŒจํ„ด์„ ์ •ํ™•ํžˆ ๋ฐ˜๋ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋‹ค์šด๋กœ๋“œ ์ง€์—ฐ์œผ๋กœ ์ธํ•ด LCP๊ฐ€ ๋‚ฎ์•„์ง€๊ณ  ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ตฌ๊ฐ„์ด ๊ธธ์–ด์ง„๋‹ค๋Š” ์ ๋งŒ ๊ณ ๋ คํ•œ ์ฑ„ ๋ง์ด์ฃ .

 

์ŠคํŠธ๋ฆฌ๋ฐ ์„น์…˜์—์„œ ์ œ๊ฐ€ ์ŠคํŠธ๋ฆฌ๋ฐ ์ฒญํฌ๊ฐ€ Suspense์— ์˜ํ•ด ๋ณดํ˜ธ๋˜๋ฉฐ, ์ด๋ฅผ ๊ธฐ์–ตํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ค๊ณ  ์–ธ๊ธ‰ํ–ˆ๋˜ ๊ฒƒ์„ ๊ธฐ์–ตํ•˜์‹ญ๋‹ˆ๊นŒ? ๋ฐ”๋กœ ์ด๊ฒƒ์ด ๊ทธ ์ด์œ ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์—ฌ๋Ÿฌ๋ถ„์ด ์ด ์ŠคํŠธ๋ฆฌ๋ฐ ์ฒญํฌ๋“ค์„ Suspense (๋˜๋Š” Next.js์˜ ๊ฒฝ์šฐ loading.ts)๋กœ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์„ ์žŠ์œผ๋ฉด, ๋ฆฌ์•กํŠธ๋Š” ์ „์ฒด ์•ฑ์„ ํ•˜๋‚˜์˜ ๊ฑฐ๋Œ€ํ•œ ์ฒญํฌ๋กœ ์ทจ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ ๊ฒฐ๊ณผ, ๋ Œ๋”๋ง ์‹œ ๋ฆฌ์•กํŠธ๋Š” ํŠธ๋ฆฌ์—์„œ ๋งˆ์ฃผ์น˜๋Š” ๋ชจ๋“  ๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ผ์ฐ ๋ณด๋‚ด๋ ค๋Š” ์‹œ๋„ ์—†์ด ๊ทธ๋ƒฅ awaitํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์•ฑ์€ ์šฐ๋ฆฌ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ณด๋‚ด๊ธฐ ์ „์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋˜ Next.js Pages๋‚˜ ์ €์˜ ์ปค์Šคํ…€ ๊ตฌํ˜„๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

 

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด, ์šฐ๋ฆฌ๋Š” ๋น„๋™๊ธฐ Server Components๋ฅผ <Suspense>๋กœ ๊ฐ์‹ธ์•ผํ•ฉ๋‹ˆ๋‹ค.

 

// ๋ Œ๋”๋ง ๊ณผ์ • ์ค‘ ์–ด๋”˜๊ฐ€์—์„œ, ์‚ฌ์ด๋“œ๋ฐ”์˜ ๊ฒฝ์šฐ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.
<Suspense fallback={<div>Loading inbox...</div>}>
  <InboxWithFixedBundlePage messages={messages} />
</Suspense>

 

์ด์ œ ์šฐ๋ฆฌ๊ฐ€ ์•Œ๊ณ  ์žˆ๋Š” ๋Œ€๋กœ ๋™์ž‘ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฆฌ์—‘ํŠธ๋Š” Suspense ๋‚ด๋ถ€์˜ ๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ์™„๋ฃŒํ•˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ , "ํ•ต์‹ฌ ๊ฒฝ๋กœ"์— ์žˆ๋Š” ๋ชจ๋“  ๊ฒƒ์„ ๋จผ์ € ๋ Œ๋”๋ง ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ฐ”๋กœ ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ, ์„œ๋ฒ„๋Š” ์ด ์ฒญํฌ๋ฅผ ํด๋ผ์ด์–ธํŠธ์— ์ „์†กํ•˜๊ณ , suspended ๋œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, Promise๋“ค์ด ๋ฐ˜ํ™˜๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ์—ฌ์œ ๋กญ๊ฒŒ ๋Œ€๊ธฐํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์‚ฌ์ด๋“œ๋ฐ” ๋ฐ์ดํ„ฐ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„, ํ•ด๋‹น Suspense ๊ฒฝ๊ณ„๊ฐ€ ์ฒ˜๋ฆฌ๋˜๊ณ , ๋˜ ๋‹ค๋ฅธ ์ฒญํฌ๊ฐ€ ์ค€๋น„๋˜์–ด ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๊ณต๊ธ‰๋ฉ๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€ ํ•ญ๋ชฉ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

 

์˜ฌ๋ฐ”๋ฅธ ๊ตฌํ˜„์— ๋Œ€ํ•œ ์ธก์ • ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.16s / 1.24s 2.16s / 1.24s 2.16s / 1.24s 4.6s / 1.4s 2.44s / 150ms
Next.js Pages (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.76s / 800ms 3.7s / 1.5s 4.2s / 2s 3.1s / 900ms 1.34s / 100ms
Next.js Pages (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.15s / 1.15s 2.15s / 1.15s 2.15s / 1.15s 3.5s / 1.25s 1.35s / 100ms
Next.js App router (Lift-and-Shift) 1.28s / 650ms 4.4s / 1.5s 4.9s / 2s 3.8s / 900ms 2.52s / 250ms
Next.js App router (Suspense ์—†๋Š” ์„œ๋ฒ„ ํŒจ์นญ) 1.78s / 1.2s 1.78s / 1.2s 1.78s / 1.2s 4.2s / 1.3s 2.42s / 100ms
Next.js App router (Suspense๋ฅผ ํฌํ•จํ•œ ์„œ๋ฒ„ ํŒจ์นญ) 1.28s / 750ms 1.28s / 750ms 1.28s / 1.1s 3.8s / 800ms 2.52s / 50ms

 

์ข‹์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ฉ‹์ง€๊ณ  ์—„์ฒญ๋‚˜๊ฒŒ ๋น ๋ฅธ ๋ชจ์Šต์ž…๋‹ˆ๋‹ค! ๋ฌผ๋ก  "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ" ๊ฐ„๊ทน์€ ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค. ๊ทธ ๋ถ€๋ถ„์€ ์—ฌ์ „ํžˆ ๋ชจ๋“  ์ธก์ •์น˜ ์ค‘์—์„œ ๊ฐ€์žฅ ๋‚˜์œ ์ƒํƒœ๋กœ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค.

 

์‚ฌ์‹ค, ์ด๊ฒƒ์€ ๋„ˆ๋ฌด ๋นจ๋ผ์„œ ๋ชจ๋“  ์ˆ˜์น˜๋“ค์ด ๋‹ค์‹œ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์กŒ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ์–ด๋”˜๊ฐ€์—์„œ ์–ด๋–ค ํ˜•ํƒœ์˜ ๋ฐฐ์นญ์ด ์ด๋ฃจ์–ด์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ด ์„ธ ๊ฐ€์ง€ ์ˆ˜์น˜๊ฐ€ ๊ฐ™์€ ์ฒญํฌ์— ํฌํ•จ๋œ ๊ฒƒ์œผ๋กœ ์ถ”์ธกํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๋งŒ์•ฝ ์ œ๊ฐ€ /api/sidebar์˜ ์‹œ๊ฐ„์„ 3s๋กœ, /api/messages์˜ ์‹œ๊ฐ„์„ 5s๋กœ ๋Š˜๋ฆฐ๋‹ค๋ฉด, ์ ์ง„์  ๋ Œ๋”๋ง์˜ ๋ชจ์Šต์ด ๋šœ๋ ทํ•˜๊ฒŒ ๋ณด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋น„๋ก ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ๋Š” ๋” ๋น ๋ฅผ ๋ฟ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง๊ณผ ์ •ํ™•ํžˆ ๋™์ผํ•˜๊ฒŒ ๋ณด์ผ์ง€๋ผ๋„ ๋ง์ž…๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ์€ ๋งค์šฐ ์žฌ๋ฏธ์žˆ์–ด์ง‘๋‹ˆ๋‹ค.

 

 

๋„คํŠธ์›Œํฌ ์„น์…˜์—์„œ ๋ณด์ด๋Š” ์•„์ฃผ ๊ธด HTML ๋ฐ”๊ฐ€ ๋ณด์ด์‹œ๋‚˜์š”? ์ €๊ฒƒ์€ ์„œ๋ฒ„๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ ์—ฐ๊ฒฐ์„ ๊ณ„์† ์—ด์–ด๋‘๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋” "์ „ํ†ต์ ์ธ" SSR๊ณผ ๋น„๊ตํ•ด ๋ณด์„ธ์š”.

 

 

HTML์€ ์™„๋ฃŒ๋˜๋Š” ์ฆ‰์‹œ ์ž‘์—…์ด ๋๋‚ฉ๋‹ˆ๋‹ค. ๊ธฐ๋‹ค๋ฆด ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

 

์‹คํ—˜์„ ์žฌํ˜„ํ•˜๋Š” ๋‹จ๊ณ„

  1. ์ด์ „ ์„น์…˜๊ณผ ๋™์ผํ•œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด๋˜, ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  2. src/frontend/next-app-router/src/app/inbox/page.tsx ํŒŒ์ผ๋กœ ์ด๋™ํ•˜์—ฌ, Suspense๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋ Œ๋”๋ง ํ•จ์ˆ˜ ๋‚ด์˜ ๊ด€๋ จ import ๋ฐ ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ์ฃผ์„ ์ฒ˜๋ฆฌ/ํ•ด์ œํ•˜์—ฌ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ์ด ํฌํ•จ๋œ "๊ณ ์žฅ ๋‚œ" ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฒฝํ—˜์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.
  3. ์œ„ ํŒŒ์ผ๊ณผ src/frontend/next-app-router/components/primary-sidebar-rsc.tsx ํŒŒ์ผ์—์„œ Suspense๋ฅผ ์ฃผ์„ ํ•ด์ œํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

 

TL; DR (์š”์•ฝ)

์ข‹์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์ด

์—ฐ๊ตฌ ๋ณด๊ณ ์„œ

์•„ํ‹ฐํด์˜ ์š”์•ฝ์€ ๋ฌด์—‡์ผ๊นŒ์š”? ์—ฌ๊ธฐ ๋ชจ๋“  ์ธก์ •๊ฐ’์ด ๋‹ด๊ธด ์ตœ์ข… ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค.

 

  LCP (์บ์‹œ X / JS ์บ์‹œ) ์‚ฌ์ด๋“œ๋ฐ” (์บ์‹œ X / JS ์บ์‹œ) ๋ฉ”์‹œ์ง€ (์บ์‹œ X / JS ์บ์‹œ) ํ† ๊ธ€ ์ƒํ˜ธ์ž‘์šฉ (์บ์‹œ X / JS ์บ์‹œ) ์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ ๊ฐ„๊ทน (์บ์‹œ X / JS ์บ์‹œ)
ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง 4.1s / 800ms 4.7s / 1.5s 5.1s / 2s 4.1s / 800ms  
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.61s / 800ms 4.7s / 1.5s 5.1s / 2s 4s / 900ms 2.39s / 100ms
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.16s / 1.24s 2.16s / 1.24s 2.16s / 1.24s 4.6s / 1.4s 2.44s / 150ms
Next.js Pages (ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 1.76s / 800ms 3.7s / 1.5s 4.2s / 2s 3.1s / 900ms 1.34s / 100ms
Next.js Pages (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํŒจ์นญ) 2.15s / 1.15s 2.15s / 1.15s 2.15s / 1.15s 3.5s / 1.25s 1.35s / 100ms
Next.js App router (Lift-and-Shift) 1.28s / 650ms 4.4s / 1.5s 4.9s / 2s 3.8s / 900ms 2.52s / 250ms
Next.js App router (Suspense ์—†๋Š” ์„œ๋ฒ„ ํŒจ์นญ) 1.78s / 1.2s 1.78s / 1.2s 1.78s / 1.2s 4.2s / 1.3s 2.42s / 100ms
Next.js App router (Suspense๋ฅผ ํฌํ•จํ•œ ์„œ๋ฒ„ ํŒจ์นญ) 1.28s / 750ms 1.28s / 750ms 1.28s / 1.1s 3.8s / 800ms 2.52s / 50ms

 

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

 

์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ๋„์ž…ํ•˜๋ฉด ์ดˆ๊ธฐ ๋กœ๋“œ ์ˆ˜์น˜๋ฅผ ๊ทน์ ์œผ๋กœ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ" ๊ฐ„๊ทน์ด๋ผ๋Š” ๋Œ€๊ฐ€๋ฅผ ์น˜๋ฅด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ„๊ทน์€ ํŽ˜์ด์ง€๋Š” ์ด๋ฏธ ๋ณด์ด์ง€๋งŒ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ž‘๋™ํ•˜๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ„๊ทน์˜ ํฌ๊ธฐ๋Š” ํŽ˜์ด์ง€ ์ดˆ๊ธฐํ™”์— ํ•„์š”ํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ํฌ๊ธฐ์— ์ขŒ์šฐ๋ฉ๋‹ˆ๋‹ค.

 

์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ ํ•˜๋ฉด ์ดˆ๊ธฐ ๋กœ๋“œ ์†๋„๋Š” ๋А๋ ค์ง€์ง€๋งŒ, ์ „์ฒด ํŽ˜์ด์ง€ ๊ฒฝํ—˜์„ ํ›จ์”ฌ ๋” ์ผ์ฐ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

"์ „ํ†ต์ ์ธ" SSR์—์„œ RSC์™€ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ, ์ฆ‰ Next.js Pages์—์„œ Next.js App Router๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๋Š” ๊ฒฝ์šฐ, ์ฃผ์˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์„ฑ๋Šฅ์ด ์•…ํ™” ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋– ํ•œ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ๋ณด๋ ค๋ฉด ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง์„ ์„œ๋ฒ„ ์ค‘์‹ฌ์œผ๋กœ ์žฌ์ž‘์„ฑํ•ด์•ผ ํ•˜๋ฉฐ, Suspense ๊ฒฝ๊ณ„๋ฅผ ์žŠ์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ƒ๋‹นํžˆ ์ค‘์š”ํ•œ ๊ฐœ๋ฐœ ๋…ธ๋ ฅ์ด ํ•„์š”ํ•˜๋ฉฐ ์ „์ฒด ์•ฑ์˜ ์žฌ์„ค๊ณ„๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

Next.js Pages์—์„œ Next.js App Router๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋‹ค์šด๋กœ๋“œ ์ง€์—ฐ์œผ๋กœ ์ธํ•ด "์ƒํ˜ธ์ž‘์šฉ ๋ถˆ๊ฐ€๋Šฅ" ๊ฐ„๊ทน์ด ๋” ๋‚˜๋น ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Š” ๋ธŒ๋ผ์šฐ์ €์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์•ฑ์ด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์™€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ํ˜ผํ•ฉ๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๋‹จ๋…์œผ๋กœ๋Š” ์„ฑ๋Šฅ์ด ๊ฐœ์„ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ธก์ • ๊ฐ€๋Šฅํ•œ ์„ฑ๋Šฅ ์˜ํ–ฅ์ด ์žˆ์„ ๋งŒํผ ์ถฉ๋ถ„ํžˆ ์ค„์ด์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ŠคํŠธ๋ฆฌ๋ฐ๊ณผ Suspense๊ฐ€ ์ค‘์š”ํ•œ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ์ฃผ๋œ ์„ฑ๋Šฅ ์ด์ ์€ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์šฐ์„  ๋ฐฉ์‹์œผ๋กœ ์™„์ „ํžˆ ์žฌ์ž‘์„ฑํ•˜๋Š” ๋ฐ์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค.

 

๊ทธ๊ฑด ๊ทธ๋ ‡๊ณ , Preply์˜ ์—”์ง€๋‹ˆ์–ด๋ง ํŒ€์€ INP๋ฅผ ๊ฐœ์„ ํ•˜๋ ค๋Š” ๊ณผ์ •์—์„œ ์ •ํ™•ํžˆ ๋™์ผํ•œ ๊ฒฐ๋ก ์— ๋„๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์„ฑ๋Šฅ ์กฐ์‚ฌ ๋ฐ ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋ฅผ ์ฝ๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•˜์‹ ๋‹ค๋ฉด, Stefano Magni์˜ ๋‹ค์Œ ์•„ํ‹ฐํด์„ ๊ฐ•๋ ฅํžˆ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.

How Preply improved INP on a Next.js application (without React Server Components and App Router)

 

 

๐Ÿš€ ํ•œ๊ตญ์–ด๋กœ ๋œ ํ”„๋ŸฐํŠธ์—”๋“œ ์•„ํ‹ฐํด์„ ๋น ๋ฅด๊ฒŒ ๋ฐ›์•„๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด Korean FE Article์„ ๊ตฌ๋…ํ•ด์ฃผ์„ธ์š”!
๋ฐ˜์‘ํ˜•

๐Ÿ’ฌ ๋Œ“๊ธ€