[React] ๋ฆฌ์กํธ Suspense ํบ์๋ณด๊ธฐ
React v18 ๋ฒ์ ๋ถํฐ Suspense๋ฅผ ๊ณต์์ ์ผ๋ก ์ง์ํ๋ค. Suspense๋ ์ปดํฌ๋ํธ ๋ ๋๋ง์ ํ์ํ ํน์ ์์ ์ด ์๋ฃ๋์ง ์์์์ React์๊ฒ ์๋ ค์ฃผ๋ ๋งค์ปค๋์ฆ์ด๋ค. ์ด ํน์ ์์ ์ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๊ฒ ์ง๋ง Data Fetching ๊ฐ์ ๋น๋๊ธฐ ์์ ์ธ ๊ฒฝ์ฐ๊ฐ ๊ฐ์ฅ ๋ง๋ค. Suspense๋ฅผ ์ด์ฉํ๋ฉด, ๋ฐ์ดํฐ๋ฅผ ๋ค ๋ถ๋ฌ์ค์ง ๋ชปํ ์ปดํฌ๋ํธ์ ๋ ๋๋ง์ ์ ์ ์ค๋จ์ํค๊ณ Loading ํ๋ฉด ๊ฐ์ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ฅผ ๋จผ์ ๋ณด์ฌ์ฃผ๋๋ก ํ ์ ์๋ค.
๐ก Suspense๋ React Query, SWR ๊ฐ์ Data Fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํจ๊ป ์ฌ์ฉํ ์ ์๋๋ก ์ค๊ณ๋๋ค. Data Fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ต์
์ธ์๋ฅผ ์ ๋ฌํ๋ ๋ฐฉ์์ผ๋ก Suspense ๊ธฐ๋ฅ์ ํ์ฑํ ํ ์ ์๋ค. ์๋ฅผ๋ค์ด useSWR
์์ ์ธ๋ฒ์งธ ์ธ์์ { suspense: true }
๋ฅผ ๋๊ธฐ๋ฉด ๋๋ค.
Suspense ์ฅ์
Suspense๋ฅผ ์ฌ์ฉํ๋ฉด ์ํฐํด(Waterfall), ๊ฒฝ์ ์ํ(Race Condition)๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
์ํฐํด(Warterfall) ๋ฌธ์ ํด๊ฒฐ
Data Fetching์์ ์ํฐํด ํ์์ ์ด์ Fetch ์์ฒญ์ ๋ํ ์๋ต์ด ๋์ฐฉํด์ผ ๋ค์ Fetch ์์ฒญ์ ๋ณด๋ผ ์ ์๋ ์ํฉ์ ๋งํ๋ค. User
์ปดํฌ๋ํธ์์ ์ด๋ฆ, ์ฃผ์, ์ด๋ฉ์ผ ๊ฐ์ ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์จ ํ ๋ ๋๋งํ๊ณ , Posts
์ปดํฌ๋ํธ์์ ํด๋น ์ ์ (ID)์ ๋ํ ํฌ์คํ
์ ๋ณด๋ฅผ ์์ฒญํ๋ ์ํฉ์ ์๋ก๋ค ์ ์๋ค.
function User({ userId }) {
const [loading, setLoading] = useState(false);
const [user, setUser] = useState([]);
useEffect(() => {
// ...Fetch user data, update user state
}, []);
if (loading) return <p>Loading user...</p>;
return (
<div>
<p>{user.name} ๋์ด ์์ฑํ ๊ธ</p>
<Posts userId={userId} />
</div>
);
}
function Posts({ userId }) {
const [loading, setLoading] = useState(false);
const [posts, setPosts] = useState([]);
useEffect(() => {
// ...Fetch posts data, update posts state
}, []);
if (loading) return <p>Loading posts...</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
- (
User
์ปดํฌ๋ํธ) ์ ์ ๋ฐ์ดํฐ ์์ฒญ → ์์ฒญ์ค์ ๋ก๋ฉ ํ๋ฉด ํ์ — 2์ด ์์ - (
User
์ปดํฌ๋ํธ) ๋ฐ์ดํฐ ์๋ต → ์ ์ ์ ๋ณด ๋ ๋ → Posts ์ปดํฌ๋ํธ๋กuserId
์ ๋ฌ - (
Posts
์ปดํฌ๋ํธ)userId
์ ๋ํ ํฌ์คํ ๋ฐ์ดํฐ ์์ฒญ → ์์ฒญ์ค์ ๋ก๋ฉ ํ๋ฉด ํ์ — 3์ด ์์ - (
Posts
์ปดํฌ๋ํธ) ๋ฐ์ดํฐ ์๋ต → ํฌ์คํ ๋ชฉ๋ก ๋ ๋
User
์ปดํฌ๋ํธ์์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ ์์ ์ผ๋ก ๋ฐ์์์ผ๋ง Posts
์ปดํฌ๋ํธ๊ฐ ํด๋น ์ ์ ์ ๋ํ ํฌ์คํ
์ ๋ณด๋ฅผ ์์ฒญํ ์ ์๋ค. ํฌ์คํ
๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋๋ด 3์ด๊ฐ ์์๋์ง๋ง User
์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ ์์ฒญ ๋๋ฌธ์ 2์ด๋ฅผ ๋ ๊ธฐ๋ค๋ ค์ผ ํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ์ด๋ฐ ํ์์ ์ํฐํด์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค.
Data Fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ ํธ๋ฆฌ ๊ตฌ์กฐ์ ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ ๋๋ง ์ด์ ์ ๋ฐ์์์ ์ํฐํด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค. ํ์ง๋ง ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ๋๊น์ง ํธ๋ฆฌ ๊ตฌ์กฐ์ ์๋ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ ์ ์๋, ๋ ๋ค๋ฅธ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
function fetchUser(userId) {
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
}
function fetchPosts(userId) {
return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
}
// Data Fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๊ณ ๊ฐ์
function fetchData(userId) {
return Promise.all([fetchUser(userId), fetchPosts(userId)])
.then((res) => Promise.all(res.map((r) => r.json())))
.then(([user, posts]) => ({ user, posts }));
}
// ์ปดํฌ๋ํธ ๋ ๋๋ง ์์ ์ ๋ฐ์ดํฐ ์์ฒญ
const promise = fetchData(1);
function User() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then((data) => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) return <p>Loading user...</p>;
return (
<div>
<p>{user.name} ๋์ด ์์ฑํ ๊ธ</p>
<Posts posts={posts} />
</div>
);
}
// ์์ ์ปดํฌ๋ํธ์์ ๋์ด์ ๋ฐ์ดํฐ ์์ฒญ์ ํ์ง ์์
function Posts({ posts }) {
if (posts === null) <p>Loading posts...</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
โญ๏ธ Suspense๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ ์์ฒญ ์งํ <Suspense>
์์ ํธ๋ฆฌ์ ์๋ ์ปดํฌ๋ํธ ๋ ๋๋ง์ ์์ํ๋ค. ๊ฐ ์ปดํฌ๋ํธ์ ์์ฒญ ๋ฆฌ์์ค๋ฅผ ์ฃผ์
ํ๋ค๋ฉด, ์์ฒญ ๋ฆฌ์์ค์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋์ง ์ง์์ ์ผ๋ก ํ์ธํ๋ค. ๋ฐ์ดํฐ๊ฐ ์์ ๋ ํด๋น ์ปดํฌ๋ํธ์ ๋ ๋๋ง์ ์ ์ ๋ฉ์ถ ํ ํธ๋ฆฌ์ ์๋ ๋ค๋ฅธ ์ปดํฌ๋ํธ์ ๋ ๋๋ง์ ์๋ํ๋ค.
ํธ๋ฆฌ์ ๋ ๋๋งํ ์ปดํฌ๋ํธ๊ฐ ๋จ์์์ง ์๊ณ ๋ฐ์ดํฐ๊ฐ ์์ด ๋ชจ๋ ์ปดํฌ๋ํธ๊ฐ ์ ์ง ์ํ๋ฉด, ๊ฐ์ฅ ๋ฐ๊นฅ Suspense(๊ฒฝ๊ณ)์ ์๋ Fallback(๋ก๋ฉ UI ๋ฑ)์ ์ฐพ์ ๋ณด์ฌ์ค๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ผ๋ฉด ์ปดํฌ๋ํธ์ ๋ฐ์ํ๊ณ Fallback์ ํด์ ๋๋ค.
// ๊ณต์ ๋ฌธ์ ์ฐธ๊ณ ํด์ ์ผ๋ถ ์์
// Suspense๋ฅผ ํตํฉํด์ ๋ง๋ ํน๋ณํ ๊ฐ์ฒด(Promise ์๋)
// { userId: 1, user: {read: ƒ}, posts: {read: ƒ} }
const resource = fetchData(1); // ๋ฐ์ดํฐ ์์ฒญ ์์
function Main() {
return (
<Suspense fallback={<p>Loading user...</p>}>
<User resource={resource} />
<Suspense fallback={<p>Loading posts...</p>}>
<Posts resource={resource} />
</Suspense>
</Suspense>
);
}
function User({ resource }) {
// read()๋ฅผ ์ง์์ ์ผ๋ก ํธ์ถํด์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์๋์ง ํ์ธ
// user ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ค๋ฉด ํด๋น ๋ผ์ธ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ์ผ์ ์ค์ง
const user = resource.user.read();
return (
<div>
<p>{user.name} ๋์ด ์์ฑํ ๊ธ</p>
</div>
);
}
function Posts({ resource }) {
// read()๋ฅผ ์ง์์ ์ผ๋ก ํธ์ถํด์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์๋์ง ํ์ธ
// posts ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ค๋ฉด ํด๋น ๋ผ์ธ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ์ผ์ ์ค์ง
const posts = resource.posts.read();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
์ด์ฒ๋ผ ์ปดํฌ๋ํธ ํธ๋ฆฌ์ ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ํ ๊ฐ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง์ ์์ํ๊ธฐ ๋๋ฌธ์ ์ํฐํด์ด ์๊ณ , ๋ฐ์ดํฐ ์๋ต์ ๋ฐ๊ธฐ ์ ๊น์ง ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋ง ํ ์ ์๋ ๋ฌธ์ ๋ ๋ฐ์ํ์ง ์๋๋ค.
๊ฒฝ์ ์ํ(Race Condition) ๋ฐฉ์ง
Suspense๋ฅผ ์ด์ฉํ๋ฉด Data Fetching์ ์ํ ๊ฒฝ์ ์ํ(Race Condition)๋ฅผ ๋ฐฉ์งํ ์ ์๋ค. ์ํค๋ฐฑ๊ณผ์ ๋ฐ๋ฅด๋ฉด ๊ฒฝ์ ์ํ๋ “ํน์ ์์์ ์ฌ๋ฌ ํ๋ก์ธ์ค๊ฐ ๋์์ ์ ๊ทผํ๋ ค๊ณ ํ ๋ ํ์ด๋ฐ์ด๋ ์์ ๋ฑ์ด ๊ฒฐ๊ณผ์ ์ํฅ์ ์ค ์ ์๋ ๊ฒ”์ด๋ผ๊ณ ๋์์๋ค. ์๋ฐ์คํฌ๋ฆฝํธ์์ ๊ฒฝ์ ์ํ๋ ์ฌ๋ฌ๊ฐ์ ๋น๋๊ธฐ ์์ (Data Fetching ๋ฑ) ๊ฒฐ๊ณผ๊ฐ ํ๋์ DOM ๊ฐ์ฒด์ ๋ฐ์๋๋ ์ํฉ์ ์๋ฏธํ๋ค.
๊ฒฝ์ ์ํ ์์ — ๋ค์์ ๋น๋๊ธฐ ์์ (์ ์ ๋ฐ์ดํฐ)์ด ํ๋์ DOM(์ ์ ์ ๋ณด ์ปดํฌ๋ํธ)์ ๋ฐ์ํ๋ ์ํฉ โผ
์ ์์ ์์ ๊ฐ ๋ฒํผ์ ๋๋ฅด๋ฉด Fake API ์์ฒญ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ ํ ํ๋์ ๊ณตํต ์ปดํฌ๋ํธ(USER INFO)์ ๋ฐ์ํ๊ณ ์๋ค. ์ฌ๋ฌ ๋ฒํผ์ ์ฐ์ํด์ ๋๋ ๋ค๋ฉด ์ผ๋ฐ์ ์ผ๋ก ๋ง์ง๋ง์ ๋๋ฅธ ๋ฒํผ์ ์ ๋ณด๊ฐ ๋ฐ์๋ ๊ฒ์ด๋ผ๊ณ ๊ฐ์ ํ๋ค. ํ์ง๋ง ๊ผญ ๊ทธ๋ ์ง๋ง์ ์๋ค. A
→ B
์์๋ก ์์ฒญ์ ๋ณด๋์ง๋ง B
→ A
์์๋ก ์๋ต์ด ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ด์ฒ๋ผ ๊ฒฝ์ ์ํ๋ ์ฝ๋๊ฐ ์คํ๋๋ ์์๋ฅผ ์๋ชป ๊ฐ์ ํด์ ๋ฐ์ํ๋ ๋ฒ๊ทธ๋ค.
React์์ useEffect
์ข
์์ฑ ๋ฐฐ์ด์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค, ๋ฐ๋ ๊ฐ์ ๋ํ API ์์ฒญ์ ๋ณด๋ผ ๋ ๊ฒฝ์ ์ํ๊ฐ ๋ฐ์ํ ์ ์๋ค(์ข
์์ฑ ๋ฐฐ์ด ๊ฐ์ด ๋จ์๊ฐ๋ด ์ฌ๋ฌ๋ฒ ๋ณ๊ฒฝ๋๋ ๊ฒฝ์ฐ). Axios์ ์ทจ์ ํ ํฐ(Cancel Token) ๋ฑ์ ์ฌ์ฉํด ์ด์ ์์ฒญ์ ์ทจ์ํ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ ์ ์์ง๋ง ์ง๊ด์ ์ด์ง ์๊ณ ๋๋ฒ๊ทธํ๊ธฐ๋ ์ฝ์ง ์๋ค.
// ์ ์ฌ์ ์ธ ๊ฒฝ์ ์ํ๋ฅผ ์ ๋ฐํ๋ ์ฝ๋ ์์
useEffect(() => {
fetchUser(id).then((user) => setUser(user));
}, [id]);
์์ฒญ ๋ฆฌ์์ค๋ฅผ State๋ก ๊ด๋ฆฌํ๊ณ Suspense๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฒฝ์ ์ํ๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
๐ก ref
๊ฐ์ ๋ณ๊ฒฝํด๋ ์ปดํฌ๋ํธ๋ ๋ฆฌ๋ ๋๋ง๋์ง ์๋๋ค. ํ์ง๋ง changeId
ํจ์์์ ref
๊ฐ์ ๋ณ๊ฒฝํ ๋ resource
์ํ๋ ์
๋ฐ์ดํธํ๋ฏ๋ก ํญ์ ๋ณ๊ฒฝ๋ ref
๊ฐ์ ๋ ๋๋ง ํ๋ค. ์ฐธ๊ณ ๋ก ๋ณ์++
ํ์ ์ฆ๊ฐ ์ฐ์ฐ์๋ ์ฆ๊ฐ๋๊ธฐ ์ด์ ์ ๊ฐ์ ๋ฐํํ๋ค(fetchData
ํ๋ผ๋ฏธํฐ์ ์ฆ๊ฐ๋๊ธฐ ์ ๊ฐ์ด ์ ๋ฌ๋จ).
// Suspense๋ฅผ ์ด์ฉํด ๊ฒฝ์ ์ํ๋ฅผ ํด๊ฒฐํ ์์ (๊ณต์ ๋ฌธ์ ์ฐธ๊ณ ํด์ ์ผ๋ถ ์ฝ๋ ์์ )
// { userId: 1, user: {read: ƒ}, posts: {read: ƒ} }
const initialResource = fetchData(1); // ๋ฐ์ดํฐ ์์ฒญ ์์
function Main() {
const [resource, setResource] = useState(initialResource);
const nextId = useRef(resource.userId + 1);
const changeId = useCallback(() => {
setResource(fetchData(nextId.current++)); // ref ๊ฐ ๋ณ๊ฒฝ → data ์์ฒญ → state ์
๋ฐ์ดํธ
}, []);
return (
<>
<button onClick={changeId}>Next User</button>
<Suspense fallback={<h1>Loading user...</h1>}>
<User resource={resource} />
</Suspense>
</>
);
}
function User({ resource }) {
// read()๋ฅผ ์ง์์ ์ผ๋ก ํธ์ถํด์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์๋์ง ํ์ธ
// user ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ค๋ฉด ํด๋น ๋ผ์ธ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ์ผ์์ ์ง
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
fetchData
ํจ์๋ User
, Posts
์ปดํฌ๋ํธ์์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ user
, posts
์์ฑ์ ํฌํจํ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค. ๊ฐ ์์ฑ์ ์๋ read()
ํจ์๋ ๋ฐ์ดํฐ ์์ฒญ์ค์ผ ๋ suspense
๋ณ์(API ํธ์ถ ํ๋ก๋ฏธ์ค)๋ฅผ ๋ฐํํ๊ณ , ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ค. ์ค์ ๊ตฌํ์ ๋ ๋ณต์กํ๊ฒ ์ง๋ง Relay ๊ฐ์ ๋ฐ์ดํฐ Fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ตฌํ์ ์๋์ ๋น์ทํ ๊ตฌ์กฐ๋ก ๋์ด ์๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
read()
ํจ์๊ฐ ์ฝ์ ๊ฒฐ๊ณผ ๊ฐ์ ๋ฐ๋ผ ์ด๋ค UI๋ฅผ ๋ณด์ฌ์ค์ง ๊ฒฐ์ ํ๋ ๊ตฌ์กฐ — ์๋ ์๋ฆฌ๋ ๋งํฌ ์ฐธ๊ณ
throw suspender
:read()
ํจ์ ํธ์ถ ๊ณ์ /<Suspense>
์ Fallback UI ํ์- (์๋ฌ)
throw result
:read()
ํจ์ ํธ์ถ ์ ์ง /<ErrorBoundary>
์ Fallback UI ํ์ - (์ฑ๊ณต)
throw result
:read()
ํจ์ ํธ์ถ ์ ์ง / ์ปดํฌ๋ํธ์ ์๋ต ๋ฐ์ดํฐ ๋ฐ์
import axios from 'axios';
// ๊ณต์ ๋ฌธ์ ์ฐธ๊ณ ํด์ ์ผ๋ถ ์ฝ๋ ์์
export default function fetchData(userId) {
const userPromise = fetchUser(userId); // ํ๋ก๋ฏธ์ค ๋ฆฌํด
const postsPromise = fetchPosts(userId); // ํ๋ก๋ฏธ์ค ๋ฆฌํด
return {
userId,
user: wrapPromise(userPromise),
posts: wrapPromise(postsPromise),
}; // { userId: 1, user: {read: ƒ}, posts: {read: ƒ} }
}
// Suspense integrations like Relay implement
// a contract like this to integrate with React.
// Real implementations can be significantly more complex.
// Don't copy-paste this into your project!
function wrapPromise(promise) {
let status = 'pending';
let result;
const suspender = promise.then(
(r) => {
status = 'success';
result = r.data;
},
(e) => {
status = 'error';
result = e;
},
);
return {
// ํจ์ ๋ฐ์์ ํ๋ก๋ฏธ์ค(suspender)์ ์งํ ์ํฉ์ ์กฐํํ๋ ์ธํฐํ์ด์ค ์ญํ (ํด๋ก์ ํ์ฉ)
read() {
if (status === 'pending') {
throw suspender; // read() ํธ์ถ ๊ณ์ / <Suspense>์ Fallback UI ํ์
} else if (status === 'error') {
throw result; // read() ํธ์ถ ์ ์ง / <ErrorBoundary>์ Fallback UI ํ์
} else if (status === 'success') {
return result; // read() ํธ์ถ ์ ์ง / ์ปดํฌ๋ํธ์ ์๋ต ๋ฐ์ดํฐ ๋ฐ์
}
},
};
}
const instance = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
});
function fetchUser(userId) {
return instance({ url: `/users/${userId}` });
}
function fetchPosts(userId) {
return instance({ url: `/posts?userId=${userId}` });
}
๋ฒํผ์ ํด๋ฆญํ ๋๋ง๋ค ์๋ก์ด userId
์ ๋ํ ์ ๋ณด๋ฅผ ์์ฒญํ๋๋ฐ, ์์ฒญ์ ๋ณด๋๊ณผ ๋์์ resource
์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ ๊ฒ์ ์ฃผ๋ชฉํ์. ์
๋ฐ์ดํธ๋ resource
๋ <Suspense>
์์ ํธ๋ฆฌ์ ์๋ ์ปดํฌ๋ํธ์ ์ฃผ์
๋๊ณ resource
๊ฐ ๋ฐ์ดํฐ๋ฅผ ํ๋ณดํ๋ ์ฆ์ ์ปดํฌ๋ํธ์ ๋ฐ์๋๋ค.
๊ฒฝ์ ์ํ๊ฐ ์กด์ฌํ ์ฝ๋์์ ์์ฒญ
→ ์๋ต
→ ์ํ ๋ณ๊ฒฝ
์์๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ์ ์ ํ ์์ ์ State๋ฅผ ์ค์ ํ์ง ์์ผ๋ฉด ์ฝ๋๊ฐ ์ ๋๋ก ์๋ํ์ง ์๋๋ค. ๋ฐ๋ฉด Suspense๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฒญ
→ ์ํ ๋ณ๊ฒฝ
→ ์๋ต
์์๋ก ๋์ํ๋ฏ๋ก ์ํ ๋ณ๊ฒฝ ์์ ์ ๊ณ ๋ ค๋ฅผ ํ์ง ์์๋ ๋๊ณ ๊ฒฝ์ ์ํ๋ ๋ฐ์ํ์ง ์๋๋ค.
Suspense ์์
User
, Posts
์ปดํฌ๋ํธ๋ฅผ Suspense
๋ก ๊ฐ์ธ๊ณ ๊ฐ ์ปดํฌ๋ํธ์ ์์ฒญ resource
(fetchData
ํจ์๊ฐ ๋ฐํํ ๊ฐ์ฒด; ํจ์ ํธ์ถ ์ ๋ฐ์ดํฐ ์์ฒญ ์์)๋ฅผ ๋๊ธด๋ค. ๋ฐ์ดํฐ๋ฅผ ์์ฒญ์ค์ผ ๋ Suspense
์ Fallback์ ํ์ํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ผ๋ฉด Fallback์ ํด์ ํ๋ค. if (...)
๊ตฌ๋ฌธ ํน์ ์์ ์ผํญ ์ฐ์ฐ์๋ฅผ ์ด์ฉํด ๋ก๋ฉ UI๋ฅผ ๋ณด์ฌ์ฃผ๋ ์กฐ๊ฑด์ ์์ฑํ์ง ์์๋ ๋๊ธฐ ๋๋ฌธ์ React์ ๋ ์ ์ด์ธ๋ฆฌ๋ “์ ์ธ์ "์ธ ์ฝ๋๊ฐ ๋๋ค.
๐ก Suspense๋ก ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๋ฉด ํด๋น ์ปดํฌ๋ํธ์ ๊ณ ์ ํ ๊ฒฝ๊ณ๊ฐ ์๊ธด๋ค. Posts๋ณด๋ค User ๋ฐ์ดํฐ๋ฅผ ๋จผ์ ์๋ต ๋ฐ์๋ค๋ฉด User ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๊ณ ์๋ ๋ฐ๊นฅ Suspense ๊ฒฝ๊ณ๋ ํด์ ๋๊ณ User ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ค. User์ Posts ์ปดํฌ๋ํธ๋ฅผ ๋์์ ๋ณด์ฌ์ฃผ๊ณ ์ถ์ผ๋ฉด ๋ ์ฌ์ด์ Suspense ๊ฒฝ๊ณ๋ฅผ ์ ๊ฑฐํ๋ฉด ๋๋ค(Posts๋ฅผ ๊ฐ์ธ๊ณ ์๋ Suspense ์ ๊ฑฐ).
import React, { Suspense, useCallback, useState } from 'react';
import fetchData from './fetchData';
export default function Main() {
const [id, setId] = useState(1);
const resource = fetchData(id); // { userId: 1, user: {read: ƒ}, posts: {read: ƒ} }
const changeId = useCallback(() => {
setId((prev) => prev + 1);
}, []);
return (
<div>
<Suspense fallback={<Loading type="user" />}>
<User resource={resource} />
<Suspense fallback={<Loading type="posts" />}>
<Posts resource={resource} />
</Suspense>
</Suspense>
<button type="button" onClick={changeId}>
{`Next ID (${id + 1})`}
</button>
</div>
);
}
์์ฒญ resource
๊ฐ ์ฃผ์
๋ ์ปดํฌ๋ํธ์์ read()
ํจ์๋ฅผ ํตํด ์ฃผ๊ธฐ์ ์ผ๋ก ์๋ต ๋ฐ์ดํฐ๊ฐ ๋ค์ด์๋์ง ํ์ธํ๋ค(read
ํจ์ ๋ด๋ถ์ ์ฝ์์ ์ฐ์ด๋ณด๋ฉด ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ธฐ์ ๊น์ง ์์์ด ํธ์ถ๋๋๊ฑธ ํ์ธํ ์ ์๋ค). ์์ง ๋ฐ์ดํฐ ์๋ต์ ๋ฐ์ง ์์์ผ๋ฉด ์ปดํฌ๋ํธ ๋ ๋๋ง์ ์ผ์ ์ ์งํ๊ณ , Suspense ํธ๋ฆฌ์ ์๋ ๋ค๋ฅธ ์ปดํฌ๋ํธ์ ๋ ๋๋ง์ ์๋ํ๋ค.
ํธ๋ฆฌ์ ๋ ๋๋งํ ์ปดํฌ๋ํธ๊ฐ ๋จ์์์ง ์๊ณ ๋ฐ์ดํฐ๊ฐ ์์ด ๋ชจ๋ ์ปดํฌ๋ํธ๊ฐ ์ ์ง ์ํ๋ฉด, ๊ฐ์ฅ ๋ฐ๊นฅ Suspense(๊ฒฝ๊ณ)์ ์๋ Fallback(๋ก๋ฉ UI ๋ฑ)์ ์ฐพ์ ๋ณด์ฌ์ค๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ผ๋ฉด ์ปดํฌ๋ํธ์ ๋ฐ์ํ๊ณ Fallback์ ํด์ ๋๋ค.
๐ก read()
ํจ์ ํธ์ถ์ API ์์ฒญ์ด ์๋, ์๋ต ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ์์
์ ์ํํ๋ ์ ์ฐธ๊ณ . ๊ฒฐ๊ณผ์ ์ผ๋ก read()
ํจ์ ํธ์ถ์ ์๋ต ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ค๊ฑฐ๋, ์ปดํฌ๋ํธ๋ฅผ ์ ์ง์ํจ๋ค.
function User({ resource }) {
// read()๋ฅผ ์ง์์ ์ผ๋ก ํธ์ถํด์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์๋์ง ํ์ธ
// user ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ค๋ฉด ํด๋น ๋ผ์ธ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ์ผ์์ ์ง
const user = resource.user.read();
return <h1>{`Written by ${user.name} (${user.id})`}</h1>;
}
function Posts({ resource }) {
// read()๋ฅผ ์ง์์ ์ผ๋ก ํธ์ถํด์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์๋์ง ํ์ธ
// posts ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ค๋ฉด ํด๋น ๋ผ์ธ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ์ผ์์ ์ง
const posts = resource.posts.read();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
);
}
์๋ฌ ์ฒ๋ฆฌ
๐ก ErrorBoundary๋ ์์ ์ปดํฌ๋ํธ์์ ๋ฐ์ํ ์๋ฌ๋ง ๊ฒ์ถํ ์ ์๋ค. ์์ ์ ๋ํ ์ค๋ฅ๋ ๊ฒ์ถํ ์ ์๋ค.
What are error boundaries in React?
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed…
— React ๊ณต์ ๋ฌธ์
ํ๋ก๋ฏธ์ค์์ .catch
๋ฉ์๋๋ฅผ ์ด์ฉํด ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ฏ Suspense์์ ErrorBoundary
(์๋ฌ ๊ฒฝ๊ณ)๋ฅผ ์ฌ์ฉํด์ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ค. ErrorBoundary
๋ ํด๋์คํ ์ปดํฌ๋ํธ์ ์์ ์ปดํฌ๋ํธ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ getDerivedStateFromError
, componentDidCatch
๋ผ์ดํ์ฌ์ดํด ๋ฉ์๋๋ฅผ ์ด์ฉํด ์๋ฌ๋ฅผ ๊ฒ์ถํ๋ค. ํ์ง๋ง ํจ์ํ ์ปดํฌ๋ํธ์ ๋์ํ๋ ๋ฉ์๋๊ฐ ์๊ธฐ ๋๋ฌธ์ ํด๋์ค ์ปดํฌ๋ํธ๋ฅผ ์ด์ฉํด ๊ตฌํํด์ผ ํ๋ค.
getDerivedStateFromError ๋ฉ์๋ ํน์ง | ์ฐธ๊ณ
- ์์ ์ปดํฌ๋ํธ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ ์๋ช ์ฃผ๊ธฐ ๋ฉ์๋
- Render ๋จ๊ณ(๊ฐ์ DOM ์กฐ์ ๋จ๊ณ / ๋ถ์ํจ๊ณผ ๋นํ์ฉ)์์ ํธ์ถ
- ์ ๋ฐ์ดํธ๋ ์ํ(State) ๊ฐ์ ํ์์ ์ผ๋ก ๋ฐํํด์ผํจ
- ํ๋ผ๋ฏธํฐ : ์ค๋ฅ ๊ฐ์ฒด
componentDidCatch ๋ฉ์๋ ํน์ง | ์ฐธ๊ณ
- ์์ ์ปดํฌ๋ํธ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ ์๋ช ์ฃผ๊ธฐ ๋ฉ์๋
- Commit ๋จ๊ณ(๊ฐ์ DOM์ ์ค์ DOM์ ๋ฐ์ํ๊ณ ๋ผ์ดํ์ฌ์ดํด ์คํ / ๋ถ์ํจ๊ณผ ํ์ฉ)์์ ํธ์ถ
- ์ฃผ๋ก ์๋ฌ ๋ฆฌํฌํ ์๋น์ค์ ์๋ฌ๋ฅผ ๊ธฐ๋กํ ๋ ์ฌ์ฉ
- ํ๋ผ๋ฏธํฐ
- ์ฒซ๋ฒ์งธ : ์ค๋ฅ ๊ฐ์ฒด
- ๋๋ฒ์งธ : ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ปดํฌ๋ํธ์ ๋ํ ์ ๋ณด
import React from 'react';
// ์ฝ๋ ์ฐธ๊ณ - ๊ณต์ ๋ฌธ์
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// ์์ ์ปดํฌ๋ํธ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋
// render ๋จ๊ณ(๊ฐ์ DOM ์กฐ์ ๋จ๊ณ / ๋ถ์ํจ๊ณผ ๋นํ์ฉ)์์ ํธ์ถ
// ํ๋ผ๋ฏธํฐ๋ก ์ค๋ฅ ๊ฐ์ฒด๋ฅผ ์ ๋ฌ๋ฐ์ผ๋ฉฐ, ์
๋ฐ์ดํธ๋ ์ํ(State) ๊ฐ์ ํ์์ ์ผ๋ก ๋ฐํํด์ผํจ
static getDerivedStateFromError(error) {
// Fallback UI ๋ฅผ ํ์ํ ์ ์๋๋ก hasError ์ํ ์
๋ฐ์ดํธ
return { hasError: true };
}
render() {
if (this.state.hasError) {
// ์ํ๋ Fallback UI๋ฅผ ์ง์ ๋ ๋๋งํ ์๋ ์์ ex) return <h1>์๋ฌ ๋ฐ์!</h1>;
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
ErrorBoundary๋ ์ค๋ฅ๋ฅผ ๊ฒ์ถํ๊ณ ์ถ์ Suspense ํธ๋ฆฌ์ ์ถ๊ฐํ๋ค. ErrorBoundary์ Suspense๋ฅผ ์ฌ์ฉํ๋ฉด UI ๋ก์ง์ ๋์ฑ ์ง๊ด์ (์ ์ธ์ )์ผ๋ก ์์ฑํ ์ ์์ด์ ์ข๋ค.
export default function Main() {
// ...
return (
<div>
<ErrorBoundary fallback={<Error id={id} />}> {/* ์คํจ UI */}
<Suspense fallback={<Loading type="user" />}> {/* ๋ก๋ฉ UI */}
<User resource={resource} /> {/* ์ฑ๊ณต UI */}
<Suspense fallback={<Loading type="posts" />}> {/* ๋ก๋ฉ UI */}
<Posts resource={resource} /> {/* ์ฑ๊ณต UI */}
</Suspense>
</Suspense>
</ErrorBoundary>
{/* ... */}
</div>
);
}
// ์ฝ๋ ์ถ์ฒ - ๊ณต์ ๋ฌธ์
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</ErrorBoundary>
</Suspense>
);
}
์์ ๋ณด๊ธฐ
์ ์ / ํฌ์คํธ ๋ฐ์ดํฐ๋ JSON Placeholder์์ ๊ฐ์ ธ์ค๋ฉฐ, 11๋ฒ์งธ ID๋ถํด ๋ฐ์ดํฐ๊ฐ ์์ด์ ์๋ฌ UI๋ฅผ ํ์ํ๋ค.
๋ ํผ๋ฐ์ค
- ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ Suspense (์คํ ๋จ๊ณ) - React
- React Suspense ์๊ฐ (feat. React v18)
- ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ 1ํธ - react suspense
- Suspense for Data Fetching์ ์๋ ์๋ฆฌ์ ์ปจ์ (feat.๋์์ ํจ๊ณผ)
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Algorithm] ๋ถํ ์ ๋ณต / ๋ณํฉ ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ (0) | 2024.05.07 |
---|---|
[Algorithm] ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ ๊ธฐ๋ณธ (๋ฒ๋ธ/์ ํ/์ฝ์ ) (0) | 2024.05.06 |
[Git] ์๋ฉด ์ ์ฉํ GitHub ๋จ์ถํค / ํ (0) | 2024.05.05 |
[TS] ํ์ ์คํฌ๋ฆฝํธ ์ ์ฝ ์กฐ๊ฑด๊ณผ ์กฐ๊ฑด๋ถ ํ์ (0) | 2024.05.05 |
[HTML/CSS] select ํ๊ทธ์ ํ์ดํ ์์ด์ฝ ๋ณ๊ฒฝํ๊ธฐ (0) | 2024.05.05 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Algorithm] ๋ถํ ์ ๋ณต / ๋ณํฉ ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ
[Algorithm] ๋ถํ ์ ๋ณต / ๋ณํฉ ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ
2024.05.07 -
[Algorithm] ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ ๊ธฐ๋ณธ (๋ฒ๋ธ/์ ํ/์ฝ์ )
[Algorithm] ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ ๊ธฐ๋ณธ (๋ฒ๋ธ/์ ํ/์ฝ์ )
2024.05.06 -
[Git] ์๋ฉด ์ ์ฉํ GitHub ๋จ์ถํค / ํ
[Git] ์๋ฉด ์ ์ฉํ GitHub ๋จ์ถํค / ํ
2024.05.05 -
[TS] ํ์ ์คํฌ๋ฆฝํธ ์ ์ฝ ์กฐ๊ฑด๊ณผ ์กฐ๊ฑด๋ถ ํ์
[TS] ํ์ ์คํฌ๋ฆฝํธ ์ ์ฝ ์กฐ๊ฑด๊ณผ ์กฐ๊ฑด๋ถ ํ์
2024.05.05