June 10, 2025
์๋ฌธ : One Roundtrip Per Navigation
๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ๋ ค๋ฉด ๋ช ๋ฒ์ ์์ฒญ์ด ํ์ํ ๊น์?
๊ฐ์ฅ ๋จ์ํ ๊ฒฝ์ฐ๋ ๋จ ํ ๋ฒ์ ์์ฒญ๋ง์ผ๋ก ํด๊ฒฐ๋ฉ๋๋ค. ์ฌ์ฉ์๊ฐ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด ๋ธ๋ผ์ฐ์ ๋ ์ URL์ ๋ํ HTML ์ฝํ ์ธ ๋ฅผ ์์ฒญํ๊ณ , ๊ทธ ์ฝํ ์ธ ๋ฅผ ํ์ํฉ๋๋ค.
์ค์ ๋ก๋, ํ์ด์ง๊ฐ ์ด๋ฏธ์ง๋ ํด๋ผ์ด์ธํธ ์ธก ์๋ฐ์คํฌ๋ฆฝํธ, ์ถ๊ฐ ์คํ์ผ ๋ฑ์ ๋ก๋ํ๊ณ ์ ํ ์ ์์ต๋๋ค. ๊ทธ๋์ ์ฌ๋ฌ ๊ฐ์ ์์ฒญ์ด ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค. ์ผ๋ถ ์์ฒญ์ ๋ ๋๋ง์ ๋ง๋ ์์ฒญ์ด ๋ ์ ์์ผ๋ฉฐ(๋ธ๋ผ์ฐ์ ๋ ์ด ์์ฒญ๋ค์ด ์๋ฃ๋ ๋๊น์ง ํ์ด์ง ํ์๋ฅผ ๋ฏธ๋ฃน๋๋ค), ๋๋จธ์ง๋ ๋ถ๊ฐ์ ์ธ ์์ฒญ๋ค์ ๋๋ค. ์ด๋ฌํ ์์ฒญ๋ค์ ์ ์ฒด์ ์ธ ์ธํฐ๋ํฐ๋ธํ ๋์์ ์ค์ํ ์ ์์ง๋ง, ๋ธ๋ผ์ฐ์ ๋ ์ด๋ค์ด ๋ก๋๋๋ ๋์์๋ ์ด๋ฏธ ํ์ด์ง๋ฅผ ํ์ํ ์ ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด, ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๋๋ ์ด๋จ๊น์?
๋ค์ ํ์ด์ง์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ๋ช ๋ฒ์ API ์์ฒญ์ด ํ์ํ ๊น์?
์น ๊ฐ๋ฐ์ด ํด๋ผ์ด์ธํธ๋ก ์ฎ๊ฒจ์ค๊ธฐ ์ ์๋, ์ด๋ฐ ์ง๋ฌธ ์์ฒด๊ฐ ์๋ฏธ๊ฐ ์์์ต๋๋ค. โAPI๋ฅผ ํธ์ถํ๋คโ๋ ๊ฐ๋ ์ด ์์๊ธฐ ๋๋ฌธ์ด์ฃ . ์๋ฒ๋ ๊ทธ๋ฅ HTML์ ๋ฐํํ๋ ์๋ฒ์ผ ๋ฟ, API ์๋ฒ๋ก ์๊ฐ๋์ง ์์์ต๋๋ค.
์ ํต์ ์ธ โHTML ์ฑโ ํน์ ์น์ฌ์ดํธ์์๋, ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ํญ์ ํ ๋ฒ์ ์๋ณต ์์ฒญ๋ง ํ์ํ์ต๋๋ค. ์ฌ์ฉ์๊ฐ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด ์๋ฒ๋ HTML์ ๋ฐํํ๊ณ , ๋ค์ ํ์ด์ง๋ฅผ ํ์ํ๋ ๋ฐ ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ ๊ทธ HTML ์์ ์ด๋ฏธ ํฌํจ๋์ด ์์ต๋๋ค. HTML ์์ฒด๊ฐ ๋ฐ์ดํฐ์ธ ์ ์ด์ฃ . ๋ณ๋์ ์ฒ๋ฆฌ ์์ด ๋ฐ๋ก ํ์ํ ์ ์์ต๋๋ค.
<article>
<h1>ํ ๋ฒ์ ์๋ณต ์์ฒญ์ผ๋ก ์ด๋ฃจ์ด์ง๋ ๋ด๋น๊ฒ์ด์
</h1>
<p>๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ๋ ค๋ฉด ๋ช ๋ฒ์ ์์ฒญ์ด ํ์ํ ๊น์?</p>
<ul class="comments">
<li>HTML์ ์ฌ์ฐฝ์กฐ</li>
<li>PHP์ ์ฌ์ฐฝ์กฐ</li>
<li>GraphQL์ ์ฌ์ฐฝ์กฐ</li>
<li>Remix์ ์ฌ์ฐฝ์กฐ</li>
<li>Astro์ ์ฌ์ฐฝ์กฐ</li>
</ul>
</article>(๋ฌผ๋ก ๊ธฐ์ ์ ์ผ๋ก๋ ์ด๋ฏธ์ง, ์คํฌ๋ฆฝํธ, ์คํ์ผ๊ณผ ๊ฐ์ ์ ์ ์ด๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๋ฉฐ ์บ์ ๊ฐ๋ฅํ ์์๋ค์ด โ์ธ๋ถํโ๋์ง๋ง, ํ์์ ๋ฐ๋ผ ์ธ์ ๋ ์ง ์ธ๋ผ์ธ ์ฒ๋ฆฌํ ์๋ ์์ต๋๋ค.)
์ ํ๋ฆฌ์ผ์ด์ ๋ก์ง์ด ์ ์ ํด๋ผ์ด์ธํธ๋ก ์ด๋ํ๋ฉด์ ์ํฉ์ด ๋ฌ๋ผ์ก์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ๊ฐ์ ธ์ค๊ณ ์ ํ๋ ๋ฐ์ดํฐ๋ ์ผ๋ฐ์ ์ผ๋ก ํ์ํ๊ณ ์ ํ๋ UI์ ๋ฐ๋ผ ๊ฒฐ์ ๋ฉ๋๋ค. ๊ฒ์๊ธ์ ๋ณด์ฌ์ฃผ๊ณ ์ถ๋ค๋ฉด ๊ฒ์๊ธ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ๊ณ , ๋๊ธ์ ๋ณด์ฌ์ฃผ๊ณ ์ถ๋ค๋ฉด ๋๊ธ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํฉ๋๋ค. ๊ทธ๋ผ ์ผ๋ง๋ ์ฌ๋ฌ๋ฒ ๊ฐ์ ธ์์ผ ํ ๊น์?
JSON API์ ํจ๊ป ์ฌ์ฉ๋๋ REST๋ผ๋ ๊ธฐ์ ์ ํตํด ๊ฐ๋ ์ ์ธ โ๋ฆฌ์์คโ๋ง๋ค ํ๋์ ์๋ํฌ์ธํธ๋ฅผ ๋ ธ์ถํ๋ ๋ฐฉ๋ฒ์ ์ ์ํฉ๋๋ค. โ๋ฆฌ์์คโ๊ฐ ์ ํํ ๋ฌด์์ธ์ง๋ ์๋ฌด๋ ๋ชจ๋ฅด์ง๋ง, ๋ณดํต์ ๋ฐฑ์๋ ํ์ด ์ด ๊ฐ๋ ์ ์ ์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด ๊ฒ์๊ธ โ๋ฆฌ์์คโ์ ๋๊ธ โ๋ฆฌ์์คโ๊ฐ ์์ ์ ์์ผ๋ฏ๋ก, ์ด๋ก ์ธํด ๊ฒ์๊ธ ํ์ด์ง(๊ฒ์๊ธ๊ณผ ๋๊ธ์ ํฌํจ)์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ ๋ฒ ๊ฐ์ ธ์์ ๋ถ๋ฌ์ฌ ์ ์๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ ๋ค๋ฉด ์ด ๋ ๋ฒ์ ๊ฐ์ ธ์ค๊ธฐ๋ ์ด๋์ ๋ฐ์ํ ๊น์?
์๋ฒ ์ค์ฌ์ HTML ์ฑ(์น์ฌ์ดํธ)์์๋ ํ๋์ ์์ฒญ ์ค์ ๋ ๊ฐ์ REST API๋ฅผ ํธ์ถํ๊ณ , ์ฌ์ ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋์ ์๋ต์ผ๋ก ๋ฐํํ ์ ์์ต๋๋ค. ์ด๋ REST API ์์ฒญ์ด ์๋ฒ์์ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. REST API๋ ์ฃผ๋ก ๋ฐ์ดํฐ ๊ณ์ธต์ ๋ช ์์ ์ธ ๊ฒฝ๊ณ๋ฅผ ์ํ ์๋จ์ผ๋ก ์ฌ์ฉ๋์์ง๋ง, ๊ผญ ํ์ํ์ง๋ ์์์ต๋๋ค(๋ง์ ๊ฒฝ์ฐ์๋ Rails๋ Django์ฒ๋ผ ์ธํ๋ก์ธ์ค ๋ฐ์ดํฐ ๊ณ์ธต์ ์ฌ์ฉํ๋ ๋ฐ ๋ง์กฑํ์ต๋๋ค). REST ์ฌ๋ถ์ ์๊ด์์ด, ๋ฐ์ดํฐ(HTML)๋ ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )์ ์จ์ ํ ์ํ๋ก ๋์ฐฉํฉ๋๋ค.
์ํธ์์ฉ์ ์ํด UI ๋ก์ง์ด ํด๋ผ์ด์ธํธ๋ก ์ฎ๊ฒจ๊ฐ๊ธฐ ์์ํ๋ฉด์, ๊ธฐ์กด REST API๋ฅผ ๊ทธ๋๋ก ๋๊ณ ํด๋ผ์ด์ธํธ์์ ์ด๋ฅผ fetchํ๋ ๊ฒ์ด ์์ฐ์ค๋ฝ๊ฒ ๋๊ปด์ก์ต๋๋ค. JSON API์ ์ ์ฐ์ฑ์ ๋ฐ๋ก ์ด๋ฐ ์ํฉ์ ์ข๋ค๊ณ ์๊ฐ๋์์ฃ . ๋ชจ๋ ๊ฒ์ด JSON API๊ฐ ๋์์ต๋๋ค.
const [post, comments] = await Promise.all([
fetch(`/api/posts/${postId}`).then(res => res.json()),
fetch(`/api/posts/${postId}/comments`).then(res => res.json()),
]);๊ทธ๋ฌ๋ ๊ทธ ๊ฒฐ๊ณผ, ๋คํธ์ํฌ ํญ์๋ ์ด์ ๋ ๊ฐ์ ๊ฐ์ ธ์ค๊ธฐ๊ฐ ๋ณด์ ๋๋ค. ํ๋๋ ๊ฒ์๊ธ์ ๋ํ ๊ฐ์ ธ์ค๊ธฐ์ด๊ณ , ๋ค๋ฅธ ํ๋๋ ๊ฒ์๊ธ์ ๋๊ธ์ ๋ํ ๊ฐ์ ธ์ค๊ธฐ์ ๋๋ค. ํ๋์ ํ์ด์ง, ํ๋์ ๋งํฌ ํด๋ฆญ์ด ์ข ์ข ๋ ๊ฐ ์ด์์ REST โ๋ฆฌ์์คโ๋ก๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ํ์๋ก ํฉ๋๋ค. ๊ฐ์ฅ ์ข์ ๊ฒฝ์ฐ์๋ ๋์ธ ๊ฐ์ ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํ๊ณ ๋๋์ง๋ง, ์ต์ ์ ๊ฒฝ์ฐ์๋ N๊ฐ์ ํญ๋ชฉ๋ง๋ค N๊ฐ์ ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํ๊ฑฐ๋, ํด๋ผ์ด์ธํธ/์๋ฒ ๊ฐ์ ์ฐ์์ ์ธ ์ํฐํด ์์ฒญ์ ํด์ผ ํ ์๋ ์์ต๋๋ค(์ผ๋ถ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์ฒ๋ฆฌํ ํ, ๊ทธ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ค์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญ).
๋นํจ์จ์ด ์์ํ ๋ฐ์ํ๊ณ ์์ต๋๋ค. ์๋ฒ์ ์์ ๋๋ ์ฌ๋ฌ ๊ฐ์ REST ์์ฒญ์ ๋ง๋๋ ๊ฒ์ด ์ ๋ ดํ์ต๋๋ค. ๋ฐฐํฌ๋ ์ฝ๋๋ฅผ ํต์ ํ ์ ์์๊ธฐ ๋๋ฌธ์ ๋๋ค. REST ์๋ํฌ์ธํธ๊ฐ ๋ฉ๋ฆฌ ์์ผ๋ฉด ์๋ฒ๋ฅผ ๊ทธ ๊ฐ๊น์ด๋ก ์ฎ๊ธฐ๊ฑฐ๋, ํด๋น ์ฝ๋๋ฅผ ์ธํ๋ก์ธ์ค ์ฝ๋๋ก ์ด์ ํ ์๋ ์์์ต๋๋ค. ๋ณต์ ๋ ์๋ฒ ์ธก ์บ์ฑ์ ์ฌ์ฉํ ์๋ ์์์ต๋๋ค. ๋ญ๊ฐ ๋นํจ์จ์ ์ด๋๋ผ๋, ์๋ฒ ์ชฝ์๋ ์ด๋ฅผ ๊ฐ์ ํ ์ ์๋ ๋ค์ํ ์๋จ์ด ์์ต๋๋ค. ์๋ฌด๊ฒ๋ ์๋ฒ ์ธก ๊ฐ์ ์ ๋ง์ ์ ์์ต๋๋ค.
ํ์ง๋ง ์๋ฒ๋ฅผ ๋ธ๋๋ฐ์ค๋ก ๋ณธ๋ค๋ฉด, ์๋ฒ๊ฐ ์ ๊ณตํ๋ API๋ฅผ ๊ฐ์ ํ ์ฌ์ง๊ฐ ์์ต๋๋ค. ์๋ฒ๊ฐ ์์ฒญ์ ๋ณ๋ ฌ๋ก ์คํํ๋ ๋ฐ ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ์ง ์์ผ๋ฉด, ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด์ ์ต์ ํํ ์ ์์ต๋๋ค. ์๋ฒ๊ฐ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐฐ์น๋ก ๋ฐํํ์ง ์์ผ๋ฉด ๋ณ๋ ฌ ์์ฒญ ์๋ฅผ ์ค์ผ ์ ์์ต๋๋ค.
์ด๋ ์๊ฐ, ํ๊ณ์ ๋๋ฌํ๊ฒ ๋ฉ๋๋ค.
์์์ ์ค๋ช ํ ๋ฌธ์ ๋ ํจ์จ์ฑ๊ณผ ์บก์ํ ๊ฐ์ ๊ธด์ฅ ์ํ๊ฐ ์์๋ค๋ฉด ๊ทธ๋ฆฌ ๋์์ง ์์์ ์๋ ์์ต๋๋ค. ๊ฐ๋ฐ์๋ก์ ์ฐ๋ฆฌ๋ ๋ฐ์ดํฐ ๋ก๋ฉ ๋ก์ง์ ๊ทธ ๋ฐ์ดํฐ๊ฐ ์ฌ์ฉ๋๋ ์์น ๊ฐ๊น์ด์ ๋ฐฐ์นํ๊ณ ์ถ์ ์๊ตฌ๋ฅผ ๋๋๋๋ค. ๋๊ตฐ๊ฐ๋ ์ด๊ฒ์ด โ์คํ๊ฒํฐ ์ฝ๋โ๋ก ์ด์ด์ง ์ ์๋ค๊ณ ๋งํ ์๋ ์์ง๋ง, ๊ผญ ๊ทธ๋ฐ ๊ฒ์ ์๋๋๋ค! ๊ทธ ์์ด๋์ด ์์ฒด๋ ํฉ๋ฆฌ์ ์ ๋๋ค. ๊ธฐ์ตํ์ธ์. UI๊ฐ ๋ฐ์ดํฐ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. ํ์ํ ๋ฐ์ดํฐ๋ ์ฐ๋ฆฌ๊ฐ ๋ฌด์์ ํ์ํ๊ณ ์ถ์์ง์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋๋ค. ๋ฐ๋ผ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ๋ก์ง๊ณผ UI ๋ก์ง์ ๋ณธ์ง์ ์ผ๋ก ๋ฐ์ ํ๊ฒ ์ฐ๊ฒฐ๋์ด ์์ผ๋ฉฐ, ํ๋๊ฐ ๋ฐ๋๋ฉด ๋ค๋ฅธ ํ๋๋ ๋ฐ๋์ ๊ทธ์ ๋ง์ถฐ ์กฐ์ ๋์ด์ผ ํฉ๋๋ค. ๋ฐ์ดํฐ๋ฅผ โ์ ๊ฒ ๊ฐ์ ธ์์(underfetching)โ ๊ธฐ๋ฅ์ ๊นจ๋จ๋ฆฌ๊ฑฐ๋, โ๋๋ฌด ๋ง์ด ๊ฐ์ ธ์(overfetching)โ ์ฑ๋ฅ์ ์ ํดํ๊ณ ์ถ์ง๋ ์์ ๊ฒ์ ๋๋ค. ๊ทธ๋ ๋ค๋ฉด UI ๋ก์ง๊ณผ ๋ฐ์ดํฐ ๋ก๋ฉ์ ์ด๋ป๊ฒ ๋๊ธฐํํ ์ ์์๊น์?
๊ฐ์ฅ ์ง์ ์ ์ธ ๋ฐฉ๋ฒ์ ๋ฐ์ดํฐ ๋ก๋ฉ ๋ก์ง์ UI ์ปดํฌ๋ํธ ์์ ์ง์ ์์ฑํ๋ ๊ฒ์
๋๋ค. ์ด๋ โBackbone.View์์ $.ajax๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์โ ๋๋ โuseEffect์์ fetch๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์โ์ผ๋ก, ํด๋ผ์ด์ธํธ ์ธก UI๊ฐ ๋ถ์ํ๋ฉด์ ํญ๋ฐ์ ์ผ๋ก ์ธ๊ธฐ๋ฅผ ๋์์ต๋๋ค. ์ง๊ธ๋ ์ฌ์ ํ ๋ง์ด ์ฌ์ฉ๋๊ณ ์์ต๋๋ค. ์ด ์ ๊ทผ์ ์ฅ์ ์ ์ฝ๋์ ๊ทผ์ ์ฑ(colocation)์ ์์ต๋๋ค. ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ๋ก๋ฉํ ์ง์ ๋ํ ์ฝ๋๊ฐ, ๊ทธ ๋ฐ์ดํฐ๋ฅผ ์๋นํ๋ ์ฝ๋ ๋ฐ๋ก ์์ ์์นํ๊ฒ ๋ฉ๋๋ค. ์๋ก ๋ค๋ฅธ ๊ฐ๋ฐ์๊ฐ ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ ์์ค๋ฅผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ ๋ค, ์ด๋ค์ ํ๋๋ก ์กฐํฉํ ์ ์์ต๋๋ค.
function PostContent({ postId }) {
const [post, setPost] = useState();
useEffect(() => {
fetch(`/api/posts/${postId}`)
.then(res => res.json())
.then(setPost);
}, []);
if (!post) {
return null;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<Comments postId={postId} />
</article>
);
}
function Comments({ postId }) {
const [comments, setComments] = useState([]);
useEffect(() => {
fetch(`/api/posts/${postId}/comments`)
.then(res => res.json())
.then(setComments);
}, []);
return (
<ul className="comments">
{comments.map(c => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}ํ์ง๋ง ์ด ์ ๊ทผ์ ์์ ์ค๋ช ํ ๋ฌธ์ ๋ฅผ ํจ์ฌ ๋ ์ฌ๊ฐํ๊ฒ ๋ง๋ญ๋๋ค. ๋จ์ผ ํ์ด์ง๋ฅผ ๋ ๋๋งํ๋ ๋ฐ ์ฌ๋ฌ ๊ฐ์ ์์ฒญ์ด ํ์ํ ๋ฟ๋ง ์๋๋ผ, ์ด ์์ฒญ๋ค์ด ์ฝ๋๋ฒ ์ด์ค ์ ๋ฐ์ ๋ถ์ฐ๋์ด ์๋ค๋ ์ ์์ ๊ทธ๋ ์ต๋๋ค. ๊ทธ ๋นํจ์จ์ ์ด๋ป๊ฒ ์ถ์ ํ ์ ์์๊น์?
๋๊ตฐ๊ฐ๊ฐ ์ปดํฌ๋ํธ๋ฅผ ์์ ํ๋ฉด์ ์๋ก์ด ๋ฐ์ดํฐ ๋ก๋ฉ์ ์ถ๊ฐํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ ์ด ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ ์์ญ ๊ฐ์ ํ๋ฉด์์ ์๋ก์ด ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด ์์ฒญ์ด ์๊ฒจ๋ ์ ์์ต๋๋ค. ๋ง์ฝ ์ฐ๋ฆฌ์ ์ปดํฌ๋ํธ๊ฐ Astro ์ปดํฌ๋ํธ์ฒ๋ผ ์๋ฒ์์๋ง ์คํ๋์๋ค๋ฉด, ๋ฐ์ดํฐ ๋ก๋ฉ์ผ๋ก ์ธํ ์ง์ฐ์ ์์ ์๊ฑฐ๋ ์ต์ ์ ๊ฒฝ์ฐ์๋ ์์ธก ๊ฐ๋ฅํ์ ๊ฒ์ ๋๋ค. ํ์ง๋ง ํด๋ผ์ด์ธํธ์์ ๋ฐ์ดํฐ ๋ก๋ฉ ๋ก์ง์ด ์ฌ๋ฌ ์ปดํฌ๋ํธ์ ํผ์ ธ ์๋ค๋ฉด, ์ด๋ฐ ๋นํจ์จ์ ๊ฑท์ก์ ์ ์์ด ํ์ฐ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ฅผ ๊ณ ์น ๋งํ ๋ง๋ ํ ์๋จ์ด ์์ต๋๋ค. ์ฌ์ฉ์๋ฅผ ์๋ฒ์ ๋ ๊ฐ๊น๊ฒ ์ฎ๊ฒจ๋์ ์๋ ์๊ณ , ๋ด์ฌ๋ ์ํฐํด ์์ฒญ์ ํด๋ผ์ด์ธํธ์์ ์๋ฌด๋ฆฌ ํ๋ฆฌํ์นญ์ ํ๋๋ผ๋ ํด๊ฒฐ๋์ง ์์ต๋๋ค.
๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ ์ฝ๋์ ๊ตฌ์กฐ๋ฅผ ์กฐ๊ธ ๋ ์ถ๊ฐํ๋ฉด ๋์์ด ๋ ์ ์๋์ง ์ดํด๋ด ์๋ค.
React Query์ useQuery์ ๊ฐ์ด ๋ฐ์ดํฐ ์์ฒญ์ ๊ตฌ์กฐ๋ฅผ ๋ถ์ฌํ๋ ค๋ ์๋ฃจ์
์ ์ ๋ฌธ์ ์ ๋ํ ๊ถ๊ทน์ ์ธ ํด๊ฒฐ์ฑ
์ด ์๋๋๋ค. ์ด๋ค์ useEffect์์ fetch๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ณด๋ค๋ ํจ์ฌ ๋ ์์น์ ์ธ ๋ฐฉ์์ด๋ฉฐ, ์บ์ฑ์ด ๋์์ด ๋๊ธด ํ์ง๋ง, ์ฌ์ ํ โN๊ฐ์ ํญ๋ชฉ์ ๋ํด N๊ฐ์ ์ฟผ๋ฆฌโ์ โํด๋ผ์ด์ธํธ/์๋ฒ ๊ฐ์ ์ฟผ๋ฆฌ ์ํฐํดโ ๋ฌธ์ ์์ ์์ ๋กญ์ง ์์ต๋๋ค.
function usePostQuery(postId) {
return useQuery(['post', postId], () =>
fetch(`/api/posts/${postId}`).then(res => res.json())
);
}
function usePostCommentsQuery(postId) {
return useQuery(['post-comments', postId], () =>
fetch(`/api/posts/${postId}/comments`).then(res => res.json())
);
}
function PostContent({ postId }) {
const { data: post } = usePostQuery(postId);
if (!post) {
return null;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<Comments postId={postId} />
</article>
);
}
function Comments({ postId }) {
const { data: comments } = usePostCommentsQuery(postId);
return (
<ul className="comments">
{comments.map(c => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}์ฌ์ค, ํด๋ผ์ด์ธํธ ์ธก ์บ์ฑ์ด ๋ง๋ฅ ํด๊ฒฐ์ฑ ์ ์๋๋๋ค. ํด๋ผ์ด์ธํธ ์ฑ์์ โ๋ค๋ก ๊ฐ๊ธฐโ ๋ฒํผ์ด ์ฆ์ ๋์ํ๋๋ก ํ๋ ค๋ฉด ์บ์ฑ์ด ๋ฐ๋์ ํ์ํ๊ณ , ํญ ์ ํ์ฒ๋ผ ํน์ ๋ด๋น๊ฒ์ด์ ์์ ์บ์ ์ฌ์ฌ์ฉ์ ๋์์ด ๋ฉ๋๋ค. ํ์ง๋ง ๋ง์ ๋ด๋น๊ฒ์ด์ , ํนํ ๋งํฌ ํด๋ฆญ์์๋ ์ฌ์ฉ์๊ฐ ์ ์ ํ ์ฝํ ์ธ ๋ฅผ ๊ธฐ๋ํฉ๋๋ค. ๋ฐ๋ก ์ด๊ฒ์ด ๋ธ๋ผ์ฐ์ ๊ฐ HTML ์ฑ์์ ํ์ด์ง๋ฅผ ๋ก๋ฉํ ๋ ๊ธฐ๋ค๋ฆฌ๋ ์ด์ ์ ๋๋ค! ์ฌ์ฉ์๊ฐ ํ์ด์ง ์ ์ฒด๊ฐ ๋์ฒด๋๊ธฐ๋ฅผ ์ํ์ง๋ ์๋๋ผ๋(ํนํ ์ฑ์ด ๋ด๋น๊ฒ์ด์ ์ ธ(shell)์ ์ฌ์ฉํ ๊ฒฝ์ฐ), ์ฝํ ์ธ ์์ญ์ ๋งํฌ ํด๋ฆญ ํ์ ์ ์ ํ๊ธธ ๊ธฐ๋ํฉ๋๋ค. (๋ฌผ๋ก , ๋ง์ฐ์ค ์ค๋ฒ ์ ํ๋ฆฌํ์นญ์ ํตํด ์ ์ ํ๋ฉด์๋ ์ฆ๊ฐ์ ์ธ ๋ด๋น๊ฒ์ด์ ์ ์ ๊ณตํ๋ ๊ฑด ํจ์ฌ ๋ ์ข์ต๋๋ค.)
์ง๊ด๊ณผ ๋ฌ๋ฆฌ, ๋ ๋น ๋ฅด๋ค๊ณ ํด์ ํญ์ ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ ๊ฒ์ ์๋๋๋ค. ์บ์๋ ์ค๋๋ ์ฝํ ์ธ ๊ฐ ์ ๊น ๋ํ๋ฌ๋ค๊ฐ ๋ฐ๋ก ๊ต์ฒด๋๋ ๋ฐฉ์(์: stale-while-revalidate)์ ์คํ๋ ค ์ฌ์ฉ์์ ์๋๋ฅผ ์ ๋ฒ๋ฆฌ๋์ผ์ผ ์ ์์ต๋๋ค. ์ฌ์ฉ์๋ ๋งํฌ๋ฅผ ํด๋ฆญํ ๋ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ํฉ๋๋ค. โํน์ ๋ชฐ๋ผ์โ Ctrl+R์ ๋๋ฅด๊ฒ ๋ง๋ค๊ณ ์ถ์ง๋ ์์ต๋๋ค.
ํด๋ผ์ด์ธํธ ์ธก ์บ์ฑ์ ์ฝํ ์ธ ๊ฐ ์์ง ๋ฐ๋์ง ์์๊ฑฐ๋, ๋ณ๊ฒฝ ์ฌํญ์ ๋ฐ์ํ ํ์๊ฐ ์์ ๋๋ ์ ์ฉํ์ง๋ง, ๋ง๋ณํต์น์ฝ์ด ์๋๋ฉฐ ๋ค๋ฅธ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง ๋ชปํฉ๋๋ค. ๋ค์ํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง๋ง ๋ฐ์ดํฐ๋ฅผ ์ต์ ์ํ๋ก ์ ์งํ๋ ค๋ ๊ฒฝ์ฐ ์์ฒญ ํ์๋ฅผ ์ค์ด์ง๋ ๋ชปํ๋ฉฐ ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด์ ๋ฐฉ์งํ๋ ๋ฐ ๋์์ด ๋์ง ์์ต๋๋ค.
์ด์ ์ฐ๋ฆฌ๋ UI์ ๋ฐ์ดํฐ ์๊ตฌ์ฌํญ์ ๊ทผ์ ํ๊ฒ ์ ์งํ๊ณ ์ถ์ง๋ง, ๋์์ ์ํฐํด ์์ฒญ์ ํผํ๊ณ ๊ณผ๋ํ ๋ณ๋ ฌ ์์ฒญ๋ ๋ง๊ณ ์ถ๋ค๋ ๊ธด์ฅ๊ฐ์ด ์๊ฒผ์ต๋๋ค. ํด๋ผ์ด์ธํธ ์ฟผ๋ฆฌ ์บ์๋ง์ผ๋ก๋ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด, ์ฐ๋ฆฌ๋ ๋ฌด์์ ํด์ผ ํ ๊น์?
์ฐ๋ฆฌ๊ฐ ํ ์ ์๋ ์ผ ์ค ํ๋๋ ์ฝ๋ ๊ทผ์ ์ฑ์ ํฌ๊ธฐํ๋ ๊ฒ์ ๋๋ค. ๊ฐ ๋ผ์ฐํธ๋ง๋ค ํด๋น ๋ผ์ฐํธ์ ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ํจ์๋ฅผ ์ ์ํ๋ค๊ณ ๊ฐ์ ํด ๋ด ์๋ค. ์ด ํจ์๋ฅผ ๋ก๋๋ผ๊ณ ๋ถ๋ฅด๊ฒ ์ต๋๋ค.
async function clientLoader({ params }) {
const { postId } = params;
const [post, comments] = await Promise.all([
fetch(`/api/posts/${postId}`).then(res => res.json()),
fetch(`/api/posts/${postId}/comments`).then(res => res.json()),
]);
return { post, comments };
}์ด ์์ ๋ ๋ฆฌ์กํธ ๋ผ์ฐํฐ์ clientLoader API๋ฅผ ์ฌ์ฉํ๊ณ ์์ง๋ง, ์ด ๊ฐ๋
์์ฒด๋ ๋ ์ผ๋ฐ์ ์
๋๋ค. ๊ฐ ๋ด๋น๊ฒ์ด์
์์ ๋ง๋ค, ๋ผ์ฐํฐ๋ ๋ค์ ๋ผ์ฐํธ์ ๋ก๋๋ฅผ ์คํํ๊ณ ๊ทธ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ๋ฅผ ์ปดํฌ๋ํธ ํธ๋ฆฌ์ ์ ๋ฌํ๋ค๊ณ ์์ํด๋ณด์ธ์.
์ด ์ ๊ทผ ๋ฐฉ์์ ๋จ์ ์, ๋ฐ์ดํฐ ์๊ตฌ์ฌํญ์ด ํด๋น ๋ฐ์ดํฐ๋ฅผ ํ์๋ก ํ๋ ์ปดํฌ๋ํธ๋ค๊ณผ ๋ ์ด์ ๋๋ํ ์์ง ์๋ค๋ ์ ์ ๋๋ค. ๊ฐ ๋ผ์ฐํธ์ โ์๋จโ์ ์๋ ์ฝ๋๊ฐ ๊ทธ ์๋์ ์ด๋ค ์ปดํฌ๋ํธ๋ค์ด ์๊ณ , ๊ทธ ์ปดํฌ๋ํธ๋ค์ด ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ํ์๋ก ํ๋์ง๋ฅผ โ์๊ณ ์์ด์ผโ ํฉ๋๋ค. ์ด ์ ์์ ๋ณด๋ฉด, ์ฟผ๋ฆฌ๋ ์ปดํฌ๋ํธ ๋ด๋ถ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ์์ ๋นํด ํ ๋จ๊ณ ํํดํ ๊ฒ์ฒ๋ผ ๋๊ปด์ง๋๋ค.
ํ์ง๋ง ์ด ์ ๊ทผ์ ์ฅ์ ์ ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด์ ํจ์ฌ ๋ ์ฝ๊ฒ ๋ฐฉ์งํ ์ ์๋ค๋ ๊ฒ์
๋๋ค. ๋ฌผ๋ก ์ฌ์ ํ clientLoader ํจ์๊ฐ ์คํ๋๋ฏ๋ก ์ํฐํด์ด ์๊ธธ ์๋ ์์ง๋ง, ์ด์ ๊ทธ ๊ตฌ์กฐ๊ฐ ๋์ ๋ณด์
๋๋ค. ์ฆ, ์ปดํฌ๋ํธ๋ ์ฟผ๋ฆฌ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋์ฒ๋ผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ํฐํด์ด ๋ฐ์ํ๋ ๋ฐฉ์์ ์๋๋๋ค.
๋ก๋๋ฅผ ์ฌ์ฉํ๋ค๋ ๋ ๋ค๋ฅธ ์ฅ์ ์, ๊ฐ ๋ผ์ฐํธ๊ฐ ๋ ๋ฆฝ์ ์ธ ๋ก๋๋ฅผ ๊ฐ์ง๊ณ ์๋ค๋ฉด ์ด ๋ก์ง์ ์ผ๋ถ๋ฅผ ์๋ฒ๋ก ์ฎ๊ธฐ๊ธฐ๊ฐ ํจ์ฌ ์ฌ์์ง๋ค๋ ์ ์ ๋๋ค. ๋ก๋๋ ์ปดํฌ๋ํธ์ ๋ ๋ฆฝ์ ์ผ๋ก ๋์ํ๋ฉฐ(์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๊ธฐ ์ ์ ์คํ๋จ), ๋ฐ๋ผ์ HTML ๋๋ API ์๋ฒ์ ์ผ๋ถ๋ก ๊ตฌ์ฑํ ์๋ ์๊ณ , ์์ ๋ณ๋์ โBFF(Backend for Frontend)โ ์๋ฒ๋ก ๊ตฌ์ฑํ ์๋ ์์ต๋๋ค.
// ์ด ์ฝ๋๋ ์๋ฒ์์ ์คํ๋ ์ ์์ต๋๋ค
async function loader({ params }) {
const { postId } = params;
const [post, comments] = await Promise.all([
fetch(`/api/posts/${postId}`).then(res => res.json()),
fetch(`/api/posts/${postId}/comments`).then(res => res.json()),
]);
return { post, comments };
}์ด๊ฒ์ ๋ฆฌ์กํธ ๋ผ์ฐํฐ์ loader ํจ์๋, ์ด์ Next.js์ getServerSideProps()๊ฐ ๋ฐ๋๋ ๋ชจ๋ธ์
๋๋ค. ๋ณดํต ๋น๋ ์์ ์ ์ฝ๋ ๋ณํ์ ํตํด ์ด ๋ก๋ ์ฝ๋๋ฅผ ํด๋ผ์ด์ธํธ์ฉ ์ฝ๋์ โ๋ถ๋ฆฌโํ๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ ๋ค๋ฉด, ์ ๋ก๋๋ฅผ ์๋ฒ๋ก ์ฎ๊ธฐ๋ ๊ฑธ๊น์?
์๋ฒ๋ฅผ ๋จ์ํ ๋ธ๋๋ฐ์ค๋ก ๋ณด์ง ์๋๋ค๋ฉด, ์๋ฒ๋ ๋ฐ์ดํฐ ์์ฒญ ์ฝ๋๋ฅผ ๋ฐฐ์นํ๊ธฐ์ ๊ฐ์ฅ ์์ฐ์ค๋ฌ์ด ์ฅ์์ ๋๋ค. ์๋ฒ๋ ์ผ๋ฐ์ ์ผ๋ก ์ฑ๋ฅ ๋ฌธ์ ๋ฅผ ๊ฐ์ ํ ์ ์๋ ์๋จ๋ค์ ๋ง์ด ๊ฐ์ง๊ณ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ง์ฐ ์๊ฐ์ ์ค์ด๊ธฐ ์ํด BFF ์๋ฒ๋ฅผ ๋ฐ์ดํฐ ์์ค ๊ฐ๊น์ด ๋ฐฐ์นํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ๋ด์ฌ๋ ์ํฐํด ์์ฒญ๋ ์ ๋ ดํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ๋ฐ์ดํฐ ์์ค๊ฐ ๋๋ฆด ๊ฒฝ์ฐ์๋, ์๋ฒ์์๋ ํฌ๋ก์ค ์์ฒญ ์บ์ ๊ฐ์ ๋ฉ์ปค๋์ฆ์ ์ถ๊ฐํ ์ ์์ต๋๋ค. ๋๋ ๋ง์ดํฌ๋ก์๋น์ค ์ ์ฒด๋ฅผ ํฌ๊ธฐํ๊ณ Rails์์ ์ฒ๋ผ ๋ฐ์ดํฐ ๊ณ์ธต์ ์ธํ๋ก์ธ์ค๋ก ์ฎ๊ธธ ์๋ ์์ต๋๋ค.
import { loadPost, loadComments } from 'my-data-layer';
async function loader({ params }) {
const { postId } = params;
const [post, comments] = await Promise.all([
loadPost(postId), loadComments(postId), ]);
return { post, comments };
}์ธํ๋ก์ธ์ค ๋ฐ์ดํฐ ๊ณ์ธต์ ์ต์ ํ๋ฅผ ์ํ ์ต๊ณ ์ ๊ธฐํ๋ฅผ ์ ๊ณตํฉ๋๋ค. ํ์ํ ๊ฒฝ์ฐ ๋ ๋ฎ์ ์์ค์ผ๋ก ๋ด๋ ค๊ฐ์ ํน์ ํ๋ฉด์ ์ํ ์ ์ฅ ํ๋ก์์ (stored procedure)๋ฅผ ์ง์ ํธ์ถํ ์๋ ์์ต๋๋ค. ์์ฒญ ๋น ๋ฉ๋ชจ๋ฆฌ ๋ด ์บ์ฑ๊ณผ ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ํตํด DB ํธ์ถ ํ์๋ฅผ ๋ ์ค์ผ ์ ์์ต๋๋ค. ์ค๋ฒํ์นญ์ด๋ ์ธ๋ํ์นญ์ ๋ํด ๊ฑฑ์ ํ ํ์๋ ์์ต๋๋ค. ๊ฐ ๋ก๋๋ ํด๋น ํ๋ฉด์ ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ๊ฒ ์ ๋ฌํ๋ฉด ๋๋๊น์. ๋ ์ด์ โRESTโ โ๋ฆฌ์์คโ๋ฅผ โํ์ฅโํ ํ์๊ฐ ์์ต๋๋ค.
์ค๋ น REST API ํธ์ถ์ ์ฌ์ฉํ๋๋ผ๋, ์ฐ๋ฆฌ๋ ์ ํต์ ์ธ โHTML ์ฑโ์ ์ ์ฉํ ํน์ฑ, ์ฆ, Rails๋ Django๋ก ๊ตฌ์ฑ๋ ์ํคํ ์ฒ๋ ๊ทธ๋๋ก ์ ์ง๋ฉ๋๋ค. ํด๋ผ์ด์ธํธ ๊ด์ ์์ ๋ณด๋ฉด, ๋ฐ์ดํฐ(JSON)๋ ๋จ์ผ ์๋ณต ์์ฒญ์ผ๋ก ๋์ฐฉํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด์ ์ด ๋ชจ๋ธ์์๋ ์ ๋ ๋ฐ์ํ์ง ์์ต๋๋ค.
์, ์ด๊ฒ์ด ์๋ฒ ๋ก๋์ ์ฅ์ ์ ๋๋ค. ๊ทธ๋ ๋ค๋ฉด ๋จ์ ์ ๋ฌด์์ผ๊น์?
์์ ๋ก๋๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ์ ๋, ์ฐ๋ฆฌ๋ ์ฝ๋ ๊ทผ์ ์ฑ์ ํฌ๊ธฐํด์ผ ํ์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด, ๋ก๋๋ฅผ ์๋ฒ์ ๋จ๊ฒจ๋๋ฉด์ ์ปดํฌ๋ํธ๋ง๋ค ๋ก๋๋ฅผ ํ๋์ฉ ์ ์ํ๋ฉด ์ด๋จ๊น์? ๋ค์ ๋งํด ์ฝ๋ ๊ทผ์ ์ฑ์ ๋์ฐพ๋ ๊ฒ๋๋ค. ์ด๋ฅผ ์ํด์ ์๋ฒ์ ํด๋ผ์ด์ธํธ ์ฝ๋ ๊ฐ์ ๊ฒฝ๊ณ๋ฅผ ์ข ๋ ๋ชจํธํ๊ฒ ๋ง๋ค์ด์ผ ํ ์๋ ์์ง๋ง, ์ผ๋จ ํ๋ฒ ์๋ํด๋ณด๋ฉด์ ๊ฒฐ๊ณผ๋ฅผ ์ง์ผ๋ด ์๋ค.
์ด๊ฑธ ์ด๋ป๊ฒ ๊ตฌํํ ์ง๋ ์ฌ์ฉํ๋ โ๊ฒฝ๊ณ ํ๋ฆฌ๊ธฐโ ๋ฐฉ์์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋๋ค. ๋จผ์ TanStack ์๋ฒ ํจ์๋ฅผ ์๋ก ๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
์ด ๋ฐฉ์์ ํด๋ผ์ด์ธํธ์์ ์ง์ importํ ์ ์๋ ์๋ฒ ํจ์๋ฅผ ์ ์ธํ ์ ์๊ฒ ํด์ค๋๋ค.
import { createServerFn } from '@tanstack/react-start';
import { loadPost, loadComments } from 'my-data-layer';
export const getPost = createServerFn({ method: 'GET' }).handler(async postId =>
loadPost(postId)
);
export const getComments = createServerFn({
method: 'GET',
}).handler(async postId => loadComments(postId));๋ ๋ค๋ฅธ ์๋ ๋ฆฌ์กํธ ์๋ฒ ํจ์ ๋ฌธ๋ฒ์ ์ฌ์ฉํ๋ ๊ฒ์ ๋๋ค.
'use server';
import { loadPost, loadComments } from 'my-data-layer';
export async function getPost(postId) {
return loadPost(postId);
}
export async function getComments(postId) {
return loadComments(postId);
}๋์ ์ฐจ์ด์ ๋ํด ์ด ๊ธ์์๋ ๊น์ด ๋ค๋ฃจ์ง ์๊ฒ ์ต๋๋ค. ์ฌ๊ธฐ์๋ ๋ ๋ค ์๋ฌต์ ์ธ RPC ์๋ํฌ์ธํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผํ๊ฒ ์ต๋๋ค.
์์ ์ ํด๋ผ์ด์ธํธ ์ธก ์ปดํฌ๋ํธ์์ ์ง์ ์๋ํฌ์ธํธ๋ฅผ importํ ์ ์๋ค๋ ๊ฒ์
๋๋ค. ๊ตณ์ด REST ์๋ํฌ์ธํธ๋ API ๋ผ์ฐํธ๋ฅผ ๋ฐ๋ก ๋ง๋ค ํ์๊ฐ ์์ต๋๋ค. import๋ง์ผ๋ก ์๋ฌต์ ์ธ API ๋ผ์ฐํธ๊ฐ ๋๋ ์
์ด์ฃ .
์ด์ ์ฝ๋ ๊ทผ์ ์ฑ์ด ๋ค์ ์๊ฒผ์ต๋๋ค! PostContent ์ปดํฌ๋ํธ๋ getPost๋ง ํ์ํฉ๋๋ค.
import { getPost } from './my-server-functions';import { Comments } from './Comments';
function usePostQuery(postId) {
return useQuery(['post', postId], () => getPost(postId));}
function PostContent({ postId }) {
const { data: post } = usePostQuery(postId);
if (!post) {
return null;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<Comments postId={postId} />
</article>
);
}๋ง์ฐฌ๊ฐ์ง๋ก Comments๋ ์๋ฒ๋ก๋ถํฐ getComments๋ฅผ ์ง์ importํ ์ ์์ต๋๋ค.
import { getComments } from './my-server-functions';
function usePostCommentsQuery(postId) {
return useQuery(['post-comments', postId], () => getComments(postId));}
export function Comments({ postId }) {
const { data: comments } = usePostCommentsQuery(postId);
return (
<ul className="comments">
{comments.map(c => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}๊ทธ๋ฐ๋ฐ ์ ๊น๋ง์โฆ
์ด ๋ฐฉ์์ ์์ ์ค๋ช ํ ๋ฌธ์ ๋ค์ ํด๊ฒฐํด์ฃผ์ง ์์ต๋๋ค!
์ฌ์ค, ์ฑ๋ฅ ์ธก๋ฉด์์ ๋ณด๋ฉด ์ปดํฌ๋ํธ๋ ์ฟผ๋ฆฌ ๋ด๋ถ์์ fetchํ๋ ๋ฐฉ์์ผ๋ก ๋๋์๊ฐ ์
์
๋๋ค. ์๋ฒ ํจ์(Server Function)์ ์ฅ์ ์ ๋ ๊น๋ํ ๋ฌธ๋ฒ(import๋ฅผ ํตํ ํธ์ถ)๋ฟ์ด๊ณ , ์ด ๋ฐฉ์์ ์ฝ๋์ ๊ทผ์ ํ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ์ ์ฌ์ฉํ๋ฉด ์๋ฒ ๋ก๋๋ณด๋ค ์ฑ๋ฅ์ด ์คํ๋ ค ํด๋ณดํฉ๋๋ค. ์๋ฒ ํจ์๋ ๋จ์ผ ์๋ณต ์์ฒญ์ ๊ฐ์ ํ์ง๋ ์๊ณ , ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด๋ ๋ฐฉ์งํ์ง ๋ชปํฉ๋๋ค. ์๋ฒ ํจ์๋ ์๋ฒ ํธ์ถ์ ๋จ์ํํด์ฃผ์ง๋ง, ๋ฐ์ดํฐ ๋ก๋ฉ ์์ฒด๋ฅผ ๊ฐ์ ํด์ฃผ์ง๋ ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋์์ ๋ฌด์์ผ๊น์?
์ํ๊น๊ฒ๋ ์คํด๋ฐ์์์ง๋ง, GraphQL์ ํจ์จ์ ์ธ ์ฝ๋ ๊ทผ์ ์ฑ์ ์คํํ๊ธฐ ์ํ ํ๋์ ์ ๊ทผ ๋ฐฉ์์ ๋๋ค.
GraphQL์ ๋ณธ๋ ์๋๋ ๊ฐ๋ณ ์ปดํฌ๋ํธ๋ค์ด ํ์ํ ๋ฐ์ดํฐ ์์กด์ฑ์ ํ๋๊ทธ๋จผํธ(fragment)๋ก ์ ์ธํ๊ณ , ์ด ํ๋๊ทธ๋จผํธ๋ค์ด ํ๋๋ก ํฉ์ณ์ง๋๋ก ๋ง๋๋ ๊ฒ์ด์์ต๋๋ค. (์๋ ์ด ์ง๋์์ผ Apollo์์๋ ์ด๋ฅผ ์ ๋๋ก ์ง์ํ๊ฒ ๋์์ต๋๋ค.)
์ด ๋ฐฉ์์์๋ Comment ์ปดํฌ๋ํธ๊ฐ ์ค์ค๋ก ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์ ์ธํ ์ ์์ต๋๋ค.
function Comments({ comments }) {
return (
<ul className="comments">
{comments.map(comment => (
<Comment key={comment.id} comment={comment} />
))}
</ul>
);
}
function Comment({ comment }) {
const data = useFragment(
graphql`
fragment CommentFragment on Comment { id text } `,
comment
);
return <li>{data.text}</li>;
}์ฌ๊ธฐ์ ์ค์ํ ์ ์ Comment ์ปดํฌ๋ํธ๊ฐ ์ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง๋ ์๋๋ค๋ ๊ฒ์
๋๋ค. ๋จ์ง ์ด๋ค ๋ฐ์ดํฐ๊ฐ ํ์ํ์ง๋ง ์ ์ธํฉ๋๋ค. ์ด์ PostContent ์ปดํฌ๋ํธ๋ฅผ ์ดํด๋ด
์๋ค.
PostContent ์ปดํฌ๋ํธ๋ Comment์ ํ๋๊ทธ๋จผํธ๋ฅผ ์์ ์ ํ๋๊ทธ๋จผํธ์ ํฌํจ์ํต๋๋ค.
function PostContent({ post }) {
const data = useFragment(
graphql`
fragment PostContentFragment on Post {
title
content
comments {
id
...CommentFragment }
}
`,
post
);
return (
<article>
<h1>{data.title}</h1>
<p>{data.content}</p>
<Comments comments={data.comments} />
</article>
);
}์ค์ ๋ฐ์ดํฐ ์์ฒญ์ ์์ ์์ค ์ด๋๊ฐ์์ ์ด๋ฃจ์ด์ง๋๋ค. ์ด ํ๋๊ทธ๋จผํธ๋ค์ ์ ์ฒด ๋ผ์ฐํธ๋ฅผ ์ํ ๋ค์๊ณผ ๊ฐ์ GraphQL ์ฟผ๋ฆฌ๋ก ํฉ์ณ์ง๊ฒ ๋ฉ๋๋ค.
query PostPageQuery($postId: ID!) {
post(id: $postId) {
# PostContentFragment์์ ๊ฐ์ ธ์จ ๊ฒ
title
content
comments {
# CommentFragment์์ ๊ฐ์ ธ์จ ๊ฒ
id
text
}
}
}์ด๋ ๋ง์น ์๋์ผ๋ก ์์ฑ๋๋ ๋ก๋์ ๊ฐ์ต๋๋ค!
์ด์ ๊ฐ ํ๋ฉด๋ง๋ค, ํด๋น ํ๋ฉด์์ ์ค์ ๋ก ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ปดํฌ๋ํธ์ ์์ค ์ฝ๋์ ๊ธฐ๋ฐํ์ฌ ์ ํํ ๋ฌ์ฌํ๋ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๊ฒ ๋ฉ๋๋ค. ์ด๋ค ์ปดํฌ๋ํธ๊ฐ ํ์๋ก ํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด, ํด๋น ์ปดํฌ๋ํธ ๋ด์ ํ๋๊ทธ๋จผํธ๋ง ์์ ํ๋ฉด ๋๊ณ , ์ ์ฒด ์ฟผ๋ฆฌ๋ ์๋์ผ๋ก ๊ฐฑ์ ๋ฉ๋๋ค. GraphQL ํ๋๊ทธ๋จผํธ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ ๋ด๋น๊ฒ์ด์ ๋ง๋ค ๋จ์ผ ์๋ณต ์์ฒญ์ผ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ฉํ ์ ์์ต๋๋ค.
๋ฌผ๋ก GraphQL์ด ๋ชจ๋ ์ฌ๋์๊ฒ ์ ํฉํ ๊ฒ์ ์๋๋๋ค. ์ ์ญ์ ์์ง ๋ฌธ๋ฒ์ด ๋ค์ ํผ๋์ค๋ฝ๊ฒ ๋๊ปด์ง ๋๊ฐ ์๊ณ (๋ถ๋ถ์ ์ผ๋ก๋ ์ ๊ฐ ์ด๋ฅผ ๋ง์ด ์ฌ์ฉํด๋ณด์ง ์์๊ธฐ ๋๋ฌธ์ ๋๋ค), ์ด๊ฑธ ์ ๋๋ก ์ฌ์ฉํ๋ ค๋ฉด ์๋ฒ ์ธก๊ณผ ํด๋ผ์ด์ธํธ ์ธก ๋ชจ๋์์ ์ผ์ ์์ค์ ์กฐ์ง์ ์ธ ์ดํด๊ฐ ํ์ํฉ๋๋ค. ์ ๋ GraphQL์ ๋ํด ์์ ์ ํ๋ ค๋ ๊ฒ ์๋๋๋ค.
ํ์ง๋ง GraphQL์ด ์ด ๋ฌธ์ ๋ฅผ ์ค์ ๋ก ํด๊ฒฐํ ๋ช ์ ๋๋ ๋ฐฉ๋ฒ ์ค ํ๋๋ผ๋ ์ ์ ๋ถ๋ช ํ ์ธ๊ธํ ํ์๊ฐ ์์ต๋๋ค. GraphQL์ ๋ฐ์ดํฐ ์๊ตฌ์ฌํญ์ UI์ ๋๋ํ ์ ์ธํ ์ ์๊ฒ ํด์ฃผ๋ฉด์๋, ๋จ์ํ๊ฒ ์ปดํฌ๋ํธ๋ ์ฟผ๋ฆฌ ์์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ ๊ทผ ๋ฐฉ์์์ ๋ฐ์ํ๋ ๋จ์ ์ ํํผํ ์ ์๊ฒ ํด์ค๋๋ค(์ด๋ฌํ ๋จ์ ์ ์๋ฒ ํจ์์ ์ฌ์ฉํ๋ ์๋๋ ์กด์ฌํฉ๋๋ค). ๋ค์ ๋งํด, GraphQL์ ์๋ฒ ๋ก๋์ ์ฑ๋ฅ ํน์ฑ๊ณผ ์ฟผ๋ฆฌ์ ์ฝ๋ ๊ทผ์ ์ฑ๊ณผ ๋ชจ๋์ฑ์ ๋ชจ๋ ์ ๊ณตํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด์ ์ ์ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋ ๋ ๋ค๋ฅธ ๋ฐฉ์์ด ์กด์ฌํฉ๋๋ค.
๋ฆฌ์กํธ ์๋ฒ ์ปดํฌ๋ํธ๋ 2010๋ ๋ ๋ด๋ด ๋ฆฌ์กํธ ํ์ ๊ดด๋กญํ๋ ์ง๋ฌธ์ ๋ํ ๋ต์ ๋๋ค. โ๋ฆฌ์กํธ์์ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ๊ฐ์ ธ์ฌ ๊ฒ์ธ๊ฐ?โ
๊ฐ ๋ฐ์ดํฐ๊ฐ ํ์ํ ์ปดํฌ๋ํธ๋ง๋ค ์์ฒด ์๋ฒ ๋ก๋๊ฐ ์์ ์ ์๋ค๊ณ ์์ํด๋ณด์ธ์. ์ปดํฌ๋ํธ๋น ํ๋์ ํจ์๊ฐ ๊ฐ์ฅ ๋จ์ํ ํด๊ฒฐ์ฑ ์ ๋๋ค.
์ด์ ์ฐ๋ฆฌ๋ ์ปดํฌ๋ํธ ์์์ ์๋ฒ ๋ก๋๋ฅผ ์ง์ ํธ์ถํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ํด๋ผ์ด์ธํธ/์๋ฒ ์ํฐํด๋ก ๋ฐ๋ก ๋์๊ฐ๋ ์ค์์์ ์๊ณ ์์ต๋๋ค. ๊ทธ๋์ ์ฐ๋ฆฌ๋ ๋ฐ๋๋ก ์ ๊ทผํฉ๋๋ค. ์๋ฒ ๋ก๋๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ ๋์ ์ปดํฌ๋ํธ๋ฅผ ๋ฐํํ ๊ฒ์ ๋๋ค.
import { loadPost, loadComments } from 'my-data-layer';
import { PostContent, Comments } from './client';
function PostContentLoader({ postId }) {
const post = await loadPost(postId);
return (
<PostContent post={post}> <CommentsLoader postId={postId} />
</PostContent> );
}
function CommentsLoader({ postId }) {
const comments = await loadComments(postId);
return <Comments comments={comments} />;}'use client';
export function PostContent({ post, children }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{children}
</article>
);
}
export function Comments({ comments }) {
return (
<ul className="comments">
{comments.map(c => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}๋ฐ์ดํฐ๋ ์์์ ์๋๋ก ํ๋ฆ
๋๋ค. ์๋ฒ๊ฐ ์ง์ค์ ์์ฒ(source of truth)์
๋๋ค. ์๋ฒ๋ก๋ถํฐ ํ๋กํผํฐ๋ฅผ ๋ฐ๊ณ ์ ํ๋ ์ปดํฌ๋ํธ๋ 'use client' ์ง์์ด๋ฅผ ํตํด ์ด๋ฅผ ๋ช
์์ ์ผ๋ก ํํํฉ๋๋ค. ์ฐ๋ฆฌ์ ์๋ฒ ๋ก๋๋ ์ปดํฌ๋ํธ์ฒ๋ผ ์๊ฒผ๊ธฐ ๋๋ฌธ์ ์ด๋ค์ ์๋ฒ ์ปดํฌ๋ํธ๋ผ๊ณ ๋ถ๋ฅด์ง๋ง, ์ค์ ๋ก๋ ๊ตฌ์ฑ ๊ฐ๋ฅํ ํํ์ ์๋ฒ ๋ก๋์ธ ์
์
๋๋ค.
์ด ๊ตฌ์กฐ๋ ์์ ์ โ์ปจํ ์ด๋ vs ํ๋ ์ ํ ์ด์ ์ปดํฌ๋ํธโ ํจํด์ ๋ ์ฌ๋ฆฌ๊ฒ ํ ์๋ ์์ง๋ง, ์ฌ๊ธฐ์๋ ๋ชจ๋ โ์ปจํ ์ด๋โ๊ฐ ์๋ฒ์์ ์คํ๋์ด ์ถ๊ฐ์ ์ธ ์๋ณต ์์ฒญ์ ๋ฐฉ์งํ๋ค๋ ์ฐจ์ด๊ฐ ์์ต๋๋ค.
์ด ์ ๊ทผ ๋ฐฉ์์ผ๋ก ๋ฌด์์ ์ป์ ์ ์์๊น์?
์ค์ ๋ก ์ ์์ ๋ ๋ค์๊ณผ ๊ฐ์ด ๋จ์ํํ ์๋ ์์ต๋๋ค.
import { loadPost, loadComments } from 'my-data-layer';
async function PostContent({ postId }) {
const post = await loadPost(postId);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<Comments postId={postId} />
</article>
);
}
async function Comments({ postId }) {
const comments = await loadComments(postId);
return (
<ul className="comments">
{comments.map(c => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์์ฒญํ ๋(์ด๊ธฐ ๋ก๋ฉ์ด๋ ์ดํ ๋ด๋น๊ฒ์ด์
์ด๋ ), ํด๋ผ์ด์ธํธ๋ ์๋ฒ์ ๋จ์ผ ์์ฒญ์ ๋ณด๋
๋๋ค. ์๋ฒ๋ <PostContent postId={123} />์์๋ถํฐ ์ถ๋ ฅ์ ์ง๋ ฌํํ๊ธฐ ์์ํ๋ฉฐ, ์ด๋ฅผ ์ฌ๊ท์ ์ผ๋ก ํผ์ณ์ ๋ฆฌ์กํธ ํธ๋ฆฌ๋ฅผ ์คํธ๋ฆฌ๋ฐํฉ๋๋ค. ์ด ํธ๋ฆฌ๋ HTML๋ก ๋ณํ๋๊ฑฐ๋ JSON์ผ๋ก ์ง๋ ฌํ๋ฉ๋๋ค.
ํด๋ผ์ด์ธํธ ์ ์ฅ์์๋, ๋ชจ๋ ๋ด๋น๊ฒ์ด์ ์ด ์๋ฒ๋ก์ ๋จ์ผ ์์ฒญ์ ์๋ฏธํฉ๋๋ค. ์๋ฒ ์ ์ฅ์์๋, ๋ฐ์ดํฐ ๋ก๋ฉ ๋ก์ง์ด ํ์ํ ๋งํผ ๋ชจ๋ํ๋์ด ์์ต๋๋ค. ์๋ฒ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๋ฐฉ์์ผ๋ก ํด๋ผ์ด์ธํธ ํธ๋ฆฌ๋ฅผ ๋ฐํํฉ๋๋ค.
์ด ๊ธ์์ ํ์๋ RSC๊ฐ ๊ธฐ์กด์ ๋ค์ํ ๋ฐ์ดํฐ ํจ์นญ ๋ฐฉ์๋ค๊ณผ ์ด๋ค ๊ด๊ณ๋ฅผ ๊ฐ์ง๋์ง๋ฅผ ์ค๋ช ํ๊ณ ์ ํ์ต๋๋ค. ๋ค๋ฃจ์ง ๋ชปํ ๋ด์ฉ๋ ๋ง์ง๋ง, ๋ช ๊ฐ์ง๋ง ์ธ๊ธํด๋ณด๊ฒ ์ต๋๋ค.
@defer ์ง์์ด๋ก ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค.)๋ง์ง๋ง์ผ๋ก ๊ฐ์กฐํ๊ณ ์ถ์ ๊ฒ์, ์ฝ๋ ๊ทผ์ ์ฑ๊ณผ ํจ์จ์ฑ์ด๋ผ๋ ๋ ๊ฐ์ง ๋ฌธ์ ๋ฅผ ๋์์ ํด๊ฒฐํ๋ ค๋ ์ ๊ทผ์ ํ์น ์๋ค๋ ์ ์ ๋๋ค. HTML ํ ํ๋ฆฟ์ด ๊ทธ๋ ๊ณ (Astro๊ฐ ๊ทธ ํ๋์ ์ธ ๊ตฌํ์ฒด ์ค ํ๋์ ๋๋ค), GraphQL์ด ๊ทธ๋ ๊ณ , RSC๋ ๊ทธ์ค ํ๋์ ๋๋ค.
๋น์ ์ด ์ข์ํ๋ ํ๋ ์์ํฌ์๊ฒ ๋ฌผ์ด๋ณผ ์ง๋ฌธ์ด ํ๋ ์๊ฒผ๋ค์.
๐ ํ๊ตญ์ด๋ก ๋ ํ๋ฐํธ์๋ ์ํฐํด์ ๋น ๋ฅด๊ฒ ๋ฐ์๋ณด๊ณ ์ถ๋ค๋ฉด Korean FE Article(https://kofearticle.substack.com/)์ ๊ตฌ๋ ํด์ฃผ์ธ์!