React Server Components ์ฌ์ธต ๋ถ์: useEffect, ์ด์ ๋ณด๋ด์ค์๋ค
Next.js 13, 14๊ฐ ๋์ค๋ฉด์ React ์ํ๊ณ๊ฐ ์๋์๋ํฉ๋๋ค. ๋จ์ํ ์๋ก์ด ํ ์ด ๋์๋ค๊ฑฐ๋ ์๋๊ฐ ๋นจ๋ผ์ก๋ค๋ ์์ค์ด ์๋์ฃ . ์ฐ๋ฆฌ๋ ์ง๊ธ React Server Components(RSC) ๋ผ๋ ๊ฑฐ๋ํ ํ๋๋ฅผ ๋ง์ฃผํ๊ณ ์์ต๋๋ค.
์์งํ ๋งํด์, ๊ฝค ํผ๋์ค๋ฝ์ง ์๋์? "์ง๋ ฌํ(serialization) ์ค๋ฅ" ๊ฐ์ ๋ฏ์ ์๋ฌ๋ค์ด ํ์ด๋์ค๊ณ , ๊ทธ๋์ ์ ์จ์๋ useEffect๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ์์ ์ด์ ์ํฐ ํจํด ์ทจ๊ธ์ ๋ฐ๊ธฐ ์์ํ์ต๋๋ค. ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ ์๋ฒ ์ปดํฌ๋ํธ๋ ๊ฒฝ๊ณ๋ฅผ ๋๋๋ ๊ฒ๋ ๋จธ๋ฆฌ ์ํ๊ณ ์.
ํต์ฌ์ ์ด๊ฒ๋๋ค. ์ฐ๋ฆฌ๊ฐ ์๋
๊ฐ "๋ฐ์ดํฐ ๊ฐ์ ธ์ฌ ๋ ๋น์ฐํ ์ฐ๋ ๊ฒ"์ด๋ผ ์ฌ๊ฒผ๋ useEffect๊ฐ ์ด์ ๋ฐ์ดํฐ ํ์นญ์ ์ฃผ๋๊ถ์ ๋บ๊ฒผ๋ค๋ ์ฌ์ค์
๋๋ค.
๊ทธ๋ผ useEffect๋ ์ด์ ์ธ๋ชจ์๋ ๊ฑธ๊น์? ๊ทธ๊ฑด ์๋๋๋ค. ํ์ง๋ง "๋ฐ์ดํฐ ํ์นญ" ์ฉ๋๋ก๋ ์ด์ ๋์์ค ๋๊ฐ ๋์์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ ๊ฒํฅ๊ธฐ ์์ผ๋ก "RSC ์ฐ๋ ๋ฒ"๋ง ๋ค๋ฃจ์ง ์์ต๋๋ค. ๋๋์ฒด ๋ด๋ถ๊ฐ ์ด๋ป๊ฒ ๋์๊ฐ๋์ง, ์ RSC๊ฐ ๊ธฐ์กด SPA์ ์ํคํ ์ฒ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ "์นํธํค"์ธ์ง, ๊ทธ๋ฆฌ๊ณ ๋ฐ๋ ๋ฉํ ๋ชจ๋ธ์ด ์ค์ ๋๊ท๋ชจ ์ฑ ๊ฐ๋ฐ์ ์ด๋ค ์ด์ ์ ์ฃผ๋์ง ์์ฃผ ๊น๊ฒ ํ๋ณด๊ฒ ์ต๋๋ค.
1. ์ฐ๋ฆฌ๊ฐ ๊ฒช๋ ๊ณ ํต: ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์ํฐํด (Waterfall)
RSC๊ฐ ์ ํ์ํ์ง ๋ฉ๋ํ๋ ค๋ฉด, ์ฐ๋ฆฌ๊ฐ ๊ทธ๋์ React๋ก ๊ฐ๋ฐํ๋ฉด์ ๊ฒช์๋ ๊ณ ์ง์ ์ธ ๋ฌธ์ ๋ฅผ ์ง์ํด์ผ ํฉ๋๋ค.
์ผ๋ฐ์ ์ธ React ์ฑ(SPA)์ ์๊ฐํด ๋ด
์๋ค. ๋ธ๋ผ์ฐ์ ๊ฐ JS ๊พธ๋ฌ๋ฏธ๋ฅผ ์๋ฉ ๋ด๋ ค๋ฐ์ต๋๋ค. React๊ฐ ์คํ๋๊ณ , ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ๊ทธ๋ฆฝ๋๋ค. ๊ทธ์ ์ผ useEffect๊ฐ "์์ฐจ, ๋ฐ์ดํฐ ๊ฐ์ ธ์์ผ์ง" ํ๊ณ API๋ฅผ ํธ์ถํฉ๋๋ค.
// ์ต์ํ์์ฃ ? ์ฐ๋ฆฌ๊ฐ ๋งจ๋ ์ง๋ ์ฝ๋์ ๋๋ค. function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // 1. ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง ๋๊ณ ๋์์ผ ํจ์นญ ์์ useEffect(() => { fetch(`/api/users/${userId}`).then(data => { setUser(data); setLoading(false); }); }, [userId]); if (loading) return <Spinner />; return ( <div> <h1>{user.name}</h1> {/* 2. ์ด๊ฒ ๋ ๋๋ง ๋์ด์ผ ๋น๋ก์ ์์์ ํจ์นญ์ด ์์๋จ */} <UserPosts userId={userId} /> </div> ); }
์ฌ๊ธฐ์ ์น๋ช
์ ์ธ ๋ฌธ์ ๋ <UserPosts />์
๋๋ค. ๋ถ๋ชจ์ธ UserProfile์ ๋ฐ์ดํฐ๊ฐ ๋ค ๋ก๋ฉ๋๊ณ ๋ ๋๋ง์ด ๋๋์ผ, ๋น๋ก์ ์์์ธ UserPosts๊ฐ ๋ง์ดํธ๋๊ณ ์์ ์ useEffect๋ฅผ ๋๋ฆฝ๋๋ค.
- JS ๋ก๋
UserProfile๋ ๋๋ง โ API ํธ์ถ- (๊ธฐ๋ค๋ฆผ...) โ ๋ฐ์ดํฐ ๋์ฐฉ โ ๋ฆฌ๋ ๋๋ง
UserPosts๋ง์ดํธ โ API ํธ์ถ- (๋ ๊ธฐ๋ค๋ฆผ...) โ ๋ฐ์ดํฐ ๋์ฐฉ โ ๋ฆฌ๋ ๋๋ง
์ด๊ฒ ๋ฐ๋ก ๋คํธ์ํฌ ์ํฐํด(Network Waterfall) ์ ๋๋ค. ํญํฌ์์ฒ๋ผ ์์ฐจ์ ์ผ๋ก ๋จ์ด์ง๋ค๋ ๊ฑฐ์ฃ . ์ฌ์ฉ์๋ ๊ณ์ ๋์๊ฐ๋ ๋ก๋ฉ ์คํผ๋๋ฅผ ๋ณด๋ฉฐ ๊ธฐ๋ค๋ ค์ผ ํฉ๋๋ค. (Loading Spinner Hell ๐)
React Query๋ SWR์ด ์ด ๋ฌธ์ ๋ฅผ ๋ง์ด ์ํํด์ฃผ๊ธด ํ์ง๋ง, ๊ทผ๋ณธ์ ์ธ ํ๊ณ๋ ์ฌ์ ํ์ต๋๋ค. "๋ฐ์ดํฐ ๋ก์ง์ ์ปดํฌ๋ํธ์ ์๋๋ฐ, ์คํ์ ๋ฌด์กฐ๊ฑด ํด๋ผ์ด์ธํธ ๋ธ๋ผ์ฐ์ ์์ ํด์ผ ํ๋ค" ๋ ์ ์ฝ ๋๋ฌธ์ด์ฃ .
2. React Server Components: ์๋ฒ๋ฅผ ์ปดํฌ๋ํธํ ํ๋ค
RSC์ ์์ด๋์ด๋ ๋จ์ํ์ง๋ง ๊ฐ๋ ฅํฉ๋๋ค. "๋ฐ์ดํฐ๊ฐ ์๋ ๊ณณ(์๋ฒ)์์ ์ปดํฌ๋ํธ๋ฅผ ๋ฏธ๋ฆฌ ๋ ๋๋งํ์."
RSC๋ ๋ฐฑ์๋์ ์ง์ ์ ๊ทผํ ์ ์์ต๋๋ค. API๋ฅผ ๋ฐ๋ก ๋ง๋ค ํ์ ์์ด ๋ฐ๋ก DB๋ฅผ ์ฐ๋ฌ๋ ๋ฉ๋๋ค. ํด๋ผ์ด์ธํธ๋ก ์ฝ๋๋ฅผ ๋ณด๋ผ ํ์๋ ์์ฃ .
RSC ๋ฐฉ์์ผ๋ก ๋ฐ๊พธ๋ฉด ์ฝ๋๊ฐ ์ด๋ ๊ฒ ๋ฐ๋๋๋ค:
// app/users/[id]/page.tsx import db from '@/lib/db'; // 1. async ์ปดํฌ๋ํธ์ ๋๋ค. async function UserProfile({ params }) { // 2. DB ์ง์ ์กฐํ! API fetch ๋์ await๋ก ๋ฐ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค. const user = await db.user.findUnique({ where: { id: params.id } }); return ( <div> <h1>{user.name}</h1> <UserPosts userId={params.id} /> </div> ); } async function UserPosts({ userId }) { const posts = await db.post.findMany({ where: { authorId: userId } }); return ( <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> ); }
ํ ๋ฌ๋ผ์ง ์ ์ด ๋ณด์ด์๋์?
useState,useEffect์ญ์ : ์ํ ๊ด๋ฆฌ๋ ์ฌ์ด๋ ์ดํํธ ์ฒ๋ฆฌ๊ฐ ํ์ ์์ต๋๋ค.async/await์ฌ์ฉ: ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ์ปดํฌ๋ํธ ๋ ๋ฒจ์์ ๋ฐ๋ก ์ผ์ด๋ฉ๋๋ค.
์๋ฒ๊ฐ UserProfile์ ๊ทธ๋ฆด ๋ DB๋ฅผ ์กฐํํฉ๋๋ค. ๋ฐ์ดํฐ๊ฐ ์ค๋ฉด ๋ฐ๋ก UserPosts๋ก ๋์ด๊ฐ์ ๋ DB๋ฅผ ์กฐํํฉ๋๋ค. ์ด ๋ชจ๋ ๊ฒ ์๋ฒ ๋ด๋ถ(๋ฐ์ดํฐ์ผํฐ) ์์ ์ผ์ด๋ฉ๋๋ค. ๋คํธ์ํฌ ์ง์ฐ(Latency)์ด ๊ฑฐ์ ์์ฃ . ํด๋ผ์ด์ธํธ์ ์๋ฒ๋ฅผ ์๋ค ๊ฐ๋ค ํ๋ ๋น์ฉ์ด '0'์ ์๋ ดํฉ๋๋ค.
3. ๋ธ๋ผ์ฐ์ ๋ HTML ๋์ "Flight"๋ฅผ ๋ฐ๋๋ค
"๊ทธ๋ผ ๊ทธ๋ฅ SSR(Server Side Rendering)์ด๋ ๊ฐ์ ๊ฑฐ ์๋?"๋ผ๊ณ ์๊ฐํ์ค ์ ์์ต๋๋ค. ๋ค๋ฆ ๋๋ค.
RSC๋ฅผ ์์ฒญํ๋ฉด ์๋ฒ๋ ์์ฑ๋ HTML ๋ฌธ์์ด์ ์ฃผ๋ ๊ฒ ์๋๋๋ค. HTML์ SSR์ ์ญํ ์ด๊ณ ์, RSC๋ "Flight" ๋ผ๊ณ ๋ถ๋ฆฌ๋ ํน๋ณํ ํฌ๋งท์ ๋ณด๋ ๋๋ค.
1:I["./src/components/ClientCounter.js",["234","345"],"ClientCounter"] 0:["$","div",null,{"children":[["$","h1",null,{"children":"Hello World"}],["$","$L1",null,{}]]}]
์ด๋ฐ ์์ ๋ฐ์ดํฐ ๋ฉ์ด๋ฆฌ๊ฐ ๋ ๋ผ์ต๋๋ค. React Element ํธ๋ฆฌ๋ฅผ ํ ์คํธ๋ก ์ง๋ ฌํ(Serialize)ํ ๊ฑฐ์ฃ .
- ์ผ๋ฐ HTML ํ๊ทธ(
div,h1)๋ ๊ทธ๋๋ก ๋ค์ด์๊ณ , - Client Component๊ฐ ํ์ํ ์๋ฆฌ์๋ "์ฌ๊ธฐ์
ClientCounter.js๊ฐ๋ค ๋ถ์ฌ!"๋ผ๋ ์ฐธ์กฐ(Reference)๋ง ๋จ๊ฒจ๋ก๋๋ค.
์ค์ํ ๊ฑด Server Component์ ์ฝ๋๋ ๋ธ๋ผ์ฐ์ ๋ก ์ ์ก๋์ง ์๋๋ค๋ ์ ์
๋๋ค.
๋ง์ฝ ์๋ฒ ์ปดํฌ๋ํธ์์ ๋ ์ง ํฌ๋งทํ
์ ์ํด ๋ฌด๊ฑฐ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ผ๋ค๊ณ ์นฉ์๋ค. ๋ธ๋ผ์ฐ์ ๋ ๊ฒฐ๊ณผ ๊ฐ์ธ "2025๋
12์ 9์ผ"์ด๋ผ๋ ํ
์คํธ๋ง ๋ฐ์ต๋๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฉ๋์ ๋ฒ๋ค์ ํฌํจ๋์ง ์์ต๋๋ค. Zero Bundle Size์ ๊ฟ์ด ์คํ๋๋ ์๊ฐ์ด์ฃ .
4. 'use client'๋ ํด๋ผ์ด์ธํธ ์ ์ฉ์ด ์๋๋ค
RSC๋ ์๋ฒ ์ ์ฉ์ด๋ผ ์ธํฐ๋์
(ํด๋ฆญ, ์
๋ ฅ ๋ฑ)์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค. ์ธํฐ๋์
์ด ํ์ํ๋ฉด "use client" ๋ฅผ ์ ์ธํด์ Client Component๋ก ์ ํํด์ผ ํฉ๋๋ค.
// src/components/LikeButton.tsx 'use client'; // "์ด๋ณด๊ฒ, ์ฌ๊ธฐ๋ถํฐ๋ React๊ฐ ๋ธ๋ผ์ฐ์ ์์ ๋๊ฒ ํด์ฃผ๊ฒ" import { useState } from 'react'; export default function LikeButton() { const [likes, setLikes] = useState(0); return <button onClick={() => setLikes(likes + 1)}>์ข์์ {likes}</button>; }
์คํดํ์๋ฉด ์ ๋๋ ๊ฒ, "use client"๋ฅผ ๋ถ์ธ๋ค๊ณ ํด์ "CSR(Client Side Rendering)"๋ง ํ๋ ๊ฒ ์๋๋๋ค. ์ด ์ปดํฌ๋ํธ๋ค๋ ์๋ฒ์์ ๋ฏธ๋ฆฌ HTML๋ก ๋ ๋๋ง(SSR) ๋์ด์ ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ํ๋ณดํฉ๋๋ค. ๋จ์ง, ๋ธ๋ผ์ฐ์ ์์ JS๊ฐ ๋ถ์ด์(Hydration) ์์ง์ผ ์ ์๊ฒ ๋๋ค๋ ๋ป์
๋๋ค.
์ปดํฌ๋ํธ ์กฐํฉ์ ๋ฌ๋ฏธ (Composition)
"๊ทธ๋ผ ์ต์์์ Context Provider ๊ฐ์ธ๋ ค๋ฉด "use client" ๋ถ์ฌ์ผ ํ๋๋ฐ, ๊ทธ๋ผ ํ์ ์ปดํฌ๋ํธ๋ค๋ ๋ค ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ๋๋ ๊ฑฐ ์๋?"
์๋๋๋ค. Children ํจํด์ ์ฐ๋ฉด ๋ฉ๋๋ค.
// app/page.tsx (Server Component) import ClientLayout from './ClientLayout'; // ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ import ServerPostList from './ServerPostList'; // ์๋ฒ ์ปดํฌ๋ํธ export default function Page() { return ( // Server Component๋ฅผ Client Component์ ์์์ผ๋ก ๋๊น๋๋ค! <ClientLayout> <ServerPostList /> </ClientLayout> ); }
// app/ClientLayout.tsx (Client Component) 'use client'; export default function ClientLayout({ children }) { // children์ผ๋ก ๋ค์ด์จ ServerPostList๋ ์ด๋ฏธ ์๋ฒ์์ ๋ค ์ฒ๋ฆฌ๋ ๊ฒฐ๊ณผ๋ฌผ(์ฌ๋กฏ)์ ๋๋ค. // ClientLayout ์ ์ฅ์์๋ ๊ทธ์ "๋ญ์ง ๋ชจ๋ฅผ ๋ฆฌ์กํธ ๋ ธ๋"์ผ ๋ฟ์ด์ฃ . return <div className="dark-theme">{children}</div>; }
์ด๋ ๊ฒ ํ๋ฉด ServerPostList๋ ์ฌ์ ํ ์๋ฒ ์ปดํฌ๋ํธ๋ก ๋จ์์ DB์ ์ง์ ์ ๊ทผํ ์ ์๊ณ , ClientLayout์ ๋ธ๋ผ์ฐ์ ์์ ์ํ ๊ด๋ฆฌ๋ฅผ ํ ์ ์์ต๋๋ค. ์ด ํฉ์ฑ(Composition) ํจํด์ด RSC ์ํคํ
์ฒ์ ํต์ฌ์
๋๋ค.
5. ๊ฒฐ๋ก : useEffect๋ ์ด์ ๋ณด๋ด์ฃผ์
์ฐ๋ฆฌ๊ฐ ๊ทธ๋์ useEffect๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋ ๊ฑด, React ๋ ๋๋ง ์ฌ์ดํด ์ค์์ ๋ถ์ ํจ๊ณผ(Side Effect)๋ฅผ ์ฒ๋ฆฌํ ์ ์ผํ ํ์ด๋ฐ์ด ๊ทธ๋์๊ธฐ ๋๋ฌธ์
๋๋ค. ์ด์ฉ ์ ์๋ ์ ํ์ด์์ฃ .
ํ์ง๋ง ๋จ์ ์ด ๋๋ฌด ์ปธ์ต๋๋ค.
- ๋๋ฆฝ๋๋ค. (๋ ๋๋ง โ ๋ง์ดํธ โ ์ดํํธ ์คํ โ ํจ์นญ ์์)
- ๋ณต์กํฉ๋๋ค. (๋ก๋ฉ ์ํ ๊ด๋ฆฌ, ๋ ์ด์ค ์ปจ๋์ ์ฒ๋ฆฌ, ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง...)
์ด์ RSC์์๋ ๋ ๋๋ง ์ค์ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ค๋ฆฝ๋๋ค.
const data = await getData(); // ๊ธฐ๋ค๋ ค! return <View data={data} />; // ๋ค ์๋ค? ๊ทธ๋ ค!
์ฝ๋๊ฐ ๋๊ธฐ(Synchronous) ์ฝ๋์ฒ๋ผ ์ง๊ด์ ์ผ๋ก ๋ณํฉ๋๋ค. ๋ฐ์ดํฐ๊ฐ ์๋ค๋ ๊ฒ ๋ณด์ฅ๋๋ ์ต์
๋ ์ฒด์ด๋(?.)์ ๋จ๋ฐํ ํ์๋ ์ค์ด๋ญ๋๋ค.
๋ฌผ๋ก ์น์์ผ ์ฐ๊ฒฐ์ด๋ ์๋์ฐ ๋ฆฌ์ฌ์ด์ฆ ์ด๋ฒคํธ ๊ฐ์ ๊ฑด ์ฌ์ ํ useEffect๊ฐ ํ์ํฉ๋๋ค. ํ์ง๋ง "ํ์ด์ง ๋ก๋ฉํ์๋ง์ API ์ฐ๋ฌ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ" ์ฉ๋๋ก๋ ์ด์ ์ํดํ ์๊ฐ์
๋๋ค.
Next.js App Router๋ก ๋์ด๊ฐ๋ฉด์ ๋ฉํ ๋ถ๊ดด๋ฅผ ๊ฒช๊ณ ๊ณ์ ๊ฐ์?
๊ด์ ์ ๋ฐ๊ฟ๋ณด์ธ์. "ํด๋ผ์ด์ธํธ์์ ์๋ฒ ํ๋ด๋ฅผ ๋ด๋ ์ง"์ ๋ด๋ ค๋๊ณ , "์ง์ง ์๋ฒ"์๊ฒ ์ผ์ ๋งก๊ธฐ๋ ๊ณผ์ ์ด๋ผ๊ณ ์.
๋ค์ ํ๋ก์ ํธ์์๋ ์ต๊ด์ ์ผ๋ก useEffect๋ฅผ ์น๊ธฐ ์ ์ ํ๋ฒ ๋ฉ์นซํด๋ณด์ธ์.
"์ด๊ฑฐ... ๊ทธ๋ฅ ์๋ฒ์์ await ํ ์ค์ด๋ฉด ๋๋๋ ๊ฑฐ ์๋์ผ?"
Next.js App Router ๋์ ํ: ํ ๋ฒ์ ๋ชจ๋ ๊ฑธ ๋ฐ๊พธ๋ ค ํ์ง ๋ง์ธ์. ๋ฒํผ, ์ ๋ ฅ์ฐฝ ๊ฐ์ ์์ 'Leaf Component'๋ถํฐ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ก ๋ง๋ค๊ณ , ํ์ด์ง ์ ์ฒด ๊ตฌ์กฐ๋ ์๋ฒ ์ปดํฌ๋ํธ๋ก ์ ์งํ๋ '๋ฐ๊นฅ์์ ์์ผ๋ก' ์ ๋ต์ด ์ ํจํฉ๋๋ค.