๊ธฐ๋ณธ๊ณผ์ (Vanilla SSR & SSG) ์ฌํ๊ณผ์ (React SSR & SSG)
- Express ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฐ ์๋ฒ ๊ตฌํ
- ๊ฐ๋ฐ/ํ๋ก๋์ ํ๊ฒฝ ๋ถ๊ธฐ ์ฒ๋ฆฌ
- HTML ํ
ํ๋ฆฟ ์นํ (
<!--app-html-->,<!--app-head-->)
- ์๋ฒ์์ ๋์ํ๋ Router ๊ตฌํ
- ์๋ฒ ๋ฐ์ดํฐ ํ๋ฆฌํ์นญ (์ํ ๋ชฉ๋ก, ์ํ ์์ธ)
- ์๋ฒ ์ํ๊ด๋ฆฌ ์ด๊ธฐํ
-
window.__INITIAL_DATA__์คํฌ๋ฆฝํธ ์ฃผ์ - ํด๋ผ์ด์ธํธ ์ํ ๋ณต์
- ์๋ฒ-ํด๋ผ์ด์ธํธ ๋ฐ์ดํฐ ์ผ์น
- ๋์ ๋ผ์ฐํธ SSG (์ํ ์์ธ ํ์ด์ง๋ค)
- ๋น๋ ํ์ ํ์ด์ง ์์ฑ
- ํ์ผ ์์คํ ๊ธฐ๋ฐ ๋ฐฐํฌ
-
renderToString์๋ฒ ๋ ๋๋ง - TypeScript SSR ๋ชจ๋ ๋น๋
- Universal React Router (์๋ฒ/ํด๋ผ์ด์ธํธ ๋ถ๊ธฐ)
- React ์ํ๊ด๋ฆฌ ์๋ฒ ์ด๊ธฐํ
- Hydration ๋ถ์ผ์น ๋ฐฉ์ง
- ํด๋ผ์ด์ธํธ ์ํ ๋ณต์
- ๋์ ๋ผ์ฐํธ SSG (์ํ ์์ธ ํ์ด์ง๋ค)
- ๋น๋ ํ์ ํ์ด์ง ์์ฑ
- ํ์ผ ์์คํ ๊ธฐ๋ฐ ๋ฐฐํฌ
SSR, SSG, CSR์ ๋ณธ์ง์ ์ธ ์ฐจ์ด๋ HTML์ ๋๊ฐ, ์ธ์ , ์ด๋ค ์ฑ ์์ผ๋ก ์์ฑํ๋๋์ด๋ค.
(1-1) ์๋ฒ
- ์์ฒญ์ ๋ฐ๋ ์ฃผ์ฒด
- ๋ฐ์ดํฐ ์กฐํ ๊ฐ๋ฅ
- HTML ๋ฌธ์์ด์ ๋ง๋ค์ด์ ๋ด๋ ค์ค ์ ์์
- ๊ฒฐ๊ณผ๋ฌผ์ ์ฑ ์์ ์ง
(1-2) ํด๋ผ์ด์ธํธ
- ์๋ฒ๊ฐ ์ค ๊ฑธ ๋ฐ์์ ์คํ
- DOM์ ๋ง๋ค๊ณ JS๋ฅผ ์คํ
- ์ฌ์ฉ์ ์ ๋ ฅ ์ฒ๋ฆฌ
- HTML์ ์กฐ๋ฆฝํ๊ฑฐ๋ ์์
(2-1) SSG
- ๋น๋ ํ์
- HTML ํ์ผ ์์ฑ
- CDN / ์๋ฒ์ ์ ์ฅ
- ์์ฒญ ์ ํ์ผ ์ ๋ฌ HTML์ ์ด๋ฏธ ๋ง๋ค์ด์ ธ์๊ณ => ์๋ฒ๋ ํด๋ผ์ด์ธํธํํ ํ์ผ์ ์ ๋ฌํ์ฌ => ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ์์ ๊ทธ๋ฆฌ๊ธฐ๋ง ํ๋ค. ์ฆ, SSG๋ ๋น๋ ํ์์ ๋๋ SSR์ด๋ผ๊ณ ์ดํดํ ์ ์๋ค!
(2-2) SSR
- ์์ฒญ ๋ฐ์
- ์๋ฒ๊ฐ ๋ฐ์ดํฐ ์กฐํ
- HTML ๋ฌธ์์ด ์์ฑ
- ์๋ต์ผ๋ก ์ ๋ฌ HTML์ ์์ฒญ ์์ ์ ์๋ฒ๊ฐ ์์ฑํด์ => ์๋ฒ๋ ๋งค ์์ฒญ๋ง๋ค ๊ณ์ฐํ๊ธฐ ๋๋ฌธ์ => ์ฌ์ฉ์๋ง๋ค ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค.
(2-3) CSR
- ์์ฒญ ๋ฐ์
- ๋น HTML + JS ๋ค์ด๋ก๋
- ๋ธ๋ผ์ฐ์ ์์ JS ์คํ
- DOM ์์ฑ
- ์๋ฒ๋ ํ๋ฉด์ ์ค์ ์ฝํ ์ธ HTML ์์ฑ์๋ ๊ด์ฌํ์ง ์๊ณ => ๋ธ๋ผ์ฐ์ ๊ฐ ์ง์ HTML ๊ตฌ์กฐ๋ฅผ ์์ฑํ๊ณ ๋ฐ์ดํฐ fetch๋ ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ค.
SSG๋ ํธ์์ ๋์๋ฝ์ผ๋ก, ์ ์๋ ์ธ์ง์ ๋ฐ์ฐ๋ฉด ๋ฐ๋ก ๋จน์ ์ ์๊ณ ์๋ฆฌํ๋ ๊ณผ์ ์ ์ด๋ฏธ ์๋ฃ๋์ด์๋ค. SSR์ ์ค๋น์ง์ผ๋ก, ์ฃผ๋ฌธ์ด ๋ค์ด์ค๋ฉด ๊ทธ ๋ ๋ฐ๋ก ์๋ฆฌ๋ฅผ ํ๋๋ฐ ์์ฒญ๋ง๋ค ๋ค๋ฅธ ๋ฉ๋ด๋ฅผ ์ ๊ณตํด์ค๋ค. CSR์ ์ง์ ์๋ฆฌํ๋ ๊ฒ์ผ๋ก, ์ฌ๋ฃ๋ง ๋งํธ์์ ์ฌ์ค๊ณ ์ง์์ ์ง์ ์กฐ๋ฆฌํ๋ค. CDN์ ๋์ฌ ๊ณณ๊ณณ์ ๋ฐฐ์น๋ ํธ์์ ๊ฐ์ ์ค๊ฐ ํฝ์ ํ๋ธ๋ค.
์๋ฒ๋์ด๋ ์๊ธฐํ๋ฉด์ ๋น์ ์ ํ์ฅ์ ์งํํด๋ณด์๋ค.
(3-1) SSG ํธ์์ ๋์๋ฝ์ ์ฅ์ ์ ์ด๋ฏธ ๋ง๋ค์ด์ ธ์์ด์(๋น๋ ํ์ + HTML ํ์ผ ์์ฑ) ํธ์์ ์ ๋น์น๋์ด์๊ณ (CDN / ์๋ฒ์ ์ ์ฅ) ๋จน๊ณ ์ถ์ ๋ ๊ตฌ๋งคํ๊ณ (์์ฒญ ๋ฐ์) ๊ทธ๋ฅ ๊ทธ๋๋ก ์ ์๋ ์ธ์ง์ ๋ฐ์๋จน์ผ๋ฉด ์ข๋ค(ํ์ผ ์ ๋ฌ)๋ ์ ์ด๋ค. ๊ทธ๋ฌ๋ ๋ด๊ฐ ๋ง์ฝ ํธ์์ ๋์๋ฝ์ ๊ณ๋๋ง์ด๋ฅผ ์ํ๋ค ํด๋ ๊ทธ๊ฑด ์ถ๊ฐ๊ฐ ๋ถ๊ฐ๋ฅํ๋ค. ์ฆ, ์๋ก์ด ์์ฒญ์ ๋ฐ์ ์ ์์ง๋ง ์์ฒญ๋ง๋ค ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํ ์๋ ์๋ค.
(3-2) SSR ์ค๋น์ง์ ๋ด๊ฐ ๊น์น๋ณถ์๋ฐฅ์ด ๋จน๊ณ ์ถ๋ค๊ณ ์ฃผ๋ฌธํ๋ฉด(์์ฒญ ๋ฐ์) ์ฌ์ฅ๋์ด ์ฃผ๋ฌธ์ ๋ฐ๊ณ (์๋ฒ๊ฐ ๋ฐ์ดํฐ ์กฐํ) ๊น์น๋ณถ์๋ฐฅ์ ๋ง๋ค์ด์ฃผ์๊ณ (HTML ๋ฌธ์์ด ์์ฑ) ๋ด ํ ์ด๋ธ๋ก ๊ฐ๋ค์ฃผ์ ๋ค.(์๋ต์ผ๋ก ์ ๋ฌ) ์ฃผ๋ฌธํ๋๋๋ก ๋จน์ ์ ์์ด์ ์ข๋ค!! ํ์ง๋ง ์ฌ์ฅ๋์ด ์์ฒญ์ ๋ฐ๊ณ ์๋ฆฌ๋ฅผ ํด์ฃผ์๋ ๋ฐ์ ์๊ฐ์ด ๊ฑธ๋ ค์ ๊ทธ๋งํผ์ ๋๋ ์ด๊ฐ ์๋ค๋ ๊ฑด ์์ฝ๋คใ
(3-3) CSR ์๋ฆฌ๋ ๋ด๊ฐ ๊น์น๋ณถ์๋ฐฅ์ ๋จน๊ณ ์ถ์ผ๋ฉด(์์ฒญ ๋ฐ์) ํ์ํ ์ฌ๋ฃ์ธ ๊น์น, ๋ฐฅ, ๊ฐ์ฅ, ์คํ ๋ฑ์ ๋งํธ์์ ์ฌ์(๋น HTML + JS ๋ค์ด๋ก๋) ์ง์์ ๋ ์ํผ๋๋ก ์๋ฆฌํ๊ณ (๋ธ๋ผ์ฐ์ ์์ JS ์คํ) ๊น์น๋ณถ์๋ฐฅ์ ์์ฑ(DOM ์์ฑ)ํด์ ๋จน์ ์ ์๋ค. ๋์ ๋งํธ์์ ์ฅ๋ณด๊ณ ์๋ฆฌํ๋ ๋ฐ ์๊ฐ์ด ๊ฝค ์์๋๋คใ
(3-4) CDN ํฝ์ ํ๋ธ๋ ์์ ํ์ด๋ ์ฌ๋ฃ๋ฅผ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ณณ์์ ๋น ๋ฅด๊ฒ ์ ๋ฌํด์ฃผ๋ ์ค๊ฐ ๊ฑฐ์ ์ด๋ค.
์ด๋ฒ ๊ณผ์ ๋ฅผ ์งํํ๋ฉฐ ๊ฐ์ฅ ํฌ๊ฒ ์ฒด๊ฐํ ๋ ํ๋์ ํฌ์ธํธ๋ SSR/SSG์์ ํ๋ฉด์ โ๋ณด์ฌ์ฃผ๋ ๊ฒโ๋ณด๋ค, ๊ทธ ์ดํ์ Hydration์ด ํจ์ฌ ์ค์ํ๋ค๋ ์ ์ด์๋ค.
์๋ฒ์์ HTML์ ๋ด๋ ค์ฃผ๋ฉด ์ฌ์ฉ์๋ ์ฆ์ ์ฝํ ์ธ ๋ฅผ ๋ณผ ์ ์์ง๋ง, ์ด ์ํ์ ํ๋ฉด์ ์์ง ์ ์ ์ธ HTML์ผ ๋ฟ, ์ค์ ์ฌ์ฉ์ ์ํธ์์ฉ์ ๋ถ๊ฐ๋ฅํ๋ค.
์ดํ ํด๋ผ์ด์ธํธ์์ Javascript๊ฐ ์คํ๋๋ฉฐ ์๋ฒ์์ ๋ด๋ ค์จ HTML๊ณผ ๋์ผํ ์ํ๋ก React/Vainlla ์ฑ์ด ๋ณต์๋๋ ๊ณผ์ ์ Hydration์ด๋ผ๊ณ ํ๋ค.
์ด ๊ณผ์ ์์ ์๋ฒ์ ํด๋ผ์ด์ธํธ์ ์ํ๋ ๋งํฌ์ ์ด ์กฐ๊ธ์ด๋ผ๋ ์ด๊ธ๋๋ฉด
- ํ๋ฉด ๊น๋นก์
- ์ด๋ฒคํธ ๋ฐ์ธ๋ฉ ์คํจ
- ์ฝ์ ๊ฒฝ๊ณ (hydration mismatch) ์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค.
์ด๋ฒ ๊ณผ์ ์์ window.__INITIAL_DATA__๋ฅผ ํตํด ์๋ฒ ์ํ๋ฅผ ์ ๋ฌํ๊ณ ํด๋ผ์ด์ธํธ์์ ๋์ผํ ์ด๊ธฐ ์ํ๋ก ๋ณต์ํ๋ ์์
์ ์ง์ ๊ตฌํํ๋ฉด์
SSR/SSG์ ํต์ฌ์ โHTML์ ๋นจ๋ฆฌ ๋ณด์ฌ์ฃผ๋ ๊ฒโ์ด ์๋๋ผ โ์๋ฒ์ ํด๋ผ์ด์ธํธ์ ์ํ๋ฅผ ์ ํํ ์ผ์น์ํค๋ ๊ฒโ์ด๋ผ๋ ์ ์ ๋ช
ํํ ์ดํดํ๊ฒ ๋์๋ค.
์ ํต์ ์ธ ์น ๊ฐ๋ฐ ๋ฐฉ์์ SSR์ด์๋ค. ๊ทธ๋ฌ๋ Javascript๊ฐ ์ ์ ๋ฐ์ ํ๋ฉด์ Javascript๋ก ํ ์ ์๋ ์ผ์ด ๋ง์์ง๊ฒ ๋์๊ณ ๊ทธ๋ฌ๋ฉด์ ํ์๋ ํ๋ ์์ํฌ๊ฐ ๋ฑ์ฅํ๋ฉด์ ๋ชจ๋ ๋ ๋๋ง์ ์ ๋ถ ๋ธ๋ผ์ฐ์ (ํด๋ผ์ด์ธํธ)์๊ฒ ์์ํ๊ฒ ๋์๋ค.
๊ทธ๋ก ์ธํด ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ค์ด CSR์ ์ฃผ๋ก ๋ค๋ฃจ๊ฒ ๋์๋ค. ๊ทธ๋ฐ๋ฐ CSR์ ์์์ ๋ค๋ค๋ฏ SEO ์ต์ ํ ๋ฌธ์ ๋ก ์ธํด ์๋น์ค์ ์ธ ํ๊ณ๊ฐ ์กด์ฌํ๊ณ ๋ค์ SSR์ด ํ์ํด์ง๊ฒ ๋์๋ค.
์ฆ, ์ง๊ธ๊น์ง์ ํ๋ฆ์ SSR => CSR => SSR+CSR์ธ ๊ฒ์ด๋ค.
๋จ, ๋ชจ๋ ์ํฉ์ SSR์ ์ ์ฉํ๋ ๊ฒ์ ์๋๋ค. ๊ฒ์์์ง์ ์ต์ ํ๊ฐ ํ์ํ ๋, ์ฌ์ฉ์์๊ฒ ๋น์ด์๋ ํ๋ฉด์์ด ๋น ๋ฅด๊ฒ ํ์ด์ง๋ฅผ ๋ณด์ฌ์ค์ผํ ๋์๋ง SSR์ ์ ์ฉํ๋ฉด ๋๋ค.
(1) ISR (Incremental Static Regeneration)
- Next.js ๋ฑ ์ต์ ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ผ๋ก, ์ ์ ํ์ด์ง๋ฅผ ํ์ํ ๋๋ง ๊ฐฑ์ ํ๋ค.
- ์ ์ฒด ์ฌ์ดํธ๋ฅผ ๋ค์ ๋น๋ํ์ง ์๊ณ ํน์ ํ์ด์ง๋ง ์ ๋ฐ์ดํธํ ์ ์์ด, SSG์ ๋จ์ (์ ์ ์ด๋ผ ์ ๋ฐ์ดํธ ์ด๋ ค์)์ ๋ณด์ํ๋ค.
(2) Edge Computing / Serverless
- SSR์ ๊ธ๋ก๋ฒ ๊ท๋ชจ๋ก ๋น ๋ฅด๊ฒ ์ ๊ณตํ๋ ค๋ฉด ์๋ฒ๋ฅผ ์ค์์ ๋๊ธฐ๋ณด๋ค๋ ์ฌ์ฉ์ ๊ฐ๊น์ด ์ฃ์ง ์์น์์ ๋ ๋๋งํ๋ ๊ฒ์ด ์ค์ํ๋ค.
- AWS Lambda@Edge, Vercel Edge Functions, Cloudflare Workers ๋ฑ์ด ๋ํ์ ์ด๋ค. (์ ๊ฐ๋ ์ ์ด๋ ดํ์ด ๋ค์ด๋ด์๊ณ ์ ํํ ์ด๋ฆ์ ์ฒ์ ์ ๊ฒ ๊ฐ๋ค.)
(3) Streaming SSR
- ์๋ฒ๊ฐ HTML์ ํ ๋ฒ์ ๋ค ๊ทธ๋ ค์ ๋ณด๋ด๋ ๋์ , ์กฐ๊ฐ ๋จ์๋ก ์คํธ๋ฆฌ๋ฐํด ์ฌ์ฉ์์๊ฒ ๋ ๋น ๋ฅด๊ฒ ์ฒซ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฒ.
- React 18, Next.js, Remix ๋ฑ์ด ์ง์. ํนํ ๋ฐ์ดํฐ๊ฐ ๋ฌด๊ฒ๊ฑฐ๋ ๋น๋๊ธฐ ํธ์ถ์ด ๋ง์ ๊ฒฝ์ฐ ์ ๋ฆฌ.
์ผ๋จ ๊ณผ์ ์ ๋ณธ์ง์ ๋ํด ์ดํดํ๋ ์๊ฐ๋ถํฐ ๊ฐ์ก๋ค. ๋ฐ์ ๋ ธ์ ๊ณผ ์ค์ผ ์ฝ์น๋์ ์ด์ ๋ธ๋ก๊ทธ ๊ธ์ ๋ณด์์ ๋ ์ด๋ฒ ๊ณผ์ ์์ ์๊ตฌํ๋ ๋ฐ๋ ์๋์ ๊ฐ๋ค๊ณ ์๊ฐํ๋ค.
์๋ฒ๋ฅผ ์ง์ ๊ตฌ์ถํ ๋ค, ์ด ํ์ด์ง๋ SSR, ์ด ํ์ด์ง๋ SSG, ์ด๋ค ์์ญ์ CSR์ด๋ผ๋ ํ๋จ ๊ทผ๊ฑฐ๋ฅผ ๊ฐ์ง๊ณ
๊ฐ ๋ ๋๋ง ๋ฐฉ์์ ์ค์ ๋ก ๊ตฌํํ๊ณ ์ฐ๊ฒฐ
์ฆ, ๊ตฌํ ์์ฒด๋ ์ค์ํ์ง๋ง ๊ทธ๋ฟ๋ง ์๋๋ผ ์ ๊ฐ ์์ญ์ ๊ฐ ๋ฐฉ์์ ๋์ ํ๊ณ ์ ํ๋์ง์ ๋ํด ์ฌ๊ณ ํ๊ณ ํ๋จํ ์ค ์๋ ๊ฒ์ด ์ค์ํ๋ค๊ณ ์๊ฐํ๋ค.
๋ ๊ทธ๋ ๋ฏ ๊ฐ ๊ฐ๋ ์ ๋ํด ๋น์ ๊น์ง ๊ฐ๋ฅํ ์ ๋๋ก ์ดํดํ๊ณ ์ ๋ฆฌํ๋ ์๊ฐ์ ๊ฐ์ก๋ค. ์ด ๋ ์๋ฒ๋๊ณผ ๋น์ ๋ด์ฉ์ ๊ณต์ ํ๋ฉฐ ์๊ฐ์ ํ์ฅ๊ณผ ํผ๋๋ฐฑ์ ์ฃผ๊ณ ๋ฐ์ ๊ฒ ๋์๋์๋ค.
๊ณผ์ TODO๋ฅผ ์์ฐจ์ ์ผ๋ก ์งํํ์ฌ ์๋ฒ ๊ตฌํ์ ์ฒซ๋ฒ์งธ๋ก ์งํํ๋ค.
์ฝ์น๋์ด ๋ผ์ด๋ธ ์ฝ๋ฉ์ผ๋ก ๋ณด์ฌ์ฃผ์ จ๋ ๋ฐฉ์์ ํ์ฉํ์ฌ ์๋ฒ ๊ตฌํ์ ํ๋๋ฐ vite์์ ์ ๊ณตํ SSR ๊ด๋ จ ์ฝ๋๋ฅผ ๋ณต๋ถํ๋ค๋ณด๋ ๋ชจ๋ฅด๋ ํค์๋๋ ์์ด์ ์ฐพ์๋ณด๊ณ ์ ๋ฆฌํ๋ค.
(2-1) compression ๋ง ๊ทธ๋๋ก HTTP ์๋ต์ ์์ถํ๋ ์ฉ๋๋ก ์ฌ์ฉ๋๋ Express ๋ฏธ๋ค์จ์ด๋ค. HTML, JS, CSS๋ ํ ์คํธ ํ์ผ์ด๊ธฐ ๋๋ฌธ์ ๊ทธ๋๋ก ๋ณด๋ด๋ฉด ์ฉ๋์ด ์ปค์ ์ ์ก ์ต์ ํ๋ฅผ ์ํด ์ฌ์ฉํ๋ค.
const compression = (await import("compression")).default;
app.use(compression());๊ทธ๋ผ ์ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ฌ์ฉํ์ง ์์๊น? ๊ฐ๋ฐ ์๋ฒ๋ ๋น ๋ฅธ ์ฌ๋น๋, ๋๋ฒ๊น , ์์ค ํ์ธ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ ์์ถํ๋ ๊ณผ์ ์ ์คํ๋ ค ๋๋ฒ๊น ์ ๋ฐฉํดํ๊ฒ ๋์ด ๋ฐฐํฌ ํ๊ฒฝ์์๋ง ์ฌ์ฉํ๋ค.
(2-2) sirv ์ ์ ํ์ผ์ ์ ๋ฌํ๋ ์ด๊ฒฝ๋ ์๋ฒ ๋ฏธ๋ค์จ์ด๋ก, ./dist/vanilla์์ ํ์ผ์ ์ฐพ์์ ํ์ผ์ด ์์ผ๋ฉด ๊ทธ๋๋ก ์๋ต์ ๋ณด๋ธ๋ค.
app.use(base, sirv("./dist/vanilla", { extensions: [] }));์๋ฅผ ๋ค์ด ์๋์ ๊ฐ์ด ์์ฒญ์ด ์ค๋ฉด ์ง์ ํ ๊ฒฝ๋ก๋ฅผ ๊ธฐ์ค์ผ๋ก ํ์ผ์ ์ฐพ์์ ์๋ต์ ๋ณด๋ธ๋ค.
GET /assets/main.js โ dist/vanilla/assets/main.js
GET /style.css โ dist/vanilla/style.css
๋ฌด์์ ์ฝ์์ผ์ง ๊ถ๊ธํด์ ์ฐพ์๋ณด๋ ๋ฑํ ์ฝ์๋ ์๋๊ณ serve์ ๊ฐ์ ์๋ฏธ๋ก, ๋น๋๋ ์ ์ ํ์ผ ์๋ฒ๋ผ๊ณ ์๊ฐํด์ฃผ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
(3-1) ๋ฌธ์ ์ ๋ฐ๋จ ์์๋๋ค..ํ ์คํธ ์ง์ฅ
๋์ผํ ํ ์คํธ๋ฅผ ์ฌ๋ฌ ํฌํธ์์ ๋์์ ์คํํ๋ฉด ํน์ ํฌํธ์์๋ ๊ธฐ๋ํ "์ด 340๊ฐ" ๊ฐ์ ๋ฌธ์์ด ๋์ TypeError, fetch ์๋ฌ, ํน์ MSW ๊ด๋ จ ์๋ฌ ์คํ์ด HTML์ ๊ทธ๋๋ก ์์ฌ ๋ด๋ ค์ค๊ณ , ๊ฒฐ๊ตญ expect(...).toContain("์ด") ๊ฐ์ assertion์ด ์คํจํ๋ ํ์์ด ๋ฐ๋ณต์ ์ผ๋ก ๋ฐ์ํ๋ค..
ํนํ ํ ์คํธ๋ฅผ ํ ๋ฒ๋ง ์์ฐจ์ ์ผ๋ก ๋๋ฆด ๋๋ ์ ํต๊ณผํ๋๋ฐ, Playwright๊ฐ ๋ณ๋ ฌ๋ก ๋๋ฆด ๋, ์ฌ๋ฌ ํฌํธ ์กฐํฉ์ ํจ๊ป ๋๋ฆด ๋ ๋ถ์์ ํ๊ฒ ๊นจ์ง๋ ๊ฒ์ด ํน์ง์ด์๋ค.
await this.page.waitForFunction(() => {
const text = document.body.textContent;
return text?.includes("์ด") && text?.includes("๊ฐ");
});(3-2) ํ๊ฒฝ ๊ตฌ์กฐ ์ดํด ๏ปฟ๋ฌธ์ ๋ฅผ ์ ๋๋ก ๋ณด๋ ค๋ฉด ํ๊ฒฝ ๊ตฌ์ฑ์ ๋จผ์ ์ดํดํ ํ์๊ฐ ์์๋ค.
Vanilla:
CSR: http://localhost:5173/
SSR: http://localhost:5174/
SSG: http://localhost:4173/front_7th_chapter4-1/vanilla/, 4174, 4178 ๋ฑ
React:
CSR: http://localhost:5175/
SSR: http://localhost:5176/
SSG: http://localhost:4175/front_7th_chapter4-1/react/, 4176, 4179 ๋ฑ
E2E ์คํ์ ๊ฐ ํฌํธ๋ฅผ ๋ชจ๋ ํ๊ฒ์ผ๋ก ๊ฐ์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค๋ฅผ ๋ฐ๋ณต ์คํํ๋ฉฐ, ์ค์ ๋ฐฑ์๋ ์๋ฒ๋ ์๊ณ MSW๋ก API๋ฅผ ๋ชจํนํ๊ณ ์๋ค.
ํด๋ผ์ด์ธํธ:
browser.ts + mockServiceWorker.js
์๋ฒ:
nodeServer.js / nodeServer.ts + msw/node์ setupServer(...handlers)
(3-3) MSW ์๋ฒ ์ด๊ธฐํ/๋ชจํน ๋ฌธ์ ?? ์ฆ์์ ๋ณด๋ฉด ๊ณตํต์ ์ ๋ค์๊ณผ ๊ฐ์๋ค.
- ์คํจํ ๋ HTML ์์ ์์ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ ์๋ฌ ์คํ์ด ๋ค์ด ์์
- ํนํ SSR/SSG ์๋ํฌ์ธํธ(417x, 5174, 5176)์์ ๋ ์์ฃผ ๋ฐ์
- MSW๊ฐ ์ ๋๋ก ๋์ํ์ง ์๋ ์๊ฐ์๋ ๋ชจ๋ ์ํ/์นดํ ๊ณ ๋ฆฌ ์์ฒญ์ด ์คํจ => E2E์ ํ ์คํธ ๋งค์นญ ์คํจ
(3-4) Node ํ๊ฒฝ์์์ MSW ๋์ ๋ฐฉ์ ๊ฐ์ Node ํ๋ก์ธ์ค ์์์ ์ฌ๋ฌ ์๋ฒ๊ฐ ๋์๊ฐ๋๋ผ๋ MSW๋ ์ ์ญ์ ์ผ๋ก ๋คํธ์ํฌ ๊ณ์ธต(fetch/http)์ ํจ์นํ๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ ์๋ฒ์ ์์ฒญ์ ๋์ผํ MSW ์ํ๊ฐ ๊ณต์ ํ๊ฒ ๋๋ค.
ํ ์คํธ๋ฅผ ๋ณ๋ ฌ๋ก ์คํํ๋ฉด ๊ฐ ์๋ฒ๊ฐ ์๋ก ๋ค๋ฅธ ํ์ด๋ฐ์ MSW์ listen()์ ํธ์ถํ๊ฒ ๋๊ณ ์ผ๋ถ ์์ฒญ์ MSW๊ฐ ํ์ฑํ๋๊ธฐ ์ด์ ์ ๋ฐ์ํ์ฌ ์ค์ ๋คํธ์ํฌ ์์ฒญ์ผ๋ก ๋น ์ง ์ ์๋ค.
์ด ํ์ด๋ฐ/์ ์ญ ์ํ ๋ฌธ์ ๋๋ฌธ์, ํฌํธ/๋ชจ๋(CSR, SSR, SSG) ์กฐํฉ์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ ๋ค์ญ๋ ์ญํ๋ ๊ฒ์ด๋ค.
(3-5) onUnhandledRequest: "bypass" ์ถ๊ฐ
mswServer.listen({
onUnhandledRequest: "bypass",
});์ด ์ค์ ์ด ์๋ฏธํ๋ ๊ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
1. MSW๋ ํญ์ ์ผ๋๋ค
- SSR/SSG ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์๋ฒ๊ฐ ๋จ๋ ์์ ์ MSW๋ฅผ ๋ช ์์ ์ผ๋ก listen()
- ์ต์ํ โ์๋ฒ๊ฐ ์ค๋น๋ ํ ๋ค์ด์ค๋ SSR ์์ฒญโ์ ๋ํด์๋ ๋ชจํน ์ผ๊ด์ฑ์ด ์ฌ๋ผ๊ฐ
2. ๋ชจํนํ์ง ์์ ์์ฒญ์ ์ค ์๋ฒ๋ก ๊ทธ๋ฅ ํ๋ ค๋ณด๋ธ๋ค
- ๋ฑ๋ก๋์ง ์์ ์๋ํฌ์ธํธ, ์ ์ ๋ฆฌ์์ค, ์์ธ์ ์ธ ์์ฒญ๋ค์ด ์์ด๋
- ํ ์คํธ๋ฅผ ๊นจ๋จ๋ฆฌ๋ ์๋ฌ๋ฅผ ๋์ง์ง ์๋๋ค
- SSR ๊ฒฐ๊ณผ HTML์ ๊ฐ๋ฐ์ฉ ์๋ฌ ํ ์คํธ๊ฐ ์์ฌ ๋ค์ด์ค๋ ์ผ์ ๋ฐฉ์ง
๊ฒฐ๊ณผ์ ์ผ๋ก SSR/SSG ํ ์คํธ์์ API ์๋ต์ด ์์ ์ ์ผ๋ก ๋ชจํน๋๊ธฐ ์์ํ๊ณ ์์ธ์ ์ธ ์์ฒญ์ด ์๋ฌ๋ฅผ ์ ๋ฐํด ํ์ด์ง ์ ์ฒด๋ฅผ ๋ง๊ฐ๋จ๋ฆฌ๋ ์ผ์ด ์ค์ด๋ค๋ฉด์ createTests.ts๊ฐ ๊ธฐ๋ํ๋ "์ด 340๊ฐ" ๊ฐ์ ๋ฌธ๊ตฌ๋ฅผ ๋ชจ๋ ํฌํธ ์กฐํฉ์์ ์ผ๊ด๋๊ฒ ํ์ธํ ์ ์๊ฒ ๋์๋ค.
๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ง๋ค์ด๋ด์ง๋ง, ๊ตฌํ ๊ณผ์ ์์ ๊ฐ๋ฐ์๊ฐ ์ง์ ์ฑ ์์ ธ์ผ ํ๋ ๋ฒ์๋ ์์ ํ ๋ฌ๋๋ค.
(4-1) SSR ๊ตฌํ ๋ฐฉ์์ ์ฐจ์ด
1๏ธโฃ Vanilla
Vanilla ํ๊ฒฝ์์๋ SSR ๊ณผ์ ์ด ๋งค์ฐ ์ง๊ด์ ์ด๋ฉด์๋, ๊ทธ๋งํผ ๋ชจ๋ ์ฑ ์์ด ๋ช ํํ ๋๋ฌ๋ฌ๋ค.
const result = await router.target(params);
return {
html: result.html,
__INITIAL_DATA__: result.data ?? {},
};- ํ์ด์ง ํจ์๊ฐ HTML ๋ฌธ์์ด์ ์ง์ ๋ฐํ
- ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด์ ์ด์ฉํด ๋ฌธ์์ด ๊ธฐ๋ฐ HTML ์์ฑ
- ์ด๊ธฐ ๋ฐ์ดํฐ๋ hydrateStoreFromSSR()๋ฅผ ํตํด ์คํ ์ด์ ์ง์ dispatch => HTML ์์ฑ, ๋ฐ์ดํฐ ์ฃผ์ , ์ํ ์ ๋ฌ์ ๋ชจ๋ ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ด๋ฆฌ
2๏ธโฃ React
React SSR์์๋ renderToString์ ํตํด ์ปดํฌ๋ํธ ํธ๋ฆฌ ์ ์ฒด๊ฐ ๊ทธ๋๋ก ์๋ฒ์์ ์คํ๋๋ค.
htmlString = renderToString(
<RouterProvider router={router}>
<ProductProvider productStore={createProductStore(result || {})}>
<App />
</ProductProvider>
</RouterProvider>
);- renderToString์ผ๋ก ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ HTML ๋ฌธ์์ด๋ก ๋ณํ
- JSX ๊ธฐ๋ฐ ์ปดํฌ๋ํธ๊ฐ ์๋ฒ/ํด๋ผ์ด์ธํธ์์ ๋์ผํ๊ฒ ์ฌ์ฉ
- ์ด๊ธฐ ๋ฐ์ดํฐ๋ createProductStore(initData)๋ฅผ ํตํด Context์ ์ฃผ์ => SSR ํ๋ฆ ์์ฒด๊ฐ ์ปดํฌ๋ํธ ๊ตฌ์กฐ ์์ผ๋ก ์์ฐ์ค๋ฝ๊ฒ ๋ น์ ์์
(4-2) Hydration ๋ฐฉ์์ ์ฐจ์ด
1๏ธโฃ Vanilla
Vanilla์์๋ hydration ์ญ์ ์ ์ ์ผ๋ก ์๋์ด๋ค.
productStore.dispatch({
type: PRODUCT_ACTIONS.SETUP,
payload: {
products: initialData.products ?? [],
categories: initialData.categories ?? {},
},
});- ์๋ฒ HTML์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ฑฐ๋, ํ์ ์ ๋ค์ ๋ ๋๋ง
- ๊ฐ ์คํ ์ด์ ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ์ง์ dispatch
- ์ํ ๋ณต์๊ณผ DOM ์ฌ์ฉ ๋ฐฉ์ ๋ชจ๋ ๊ฐ๋ฐ์๊ฐ ์ ์ด => hydration์ โ์๋ ๊ณผ์ โ์ด ์๋๋ผ ๋ช ์์ ์ผ๋ก ์ค๊ณํด์ผ ํ๋ ๋จ๊ณ๋ผ๋ ๊ฑธ ์ฒด๊ฐํ๋ค.
2๏ธโฃ React
React์์๋ hydration ๊ณผ์ ์ด ์๋์ ์ผ๋ก ๋จ์ํด์ง๋ค.
const initData = window.__INITIAL_DATA__;
<ProductProvider productStore={createProductStore(initData || {})}>
<App />
</ProductProvider>- hydrateRoot๋ฅผ ํตํด ์๋ฒ DOM์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ง ์ฐ๊ฒฐ
- Context ์ด๊ธฐํ๋ก ์ํ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋ณต์
- ํด๋ผ์ด์ธํธ์์๋ ๋ฐ์ดํฐ ์ ๋ฆฌ๋ง ์ํ => React๊ฐ hydration์ ๋ณต์ก์ฑ์ ํ๋ ์์ํฌ ์ฐจ์์์ ํก์ํ๊ณ ์๋ค๋ ๊ฑธ ์ค๊ฐํ๋ค.
(4-3) ๋ผ์ฐํ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ฐจ์ด
1๏ธโฃ Vanilla
// Vanilla ๋ผ์ฐํ
import { router } from "./router";
const PageComponent = router.target;
rootElement.innerHTML = PageComponent();- ์ ์ญ router ์ธ์คํด์ค๋ฅผ ์ง์ import
- router.target, router.params์ ์ง์ ์ ๊ทผ
- ๋ผ์ฐํฐ ๋ณ๊ฒฝ ์ render ํจ์๋ฅผ ์๋ ํธ์ถ => ํ๋ฆ์ ๋ช ํํ์ง๋ง, ๊ด๋ฆฌ ํฌ์ธํธ๊ฐ ๋ง์
2๏ธโฃ React
// React ๋ผ์ฐํ
const router = useRouterContext();
const PageComponent = useCurrentPage();
return PageComponent ? <PageComponent /> : null;- RouterProvider๋ก ๋ผ์ฐํฐ๋ฅผ Context์ ์ฃผ์
- useRouterContext, useCurrentPage ๊ฐ์ Hook ์ฌ์ฉ
- ๋ผ์ฐํฐ ๋ณ๊ฒฝ ์ ์ปดํฌ๋ํธ๊ฐ ์๋ ๋ฆฌ๋ ๋๋ง => ๋ผ์ฐํ ๋ํ ์ ์ธ์ ์ธ ๊ตฌ์กฐ๋ก ์ถ์ํ๋จ
[๊นจ๋ฌ์ ์ ]
๊ฐ์ SSR/SSG๋ผ๋ Vanilla์์๋ SSR์ด ๋ฌด์์ธ์ง๋ฅผ ๋ชธ์ผ๋ก ์ดํดํ๊ฒ ๋์๊ณ , React์์๋ ์ ์ด๋ฐ ์ถ์ํ๊ฐ ํ์ํ์ง๋ฅผ ์ดํดํ๊ฒ ๋์๋ค.
์ด ๋น๊ต๋ฅผ ํตํด hydration์ด ๋จ์ํ ํ์ฒ๋ฆฌ๊ฐ ์๋๋ผ, SSR ๊ตฌ์กฐ์์ ๊ฐ์ฅ ์ค์ํ ์ฐ๊ฒฐ ์ง์ ์ด๋ผ๋ ๊ฑธ ๋ช ํํ ๊นจ๋ฌ์๋ค.
// packages/vanilla/server.js:28-30
// packages/react/server.ts:31-33
mswServer.listen({
onUnhandledRequest: "bypass", // ๋ชจํนํ์ง ์์ ์์ฒญ์ ๊ทธ๋๋ก ํต๊ณผ
});-
ํ์ฌ ํด๊ฒฐ์ฑ ์ ํ๊ณ
onUnhandledRequest: "bypass"๋ก ํด๊ฒฐํ์ง๋ง, ์ฌ๋ฌ ์๋ฒ๊ฐ ๋์์ MSW๋ฅผ ์ฌ์ฉํ ๋ ์ ์ญ ์ํ ๊ณต์ ๋ฌธ์ ๊ฐ ์์ ํ ํด๊ฒฐ๋์๋์ง ํ์ธ์ด ํ์ํฉ๋๋ค.- ๊ฐ ์๋ฒ๋ง๋ค ๋ ๋ฆฝ์ ์ธ MSW ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ๊ฒ์ด ๋ ๋์ ๋ฐฉ๋ฒ์ผ๊น์?
-
ํ๋ก๋์ ํ๊ฒฝ ๊ณ ๋ ค
- ํ์ฌ ํ๋ก๋์
ํ๊ฒฝ์์๋ MSW๊ฐ ์คํ๋๋๋ฐ (
packages/vanilla/server.js:28,packages/react/server.ts:31), ํ๋ก๋์ ์์๋ MSW๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ํ๊ฒฝ ๋ณ์๋ก ๋ถ๊ธฐํ๋ ๊ฒ์ด ์ข์๊น์?
// ์ ์: ํ๋ก๋์ ์์๋ MSW ๋นํ์ฑํ if (process.env.NODE_ENV !== "production") { mswServer.listen({ onUnhandledRequest: "bypass" }); }
- ํ์ฌ ํ๋ก๋์
ํ๊ฒฝ์์๋ MSW๊ฐ ์คํ๋๋๋ฐ (
-
ํ ์คํธ ํ๊ฒฝ์์์ ์์ ์ฑ
- E2E ํ ์คํธ์์ ์ฌ๋ฌ ํฌํธ๋ฅผ ๋์์ ํ ์คํธํ ๋, MSW์ ์ ์ญ ์ํ๊ฐ ์ฌ์ ํ ๊ฐ์ญํ ๊ฐ๋ฅ์ฑ์ด ์๋์ง ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.
์์ธํ ๊ตฌํ ๊ณผ์ ๊ณผ ํ๊ณ ๋ ์๋ ๋ธ๋ก๊ทธ์ ์ ๋ฆฌํ์ต๋๋ค! ๐
9์ฃผ์ฐจ_์ฑ๋ฅ์ต์ ํ: SSR(Server Side Rendering), SSG(Static Site Generation), Infra