๋ธ๋ผ์ฐ์ ๋ ๋๋ง ๋ฅ๋ค์ด๋ธ: DOM๋ถํฐ ํ๋ ์๊น์ง
์ ์ค์ํ๊ฐ
- ์ฌ์ฉ์๋ ๋น ๋ฅด๊ณ ๋งค๋๋ฌ์ด ํ๋ฉด์ ๊ธฐ๋ํ๋ค. ๋ ๋๋ง ํ์ดํ๋ผ์ธ์ ์ดํดํ๋ฉด LCP/CLS/INP ๊ฐ์ ํต์ฌ ์งํ๋ฅผ ๊ฐ์ ํ๊ณ , ์ฝ๋ยท์คํ์ผยท์ด๋ฏธ์ง ์ ๋ต์ ๊ตฌ์กฐ์ ์ผ๋ก ์ต์ ํํ ์ ์๋ค.
๋ ๋๋ง ํ์ดํ๋ผ์ธ ๊ฐ์
- HTML ํ์ฑ โ DOM ๊ตฌ์ฑ
- CSS ํ์ฑ โ CSSOM ๊ตฌ์ฑ
- Render Tree ์์ฑ(DOM + CSSOM ๊ฒฐํฉ)
- Layout(Reflow): ๊ฐ ๋ ธ๋์ ํฌ๊ธฐยท์์น ๊ณ์ฐ
- Paint: ํฝ์ ๊ทธ๋ฆฌ๊ธฐ(๋ฐฐ๊ฒฝ, ํ ์คํธ, ๋ณด๋ ๋ฑ)
- Composite: ๋ ์ด์ด(ํ์ผ)๋ฅผ GPU๋ก ํฉ์ฑํด ํ๋ฉด ํ์
Tip: transform/opacity ๋ณ๊ฒฝ์ ๋ณดํต ๋ ์ด์์ ์์ด ํฉ์ฑ๋ง ๋ฐ์. width/height/top/left ๋ฑ์ ๋ ์ด์์์ ์ ๋ฐํด ๋น์ฉ์ด ํฌ๋ค.
Critical Rendering Path(CRP)
- ๋ ๋์ ๊ผญ ํ์ํ ๋ฆฌ์์ค๊ฐ ํ๋ฉด ํ์๋ฅผ ์ง์ฐ์ํค๋ ๊ฒฝ๋ก
- ์์น
- CSS๋ ๋ ๋ ์ฐจ๋จ. ํฌ๋ฆฌํฐ์ปฌ CSS(Above-the-fold) ์ต์ํ, ๋๋จธ์ง๋ ์ง์ฐ ๋ก๋
- JS๋
defer/async๋ก ํ์ ์ฐจ๋จ์ ์ค์ด๊ณ , ๋ชจ๋ ๋ถํ ๋ก ์ด๊ธฐ ๋ฒ๋ค ์ถ์ preload(์ฆ์ ํ์),prefetch(์ถ๊ฐ ํ์ ๋๋น)๋ก ์ฐ์ ์์ ํํธ ์ ๊ณต- HTTP ์บ์/์์ถ/HTTP2/3 ๋ณ๋ ฌ์ฑ์ผ๋ก ๋คํธ์ํฌ ๋ ์ดํด์ ์ํ
Layout / Paint / Composite ์ฌํ
- Layout(Reflow)
- ๋ฐ์ ์กฐ๊ฑด: ๋ ์ด์์ ์ํฅ ์์ฑ ๋ณ๊ฒฝ, DOM ์ถ๊ฐ/์ญ์ , ํฐํธ ๋ก๋, ๋ทฐํฌํธ ๋ณํ ๋ฑ
- ์ํฐํจํด: ๋ ์ด์์ ์ค๋์ฑ(์ฝ๊ธฐ/์ฐ๊ธฐ ๊ต์ฐจ ๋ฐ๋ณต)
- ๊ฐ์ : ์ฝ๊ธฐโ๊ณ์ฐโ์ฐ๊ธฐ ์์๋ก ๋ฐฐ์น, ์คํ์ผ ๋ณ๊ฒฝ์ ํด๋์ค ํ ๊ธ๋ก ๋ฌถ๊ธฐ
- Paint
- ๋ฐ์ค ๊ทธ๋ฆผ์, ๊ทธ๋ผ๋ฐ์ด์ , ํฐ ์ด๋ฏธ์ง ์ค์ผ์ผ๋ง ๋ฑ์ ํ์ธํธ ๋น์ฉ ์ฆ๊ฐ
- ๊ฐ์ : ๊ณผ๋ํ ์๊ฐ ํจ๊ณผ ์ต์ํ, ์ ์ ํ ์ด๋ฏธ์ง ํฌ๊ธฐ ์ฌ์ฉ
- Composite(ํฉ์ฑ)
- ๋ ์ด์ด ๋ถ๋ฆฌ๋ก ์คํฌ๋กค/์ ๋๋ฉ์ด์
๋ถ๋๋ฝ๊ฒ.
will-change,transform: translateZ(0)๋ฑ์ผ๋ก ๋ ์ด์ด ์น๊ฒฉ - ๋จ์ฉ ์ฃผ์: ๋ ์ด์ด ์ฆ๊ฐโ๋ฉ๋ชจ๋ฆฌ/์ ๋ก๋ ๋น์ฉ ์์น. ์ผ์์ ์ ๋๋ฉ์ด์ ๊ตฌ๊ฐ์๋ง ์ฌ์ฉ
- ๋ ์ด์ด ๋ถ๋ฆฌ๋ก ์คํฌ๋กค/์ ๋๋ฉ์ด์
๋ถ๋๋ฝ๊ฒ.
๋ฉ์ธ ์ค๋ ๋, ์ด๋ฒคํธ ๋ฃจํ, ํ๋ ์ ๋ฒ์ง
- 60fps ๊ธฐ์ค ํ๋ ์๋น ์ฝ 16.7ms. ์ด ๋ด์ JS ์คํ, ์คํ์ผ/๋ ์ด์์, ํ์ธํธ/ํฉ์ฑ์ด ์๋ฃ๋์ด์ผ ํ๋ค.
- Long Task(>50ms) ํ์ง ๋ฐ ๋ถํ . Web Worker๋ก CPU ๋ฐ์ด๋ ์์ ์ ๋ถ๋ฆฌ.
- ์ค์ผ์ค๋ง
requestAnimationFrame(rAF): ํ์ธํธ ์ง์ ์ฝ๋ฐฑ, ๋ ์ด์์ ์์ ํ ํ์ด๋ฐrequestIdleCallback: ์ ํด ์๊ฐ ์์ ์ฒ๋ฆฌ(ํ์ ์์ ์ ๊ธ์ง)- ๋ง์ดํฌ๋กํ์คํฌ(Promise) vs ๋งคํฌ๋กํ์คํฌ(setTimeout) ์์ ์ดํด๋ก ์ง์ฐ/์ ์ฒด ๋ฐฉ์ง
์ด๋ฏธ์งยทํฐํธ ๋ก๋ฉ ์ ๋ต
- ์ด๋ฏธ์ง
- ์ ์ ํ ์ฌ์ด์ฆ์ ํฌ๋งท(WebP/AVIF) ์ฌ์ฉ,
loading="lazy",srcset/sizes๋ก ๋ฐ์ํ ์ ๊ณต - LCP ๋์ ์ด๋ฏธ์ง์
priority(ํ๋ ์์ํฌ ์ต์ ) ๋๋preload์ ์ฉ
- ์ ์ ํ ์ฌ์ด์ฆ์ ํฌ๋งท(WebP/AVIF) ์ฌ์ฉ,
- ํฐํธ
font-display: swap|optional๋ก FOIT ๋ฐฉ์ง, ์๋ธ์ ํฐํธ๋ก ํ์ผ ํฌ๊ธฐ ์ถ์- ์ค์ํ ํ ์คํธ๋ ์์คํ ํฐํธ ํด๋ฐฑ ๊ณ ๋ ค, ๊ฐ๋ณ ํฐํธ๋ก ์คํ์ผ ํตํฉ
React ๊ด์ : ๊ฐ์ DOM, CSR/SSR/SSG/ISR, ํ์ด๋๋ ์ด์
- ๊ฐ์ DOM๊ณผ Reconciliation: key ์์ ์ฑ์ผ๋ก ์ต์ ๋ณ๊ฒฝ ๋ณด์ฅ. ๋ถ์์ ํ key๋ ๋ถํ์ ์ฌ๋ง์ดํธ/๋ ์ด์์ ๋ณ๋ ์ ๋ฐ
- ๋ ๋๋ง ๋ฐฉ์
- CSR: ํ๋ถํ ์ธํฐ๋์ ์ ์ ๋ฆฌํ๋ ์ด๊ธฐ ๋ก๋ ๋น์ฉโ
- SSR/SSG/ISR: ์ด๊ธฐ ํ์/SEO ์ฐ์. ์ดํ ํ์ด๋๋ ์ด์ ์ผ๋ก ์ํธ์์ฉ ํ์ฑํ
- Next.js App Router
- ์๋ฒ ์ปดํฌ๋ํธ(๋ฐ์ดํฐ ๊ทผ์ , ๊ธฐ๋ณธ SSR) + ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ(์ํ/์ด๋ฒคํธ)
- ์คํธ๋ฆฌ๋ฐ/๋ถ๋ถ ํ์ด๋๋ ์ด์ ์ผ๋ก TTFBยทLCP ๊ท ํ
Web Vitals๋ก ์ธก์ ํ๊ณ ๊ฐ์ ํ๊ธฐ
- LCP: ์ต๋ ์ฝํ ์ธ ํ์ ์์ . ์ด๋ฏธ์ง/ํฐํธ ์ต์ ํ, SSR/SSG๋ก ๊ฐ์
- CLS: ๋ ์ด์์ ์ด๋. ์ด๋ฏธ์ง ํฌ๊ธฐ ์์ฝ, ๊ด๊ณ /์๋ฒ ๋์ ๊ณ ์ ๋ฐ์ค, ํฐํธ ์ค์
- INP(์ ๊ท): ์ ์ฒด ์ํธ์์ฉ ์ง์ฐ์ ๋ํ ์งํ. ๋ฉ์ธ ์ค๋ ๋ ์ ์ ๊ฐ์, ์ด๋ฒคํธ ํธ๋ค๋ฌ ๊ฒฝ๋ํ
- TTFB/TBT: ์๋ฒ/JS ์ ์ ์๊ฐ. ์บ์ฑยท์ฝ๋ ๋ถํ ยท์ง์ฐ ๋ก๋
- ๋๊ตฌ: Lighthouse, WebPageTest, Chrome DevTools, web-vitals ๋ผ์ด๋ธ๋ฌ๋ฆฌ(์ค์ฌ์ฉ ์ธก์ )
์ฒดํฌ๋ฆฌ์คํธ(์์ฝ)
- ์ค์ํ ์์ฐ ๋จผ์ : ํฌ๋ฆฌํฐ์ปฌ CSS ์ต์ํ, ํต์ฌ ์ด๋ฏธ์ง Preload
- ์ ๋๋ฉ์ด์ ์ transform/opacity ์ค์ฌ, rAF ์ฌ์ฉ
- ์ฝ๊ธฐ/์ฐ๊ธฐ ๋ถ๋ฆฌ๋ก ๋ ์ด์์ ์ค๋์ฑ ๋ฐฉ์ง, ๋ฐฐ์น ์ ๋ฐ์ดํธ
- ์ฝ๋ ๋ถํ ๊ณผ ์ง์ฐ ๋ก๋๋ก ์ด๊ธฐ ๋ฒ๋ค ์ถ์
- ์์ ์ ์ธ key๋ก ๊ฐ์ DOM ๋ณ๊ฒฝ ์ต์ํ
- ํฐํธ/์ด๋ฏธ์ง ์ ๋ต์ผ๋ก LCPยทCLS ๊ฐ์
์ฝ๋ ์ค๋ํซ
- ๋ ์ด์์ ์ค๋์ฑ ๋ฐฉ์ง(์ฝ๊ธฐโ์ฐ๊ธฐ ๋ฐฐ์น)
function batchDomUpdates(callback: () => void) {
// ์ฝ๊ธฐ ๋จ๊ณ
const width = element.offsetWidth;
const height = element.offsetHeight;
// ๊ณ์ฐ ๋จ๊ณ
const nextScale = Math.min(2, (width + height) / 500);
// ์ฐ๊ธฐ ๋จ๊ณ(rAF๋ก ํ์ธํธ ์ง์ ๋ณด์ฅ)
requestAnimationFrame(() => {
element.style.transform = `scale(${nextScale})`;
});
}
- transform/opacity ๊ธฐ๋ฐ ์ ๋๋ฉ์ด์
.card {
will-change: transform, opacity; /* ์ฅ์๊ฐ ์์ ์ฌ์ฉ์ ์ง์ */
transition: transform 200ms ease, opacity 200ms ease;
}
.card--enter {
transform: translateY(8px);
opacity: 0.8;
}
.card--enterActive {
transform: translateY(0);
opacity: 1;
}
- Next.js(SSR + ์คํธ๋ฆฌ๋ฐ ์์ ๋๋)
// app/articles/page.tsx (Server Component)
export default async function Page() {
const res = await fetch("https://api.example.com/articles", {
cache: "no-store",
});
const articles = await res.json();
return (
<main>
<h1>Articles</h1>
<ul>
{articles.map((a: any) => (
<li key={a.id}>{a.title}</li>
))}
</ul>
</main>
);
}
๋ง๋ฌด๋ฆฌ
- ๋ ๋๋ง์ ๋คํธ์ํฌโํ์โ์คํ์ผ/๋ ์ด์์โํ์ธํธโํฉ์ฑ์ผ๋ก ์ด์ด์ง๋ ์ฒด์ธ์ ๋ฌธ์ ๋ค. ๊ฐ ๋จ๊ณ์ ๋ณ๋ชฉ์ ์ ๋ ์ธก์ ํ๊ณ (์น ๋ฐ์ดํ/ํ๋กํ์ผ๋ฌ), ์์ธ์ ๋จ๊ณ๋ณ๋ก ์ ๊ฑฐํ๋ ๊ฒ์ด ๊ฐ์ฅ ํ์คํ ์ต์ ํ๋ค. ํ๋ ์์ํฌ์ ๊ธฐ๋ฅ(์คํธ๋ฆฌ๋ฐ, ์๋ฒ ์ปดํฌ๋ํธ, ์ฝ๋ ๋ถํ )๊ณผ ๋ธ๋ผ์ฐ์ ์ ํํธ(preload/prefetch)๋ฅผ ํจ๊ป ํ์ฉํ์.