
์๋ฌธ: 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 ํญ์ ๊ธฐ๋ณธ์ ์ธ ๊ฐ๋
๊ณผ ์ฑ๋ฅ ๋ถ์ ๊ทธ๋ํ๋ฅผ ์ฝ๋ ๋ฐฉ๋ฒ์ ์๊ณ ์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
๋ง์ฝ ๋ณต์ต์ด ํ์ํ์๋ค๋ฉด, ์ ๊ฐ ์ถ์ฒํ๋ ๊ธ๋ค์ ์๋ ์์๋๋ก ๋จผ์ ์ฝ์ด๋ณด์๋ ๊ฑธ ๊ถ์ฅ๋๋ฆฝ๋๋ค.
- Initial load performance for React developers: investigative deep dive
- Client-Side Rendering in Flame Graphs
- 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์์ ์ค๋ ์ฌ์ฉํ ๋ถ๋ถ๋ค์ ๊ฐ๋ตํ ์ ๋ฆฌํด ๋์์ต๋๋ค.
์ธก์ ๋์์ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์ฒ์์ผ๋ก ๋ค์ด๋ก๋๋๋ ์ฒซ ๋ฐฉ๋ฌธ์์ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์ผ๋ฐ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ์บ์์์ ์ ๊ณต๋๋ ์ฌ๋ฐฉ๋ฌธ์ ๋ชจ๋์ ๋๋ค.
์ธก์ ํ ๊ตฌ์ฒด์ ์ธ ํญ๋ชฉ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Largest Contentful Paint (LCP)๋ ์ฌ์ฉ์๊ฐ ์ฌ์ด๋๋ฐ์ ๋ฉ์์ง์ "์ค์ผ๋ ํค"์ผ๋ก ๋ ๋๋ง ๋ ํ์ด์ง๋ฅผ ๋ณด๋ ์๊ฐ๊ณผ ์ผ์นํฉ๋๋ค.
- "์ฌ์ด๋๋ฐ ํญ๋ชฉ ํ์" ์๊ฐ์ ์ค๋ช ์ด ํ์ ์์ ์ ๋๋ก ๊ฐ๋จํฉ๋๋ค. ์ฌ์ด๋๋ฐ ํญ๋ชฉ์ด ์๋ํฌ์ธํธ์์ ๊ฐ์ ธ์ ํ์ด์ง์ ๋ ๋๋ง ๋๋ ์๊ฐ์ ๋๋ค.
- "๋ฉ์์ง ํ์" ์๊ฐ์ ์์ ๋์ผํ์ง๋ง ๋ฉ์์ง์๋ง ์ ์ฉ๋ฉ๋๋ค.
- "ํ์ด์ง ์ํธ ์์ฉ" ์๊ฐ์ ํค๋์ ํ ๊ธ์ด ์๋ํ๊ธฐ ์์ํ๋ ์๊ฐ์ ๋๋ค(์ด ๋ถ๋ถ์ ์ค์์ฑ์ ๋์ค์ ์ค๋ช ํ๊ฒ ์ต๋๋ค).

๊ฐ ์ธก์ ์ ์ฌ๋ฌ ๋ฒ ๋ฐ๋ณตํ๊ณ , ์ค๊ฐ๊ฐ์ ์ฌ์ฉํด ์ด์์น๋ฅผ ์ ๊ฑฐํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ณง 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 |
์คํ์ ์ฌํํ๋ ๋จ๊ณ
- ์ ์ฅ์๋ฅผ ํด๋ก ํ๊ณ
npm install๋ก ๋ชจ๋ ์์กด์ฑ์ ์ค์นํฉ๋๋ค. - ๋ฐฑ์๋ API๋ฅผ ์คํํฉ๋๋ค:
npm run start --workspace=backend-api - ํ๋ฐํธ์๋๋ฅผ ๋น๋ํฉ๋๋ค:
npm run build --workspace=client-fetch-frontend - ํ๋ฐํธ์๋๋ฅผ ์คํํฉ๋๋ค:
npm run start --workspace=client-fetch-frontend - HTTP/2๋ฅผ ์ํ ๋ฆฌ๋ฒ์ค ํ๋ก์๋ฅผ ์คํํฉ๋๋ค:
caddy reverse-proxy --to :3000 - ์น์ฌ์ดํธ๋ฅผ
https://localhost/inbox์ฃผ์๋ก ์ฝ๋๋ค. - ์ธก์ ํ์ธ์!
์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง ์ธก์ (๋ฐ์ดํฐ ํจ์นญ ๋ฏธํฌํจ)
์ฌ์ฉ์๋ค์ด ๋น ํ์ด์ง๋ฅผ ๋๋ฌด ์ค๋ซ๋์ ์ณ๋ค๋ด์ผ ํ๋ค๋ ์ฌ์ค์ ์ด๋ ์์ ๋ถํฐ ์ฌ๋๋ค์ ์ง์ฆ๋๊ฒ ๋ง๋ค์์ต๋๋ค. ๋น๋ก ์ด๊ฒ์ด ์ฒซ ๋ฐฉ๋ฌธ์์๋ง ๋ฐ์ํ๋ ์ผ์ด๋ผ ํ๋๋ผ๋ ๋ง์ด์ฃ . ๊ฒ๋ค๊ฐ, 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 ๊ฐ์ ์ ์ํด ๊ฐ์ํด์ผ ํ๋ ๋๊ฐ์ ๋๋ค. ์์ ํ ์์จ ์ ์๋ ๋ฐฉ๋ฒ์ ์์ง๋ง, ์ฒซ ์คํ ์ ๋ค์ด๋ก๋ํด์ผ ํ๋ ์๋ฐ์คํฌ๋ฆฝํธ์ ์์ ์ค์์ผ๋ก์จ ๊ทธ ์ํฅ์ ์ต์ํํ ์ ์์ต๋๋ค.
์คํ์ ์ฌํํ๋ ๋จ๊ณ
- ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง ๋์ ์๋ฒฝํ๊ฒ ๋์ผํ ๋จ๊ณ๋ฅผ ๋ฐ๋ฅด๋, ๋ค์ ๋จ๊ณ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
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)์ด ๋ ๊ฒ์ ๋๋ค.
์คํ์ ์ฌํํ๋ ๋จ๊ณ
- ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง ๋์ ์๋ฒฝํ๊ฒ ๋์ผํ ๋จ๊ณ๋ฅผ ๋ฐ๋ฅด๋, ๋ค์ ๋จ๊ณ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
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 ์ ๋ ๋ ๋น ๋ฅด๊ฒ ๋ก๋๋์ด, ํ์ด์ง๊ฐ ์ํธ์์ฉ ๊ฐ๋ฅํ ์ํ๊ฐ ๋๋ ์๊ฐ์ด ํจ์ฌ ๋จ์ถ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฒฐ๊ณผ์ ์ผ๋ก, "์ํธ์์ฉ ๋ถ๊ฐ๋ฅ" ๊ฐ๊ทน์ ํจ์ฌ ๋ ์งง๊ฒ ๋ง๋ญ๋๋ค.
์คํ์ ์ฌํํ๋ ๋จ๊ณ
- ์ ์ฅ์๋ฅผ ํด๋ก ํ๊ณ
npm install๋ก ๋ชจ๋ ์์กด์ฑ์ ์ค์นํฉ๋๋ค. - ๋ฐฑ์๋ API๋ฅผ ์คํํฉ๋๋ค:
npm run start --workspace=backend-api frontend/utils/link.tsxํ์ผ๋ก ์ด๋ํด์ Next.js Link๋ฅผ ์ฃผ์ ํด์ ํ๊ณ ์ปค์คํ ๊ตฌํ์ ์ฃผ์ ์ฒ๋ฆฌ ํฉ๋๋ค.src/frontend/next-pages/pages/inbox.tsxํ์ผ๋ก ์ด๋ํ์ฌ ๋ฐ์ดํฐ ํจ์นญ ์ฌ๋ก ๊ฐ ์ ํ์ ์ํดgetServerSideProps๋ฅผ ์ฃผ์ ์ฒ๋ฆฌ/ํด์ ํฉ๋๋ค.- ํ๋ฐํธ์๋๋ฅผ ๋น๋ํฉ๋๋ค:
npm run build --workspace=next-pages - ํ๋ฐํธ์๋๋ฅผ ์คํํฉ๋๋ค:
npm run start --workspace=next-pages - HTTP2/3๋ฅผ ์ํ ๋ฆฌ๋ฒ์ค ํ๋ก์๋ฅผ ์คํํฉ๋๋ค:
caddy reverse-proxy --to :3000 - ์น์ฌ์ดํธ๋ฅผ
https://localhost/inbox์ฃผ์๋ก ์ฝ๋๋ค.
๋ฆฌ์กํธ ์๋ฒ ์ปดํฌ๋ํธ ์๊ฐ
์ข์ต๋๋ค. ์ด์ ์น์ ์ ์์ฝํ์๋ฉด, ์๋ฒ์์ ๋ฐ์ดํฐ ํจ์นญ์ ํ๊ณ ์ฌ์ ๋ ๋๋ง ํ๋ ๊ฒ์ ์ด๊ธฐ ๋ก๋ ์ฑ๋ฅ ์์น์ ์ ๋ง๋ก ํฐ ๋์์ด ๋ ์ ์์ต๋๋ค. ํ์ง๋ง ์ฌ์ ํ ๋ช ๊ฐ์ง ๋ฌธ์ ๊ฐ ๋จ์์์ต๋๋ค.
SSR์ ๊ฐ์ฅ ํฐ ๋ฌธ์ ๋ "์ํธ์์ฉ ๋ถ๊ฐ๋ฅ ๊ฐ๊ทน"์ ๋๋ค. ์ฆ, ํ์ด์ง๊ฐ ์ด๋ฏธ ๋ณด์ด์ง๋ง ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์ฌ์ ํ ๋ค์ด๋ก๋ ๋ฐ ์ด๊ธฐํ๋๊ณ ์๋ ์๊ฐ์ด์ฃ . ์ด ๊ฐ๊ทน์ ๋จ์ถ์ํค๋ ์ ์ผํ ๋ฐฉ๋ฒ์ ์ํธ์์ฉ์ ํ์ํ ์๋ฐ์คํฌ๋ฆฝํธ์ ์์ ์ค์ด๋ ๊ฒ์ ๋๋ค. ์ฐ๋ฆฌ๋ ์ด๋ฏธ Next.js Pages๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ๋ฉด์ ์ฝ๋ ์คํ๋ฆฌํ ์ ์ํํ๊ณ , ์ด ์์ญ์์ 1s ์ด์์ ๊ฐ์ ์ ํ์ธํ์ต๋๋ค. ์ฝ๋ ์คํ๋ฆฌํ ์ธ์ ์ฌ๊ธฐ์ ๋ ํ ์ ์๋ ์ผ์ด ์์๊น์? ์ฐ๋ฆฌ๋ ๊ณผ์ฐ ๊ทธ ๋ชจ๋ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ํ์ํ๊ธฐ๋ ํ ๊ฑธ๊น์?
SSR์ ๋ ๋ฒ์งธ ๋ฌธ์ ๋ ๋ฐ์ดํฐ ํจ์นญ์ ๋๋ค. ํ์ฌ ์ํฉ์์, ๋ฉ์์ง๊ฐ ๋ํ๋๋ ๋๊ธฐ ์๊ฐ์ ์ค์ด๊ธฐ ์ํด ์๋ฒ์์ ๋ฉ์์ง๋ฅผ ๋ฏธ๋ฆฌ ํจ์นญ ํ๋ ค ํ๋ค๋ฉด, ์ด๋ ์ด๊ธฐ ๋ก๋ ์๊ฐ๊ณผ ์ฌ์ด๋๋ฐ ํญ๋ชฉ์ด ๋ํ๋๋ ์๊ฐ ๋ชจ๋์ ๋ถ์ ์ ์ธ ์ํฅ์ ๋ฏธ์น๊ฒ ๋ฉ๋๋ค.
์ด๊ฒ์ ํ์ฌ ์๋ฒ ๋ ๋๋ง์ด ๋๊ธฐ์ ์ธ ํ๋ก์ธ์ค๋ผ๋ ์ฌ์ค ๋๋ฌธ์
๋๋ค. ์ฐ๋ฆฌ๋ ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์ค๋น๋ ๋๊น์ง ๋จผ์ ๊ธฐ๋ค๋ฆฐ ๋ค์, ๊ทธ ๋ฐ์ดํฐ๋ฅผ renderToString์ ์ ๋ฌํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฌผ์ ํด๋ผ์ด์ธํธ์ ์ ์กํฉ๋๋ค.

ํ์ง๋ง ๋ง์ฝ ์ฐ๋ฆฌ ์๋ฒ๊ฐ ๋ ๋๋ํด์ง ์ ์๋ค๋ฉด ์ด๋จ๊น์? ํจ์นญ ์์ฒญ๋ค์ Promise์ด๋ฉฐ, ๋น๋๊ธฐ ํจ์์ ๋๋ค. ๊ธฐ์ ์ ์ผ๋ก, ์ฐ๋ฆฌ๋ ๋ค๋ฅธ ์์ ์ ์์ํ๊ธฐ ์ํด ๊ทธ๋ค์ ๊ธฐ๋ค๋ฆด ํ์๊ฐ ์ ํ ์์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ด ํ ์ ์๋ค๋ฉด ์ด๋จ๊น์?
- ๊ทธ ํจ์น Promise๋ค์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์คํํฉ๋๋ค.
- ํด๋น ๋ฐ์ดํฐ๊ฐ ํ์ ์๋ ๋ฆฌ์กํธ ์์๋ค์ ๋ ๋๋ง์ ์์ํ๊ณ , ์ค๋น๊ฐ ๋๋ฉด ์ฆ์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํฉ๋๋ค.
- ์ฌ์ด๋๋ฐ ํญ๋ชฉ์ Promise๊ฐ ๋ฐํ๋๊ณ ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ฉด, ์ฌ์ด๋๋ฐ ๋ถ๋ถ์ ๋ ๋๋ง ํ๊ณ , ์๋ฒ ํ์ด์ง์ ์ฃผ์ ํ ๋ค์, ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํฉ๋๋ค.
- ๋ฉ์์ง ํญ๋ชฉ์๋ ๋์ผํ๊ฒ ์ ์ฉํฉ๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก, ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง์์ ๊ฐ์ง๊ณ ์๋ ๋ฐ์ดํฐ ํจ์นญ์ ์ ํํ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ์๋ฒ์์ ์ ์ฉํ๋ ๊ฒ์ ๋๋ค.

์ด๊ฒ์ด ์ด๋ก ์ ์ผ๋ก ๊ฐ๋ฅํ๋ค๋ฉด, ๋ฏธ์น ๋ฏ์ด ๋น ๋ฅผ ์ ์์ต๋๋ค. ์ฐ๋ฆฌ๋ ๊ฐ์ฅ ๋จ์ํ 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๊ฐ ๋๋ฌด ๋ง์ ๊ณ ๋ฏผ ์์ด ์ฌ๊ธฐ์ ๊ธฐ ๋ฌด์์๋ก ๋ถ์ด ์๊ณ ๊ฒฐ๊ตญ ๋๋ถ๋ถ์ ํ์ด์ง์ ์ต์๋จ๊น์ง ๊ฑฐ์ฌ๋ฌ ์ฌ๋ผ๊ฐ๋ ๋๋ถ๋ถ์ ์ค์ ๋ณต์กํ ์ฑ์์๋ ๋์ผํ ๊ฒ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
์คํ์ ์ฌํํ๋ ๋จ๊ณ
- ๋ฐฑ์๋ API๋ฅผ ์คํํฉ๋๋ค:
npm run start --workspace=backend-api frontend/utils/link.tsxํ์ผ๋ก ์ด๋ํด์ Next.js Link๋ฅผ ์ฃผ์ ํด์ ํ๊ณ ์ปค์คํ ๊ตฌํ์ ์ฃผ์ ์ฒ๋ฆฌ ํฉ๋๋ค.src/frontend/next-app-router/src/appํด๋๋ก ์ด๋ํ์ฌ, ๋ชจ๋ page ํ์ผ ์๋จ์use client๋ฅผ ์ถ๊ฐํ์ฌ ๋ชจ๋ ๊ฒ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ก ๊ฐ์ ์ ํํฉ๋๋ค. ์ดํ, ํฉ๋ฆฌ์ ์ธ ์์ ์๋ฒ ์ปดํฌ๋ํธ๋ก ๋๋๋ฆฌ๋ ค๋ฉด (์์ง ๋ฐ์ดํฐ ํจ์นญ์ ์ ์ธ), ๋ชจ๋use client๋ฅผ ์ ๊ฑฐํฉ๋๋ค.- ํ๋ฐํธ์๋๋ฅผ ๋น๋ํฉ๋๋ค:
npm run build --workspace=next-app-router - ํ๋ฐํธ์๋๋ฅผ ์คํํฉ๋๋ค:
npm run start --workspace=next-app-router - HTTP2/3๋ฅผ ์ํ ๋ฆฌ๋ฒ์ค ํ๋ก์๋ฅผ ์คํํฉ๋๋ค:
caddy reverse-proxy --to :3000 - ์น์ฌ์ดํธ๋ฅผ
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์ ์๋ฃ๋๋ ์ฆ์ ์์ ์ด ๋๋ฉ๋๋ค. ๊ธฐ๋ค๋ฆด ํ์๊ฐ ์์ต๋๋ค.
์คํ์ ์ฌํํ๋ ๋จ๊ณ
- ์ด์ ์น์ ๊ณผ ๋์ผํ ๋จ๊ณ๋ฅผ ๋ฐ๋ฅด๋, ๋ค์ ๋จ๊ณ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
src/frontend/next-app-router/src/app/inbox/page.tsxํ์ผ๋ก ์ด๋ํ์ฌ, Suspense๋ฅผ ์ ์ธํ๊ณ ๋ ๋๋ง ํจ์ ๋ด์ ๊ด๋ จ import ๋ฐ ๋ชจ๋ ์ฝ๋๋ฅผ ์ฃผ์ ์ฒ๋ฆฌ/ํด์ ํ์ฌ ์๋ฒ ๋ฐ์ดํฐ ํจ์นญ์ด ํฌํจ๋ "๊ณ ์ฅ ๋" ์คํธ๋ฆฌ๋ฐ ๊ฒฝํ์ ํ ์คํธํฉ๋๋ค.- ์ ํ์ผ๊ณผ
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์ ๊ตฌ๋ ํด์ฃผ์ธ์!
'๐จโ๐ป web.dev > translates' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ฐ์คํฌ๋ฆฝํธ ํ์ด๋จธ๋ฅผ ์ค๋กํ๋ง(throttle) ํ๋ ์ด์ ๋ ๋ฌด์์ผ๊น์? (2) | 2025.10.23 |
|---|
๐ฌ ๋๊ธ