dev-resources.site
for different kinds of informations.
How to used Suspense?
React 18๋ถํฐ Suspense๊ฐ API ์์ฒญ์ ๋ฐ๋ฅธ loading ์ํ๋ฅผ ํํํ ์ ์๊ฒ ๋์๋ค. ๊ทธ์ ๋ฐ๋ผ react-query, swr ๊ฐ์ data fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ญ์ Suspense๋ฅผ ์ง์ํ๊ณ ์๋ค. Suspense ์ต์ ๋ง true ๋ก ์ค์ ํด์ฃผ๋ฉด, API ์์ฒญ์ด ์์์ ๋ด๋ถ ์ฒ๋ฆฌ๋ฅผ ํตํด Suspense๋ฅผ ๋์์ํจ๋ค. ์ด๋ก์จ loading ์ํ๋ฅผ ์ ์ธ์ ์ผ๋ก ๋ณด์ฌ์ค ์ ์๊ฒ ๋์๋ค.
์ถ์ํ์ ๋ํ ํธ๋ฆฌํจ๊ณผ ์ฌ์ฉ๋ฒ์ ์ธ์งํ๊ณ ์์์ง๋ง, ์ถ์ํ์ ๊ฐ๋ ค์ง Suspense์ ๊น์ ๋์ ์๋ฆฌ์ ๋ํด์๋ ์ธ์งํ์ง ๋ชปํ์๋ค.
ํ๋ก์ ํธ๋ฅผ ํตํด Suspense ์ฌ์ฉ์ผ๋ก ๋ก๋ฉ ์ฑ๋ฅ์ด ์ ํ๋๋ ๊ฒฝํ์ ํตํด Suspense๊ฐ ์ด๋ป๊ฒ ๋ก๋ฉ ์ฑ๋ฅ์ ์ ํ ์ํฌ ์ ์๋์ง ์ด๋ฒ ๊ธ์ ํตํด ๊ทธ ๋ฌธ์ ์ ๊ณผ ํด๊ฒฐ์ฑ ์ ๊ณ ๋ฏผํด ๋ณด์๋ค.
data fetching ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก react-query๊ฐ ์ฌ์ฉ๋์๋ค.
์ ํํ ๋งํ๋ฉด data fetching์ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ๊ณ ๊ด๋ฆฌํ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ react-query๊ฐ ์ฌ์ฉ๋์๋ค.
ย
Use Suspense
function App() {
return (
<Suspense fallback={<div>...loading</div>}>
<TodoList />
</Suspense>
);
}
function TodoList() {
const { data: todoList } = useQuery("todos", () => client.get("/todos"), {
suspense: true,
});
return (
<div>
<section>
<h2>Todo List</h2>
{todoList?.data.map(({ id, title }) => (
<div key={id}>{title}</div>
))}
</section>
</div>
);
}
์์์์ Suspense๋ก ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ์ฃผ๊ณ , useQuery ์ต์ ์์ suspense:true๋ก ์ค์ ํด ์ฃผ๊ธฐ๋ง ํ๋ฉด fallback์ผ๋ก Loading ์ํ๋ฅผ ๋ ๋ ํ๋ค. TodoList ์์ API fetch๊ฐ ๋ฐ์ํ๋ ๋์ Loading fallback์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ด๋ค.
๊ทธ๋ผ Suspense๋ ์ด๋ป๊ฒ ๋์ํ๋ ๊ฒ์ธ๊ฐ?
Suspense๋ React์์ ๋ฐ์ดํฐ ๋ก๋ฉ ์ํ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋์ ๋ ๊ฐ๋ ์ผ๋ก, ์ฃผ๋ก ๋น๋๊ธฐ ์์ ๊ณผ ์ฐ๊ด๋์ด ์๋ค.
Suspense์ ๋์ ์๋ฆฌ๋ Promise์ ์ํ์ ๋ฐ์ ํ๊ฒ ๊ด๋ จ๋์ด ์์ผ๋ฉฐ, ์ด๋ฅผ ํตํด ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฅผ ์์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ค.
Pending ์ํ (๋๊ธฐ ์ค):
์ปดํฌ๋ํธ๊ฐ ๋น๋๊ธฐ ์์ ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋, ํด๋น Promise๋ ์ฒ์์ pending ์ํ๊ฐ ๋๋ค.
์ด ์์ ์์ Suspense๋ ๋ก๋ฉ ์ํ๋ฅผ ๊ฐ์งํ๊ณ ,
fallback
์ผ๋ก ์ง์ ๋ ๋ก๋ฉ UI๋ฅผ ํ์ํ๋ค.์๋ฅผ ๋ค์ด, ๋คํธ์ํฌ ์์ฒญ์ด ์๋ฃ๋ ๋๊น์ง ์คํผ๋(spinner)๋ ๋ก๋ฉ ๋ฉ์์ง๊ฐ ํ์๋ ์ ์๋ค.
Fulfilled ์ํ (์๋ฃ๋จ):
๋น๋๊ธฐ ์์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋๋ฉด Promise๋ fulfilled ์ํ๊ฐ ๋๋ค.
์ด ๋ Suspense๋ ๋ก๋ฉ UI๋ฅผ ์ ๊ฑฐํ๊ณ , ๋ฐ์ดํฐ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๊ฐ์ ธ์จ ์ปดํฌ๋ํธ๋ฅผ ํ๋ฉด์ ๋ ๋๋งํ๋ค.
์๋ฅผ ๋ค์ด, ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ฐ์์ ํ๋ฉด์ ๋ฐ์ดํฐ๋ฅผ ํ์ํ๋ค.
Rejected ์ํ (์คํจํจ):
๋น๋๊ธฐ ์์ ์ด ์คํจํ๋ฉด Promise๋ rejected ์ํ๊ฐ ๋๋ค.
์ด ๊ฒฝ์ฐ, Suspense ์์ฒด๋ ์คํจ ์ํ๋ฅผ ์ฒ๋ฆฌํ์ง ์์ง๋ง, ์ปดํฌ๋ํธ ๋ด๋ถ์์ ์๋ฌ ๊ฒฝ๊ณ(Error Boundary)๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ค.
์๋ฅผ ๋ค์ด, ๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์์ ๊ฒฝ์ฐ ์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ํ๋ค.
ย
Suspense ๋จ์ฉ์ ๋ฌธ์
์๋ ์์์์๋ Before ์ปดํฌ๋ํธ๋ฅผ Suspense๊ฐ ๊ฐ์ธ๊ณ ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ด๋ถ์์ 2๊ฐ์ Query๋ฅผ ์์ฒญํ๊ณ ์๋ค.
// App.jsx
function App() {
return (
<Suspense fallback={<div>...loading</div>}>
<Before />
</Suspense>
);
}
// Before.jsx
const BASE_URL = "https://jsonplaceholder.typicode.com";
const client = axios.create({ baseURL: BASE_URL });
function Before() {
const { data: todoList } = useQuery("todos", () => client.get("/todos"), {
suspense: true, // โจ
});
const { data: postList } = useQuery("posts", () => client.get("/posts"), {
suspense: true, // โจ
});
return (
<div style={{ display: "flex" }}>
<section>
<h2>Todo List</h2>
{todoList?.data.map(({ id, title }) => (
<div key={id}>{title}</div>
))}
</section>
<section>
<h2>Post List</h2>
{postList?.data.map(({ id, title }) => (
<div key={id}>{title}</div>
))}
</section>
</div>
);
}
API ์์ฒญ์ด ์ด๋ป๊ฒ ๊ฐ๊ณ ์๋์ง ๋คํธ์ํฌ ํญ์ ์ดํด๋ณด๋ฉด
network waterfall์ ๋ง๋ค๊ณ ์๋ค. ํด๋น ๋ฌธ์ ์ ์ ๊ฒช์ผ๋ฉด์ ์ฑ์ ๋ก๋ฉ ์๊ฐ์ด ๊ธธ์ด์ง๊ฒ ๋์๋ค.
์ ์ด๋ฐ ํ์์ด ๋ฐ์ํ์์๊น? ์์ธ์ ๋ฐ๋ก Suspense์ ์๋ชป๋ ์ฌ์ฉ, ์๋ ์ ๋ชจ๋ฅด๊ณ ์ฌ์ฉํ ์์ธ์ด ๋ ํฌ๋ค.
์์์ ์ค๋ช ํ ๋๋ก Suspense๋ Promise ์ํ์ ๋ฐ๋ผ์ children ๋๋ fallback ์ปดํฌ๋ํธ๋ฅผ ๋ฐํํ๋ค. ์ฆ, pending ์ํ์ผ ๋์๋ Loading์ ๋ฐํํ๊ณ ์๊ณ , children์ ์คํ์ํค์ง ์๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์, ํ๋์ API ์์ฒญ์ด ๋ฐ์ํ๋ฉด children ์ปดํฌ๋ํธ์ ์คํ์ ๋ฉ์ถ๊ณ fallback์ ๋ฐํํ๊ฒ ๋๋ค. Promise๊ฐ fulfilled ์ํ๊ฐ ๋๋ฉด ๋ค์ children์ ๋ฐํํ์ฌ children ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ค.
์ด๋ฌํ ์ด์ ๋๋ฌธ์ Suspense๊ฐ ๊ฐ์ธ๊ณ ์๋ ํ๋์ ์ปดํฌ๋ํธ์์ 2๊ฐ ์ด์์ ์์ฒญ์ ํ ๋ ๋คํธ์ํฌ ๋ณ๋ชฉ ํ์์ด ๋ฐ์ํ๊ฒ ๋๋ค.
ย
ํด๊ฒฐ ๋ฐฉ๋ฒ 1 - Component์ Suspense 1:1 ๋์
Suspense ๋ด์ ์ปดํฌ๋ํธ์์ ๋ ๊ฐ ์ด์์ ์์ฒญ์ด ๋ฐ์ํ๋ฉด ๋คํธ์ํฌ water fall์ด ์ง๋ ฌ๋ก ์ฒ๋ฆฌ๋๋ ํ์์ด ์๊ธด๋ค. ๊ทธ๋ ๋ค๋ฉด ํ ์ปดํฌ๋ํธ์ ํ๋์ ์์ฒญ์ ์ ์งํ๋ฉด ํด๊ฒฐ๋๋ค.
โจ ๋ ๊ฐ ์์ฒญ ๋ชจ๋ useQuery,
suspense: true
๋ก ์์ฒญ ์
์๋ ์ฝ๋๋ 2๊ฐ์ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๊ณ , ๊ฐ๊ฐ Query ์์ฒญ์ ์ํํ๊ณ , ๊ฐ๊ฐ Suspense๋ก ๊ฐ์ธ์ค ์ฝ๋์ด๋ค.
function AfterEachSuspense() {
return (
<div>
<div style={{ display: "flex" }}>
<section>
<h2>Todo List</h2>
<Suspense fallback={<div>...loading</div>}>
<TodoList />
</Suspense>
</section>
<section>
<h2>Post List</h2>
<Suspense fallback={<div>...loading</div>}>
<PostList />
</Suspense>
</section>
</div>
</div>
);
}
const TodoList = () => {
const { data: todoList } = useQuery("todos", () => client.get("/todos"), {
suspense: true, // โจ
});
return (
<div>
{todoList?.data.map(({ id, title }) => (
<div key={id}>{title}</div>
))}
</div>
);
};
const PostList = () => {
const { data: postList } = useQuery("posts", () => client.get("/posts"), {
suspense: true, // โจ
});
return (
<div>
{postList?.data.map(({ id, title }) => (
<div key={id}>{title}</div>
))}
</div>
);
};
๋คํธ์ํฌ ์์ฒญ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ๋ค.
๋ณ๋ชฉ ํ์์ ํด๊ฒฐํ๊ณ ์ ์์ ์ผ๋ก ๋ณ๋ ฌ ์ฒ๋ฆฌ๋๋ ๋ชจ์ต์ ํ์ธํ ์ ์๋ค.
Suspense๊ฐ ๋ถ๋ฆฌ๋์๊ณ , ๊ฐ๊ฐ์ Suspense๊ฐ ๊ฐ๊ฐ์ children์ ๊ด์ฅํ๋ฏ๋ก ๋ณ๋ ฌ์ ์ผ๋ก ์คํ๋๋ ๊ฒ์ด ๋น์ฐํ ๊ฒฐ๊ณผ๋ค.
ํ์ง๋ง ์ด ๋ฐฉ์์ ์ํฉ์ ๋ฐ๋ผ์ ๋ฌธ์ ๊ฐ ๋ ์ ์๋ค.
๋ฐ๋ก, ๋ก๋ฉ ์ํ๊ฐ ์ ๊ฐ๊ฐ ์ด๋ผ๋ ๊ฒ์ด๋ค.
์ ๋คํธ์ํฌ ํญ์์ todos์ posts ์์ฒญ์ ๋ณด๋ฉด ๋๋๋ ์๊ฐ์ด ๋ค๋ฅด๋ฉฐ, ๊ฐ๊ฐ์ Suspense ๋ ๊ฐ๊ฐ์ ๋คํธ์ํฌ ์์ฒญ์ด ๋๋๋ ๊ฒ์ ๊ฐ์งํ์ฌ children์ ๋ ๋๋ง ํ๋ค. ์ฆ, ์์ฒญ์ด ๋จผ์ ๋๋ ์ปดํฌ๋ํธ๊ฐ ๋จผ์ ๋ณด์ฌ์ง๊ฒ ๋๋ค.
TodoList๊ฐ ๋จผ์ ๋ ๋๋ง ๋๋ค๋ฉด ์ ์ ์๊ฒ ๋ฒ๋ฒ ๊ฑฐ๋ฆฌ๋ ๋๊น์ ์ค ์ ์๋ค๋ ์ ์์ ์ ์ ๊ฒฝํ์ ์ ํ์ํฌ ๊ฐ๋ฅ์ฑ๋ ์๋ค. ์ด๋ ์ํฉ์ ๋ฐ๋ผ์ ์ข์ ๊ฒฝํ์ผ ์ ์๊ณ , ์ข์ง ์์ ๊ฒฝํ์ผ ์๋ ์๋ค.
์ ๋ฆฌํ์๋ฉด, ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ฆฌํ ํ, ๊ฐ๊ฐ Suspense๋ฅผ ๊ฐ์ธ์ฃผ๋ ๊ฒ์ ์ ์ ๊ฒฝํ์ ์ ํ์ํฌ ๊ฐ๋ฅ์ฑ์ด ์๋ค. ๊ทธ๋ ๋ค๋ฉด ๋ ์ปดํฌ๋ํธ ๋ชจ๋ ๋ก๋ฉ์ด ๋๋๋ ์์ ์ ํ๋ฒ์ ๋ ๋๋ง ํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํ ์ ์๊ฒ ๋ค.
ย
ํด๊ฒฐ ๋ฐฉ๋ฒ 2 - Component์ Suspense n:1 ๋์
์์ ๊ฐ์ด ์ปดํฌ๋ํธ๋ก Query๋ฅผ ๊ฐ์ธ์ฌ ์์ฒญ์ ๋ถ๋ฆฌํ์์ง๋ง, Suspense๋ ํ๋๋ก ๊ฐ์ธ๊ณ ์๋ ์ ์ด ๋ค๋ฅด๋ค.
function AfterSameSuspense() {
return (
<div>
<Suspense fallback={<div>...loading</div>}>
<div style={{ display: "flex" }}>
<section>
<h2>Todo List</h2>
<TodoList />
</section>
<section>
<h2>Post List</h2>
<PostList />
</section>
</div>
</Suspense>
</div>
);
}
๋คํธ์ํฌ ์์ฒญ ๊ฒฐ๊ณผ๋ ๋์ผํ๊ฒ ๋ณ๋ ฌ ์ฒ๋ฆฌ๋๊ณ ์์ผ๋ฉฐ, ์์์ API call์ด ๋จผ์ ๋๋๋ ์ํฉ์ด๋ค.
์๋ํ ๋๋ก, ๋ ์์ฒญ์ด ๋ชจ๋ ๋๋ ๋๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ณ ๋ ๋๋ง์ด ์ํ๋๋ค.
์ด๋ Suspense ๋ด๋ถ ์ฝ๋๋ฅผ ์ดํด๋ด์ผ ์ ํํ ์๊ฒ ์ง๋ง, Suspense ๋ด๋ถ์์ API ์์ฒญ์ด ๋ฐ์ํ ์์ฌ๋ฅผ ํ์ ํ์ฌ, ๊ทธ๊ณณ์ fallback์ผ๋ก ๋์ฒดํ๊ณ , ๋๋จธ์ง ๋ถ๋ถ์ ์ ์์ ์ผ๋ก ์คํ์ํค๋ ๊ฒ์ ์ ์ ์๋ค.
ย
ํด๊ฒฐ ๋ฐฉ๋ฒ 3 - useQueries
์ฌ๋ฌ ๊ฐ์ query๋ฅผ ๋ณ๋ ฌ๋ก ์คํ์ํฌ ์ ์๊ฒ ํด์ฃผ๋ useQueries๋ Suspense์ ํจ๊ป ์ฌ์ฉ ์ ์ ์์ ์ผ๋ก ๋์ํ์ง ์๋ ์ด์๊ฐ ์๋๋ฐ v4.5.0 ํจ์น์ ํด๋น ์ฌํญ์ด ์์ ๋์๋ค.
const results = useQueries({queries: [
{queryKey: ["posts"], queryFn: () => fetch("/posts") , suspense: true},
{queryKey: ["comments"], queryFn: () => fetch("/comments"), suspense:true},
{queryKey: ["issues"], queryFn: () => fetch("/issues"), suspense:true }
]})
useQueries๋ฅผ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ ๋ด๋ถ context๋ฅผ ์ด์ฉํด ๋ณ๋ ฌ์ ์ผ๋ก api๋ฅผ ํธ์ถํ ์ ์์ด์ context ์ฌ์ฉ์ ์ํ ๋ถ๊ฐ์ ์ธ ๊ตฌํ์ ํ์ง ์์๋ ๋๋ค๋ ์ฅ์ ์ด ์๋ค.
ย
๋ง์น๋ฉฐ
๊ฐ๋ ์ฑ๊ณผ ํด๋ฆฐ ์ฝ๋๋ ์ข์ง๋ง ๋ฌด๋ถ๋ณํ Suspense๋ฅผ ์ฌ์ฉ์ผ๋ก ๋ก๋ฉ ์ฑ๋ฅ์ ์ ํ ์ํค๋ ๊ฒฐ๊ณผ๋ฅผ ๋์ ์ ์๋ค. ๊ทธ๋ ๋ฏ Suspense์ ์ฅ์ ๋ง ์ธ์งํ๊ณ Suspense๋ฅผ ์ฌ์ฉํด์๋ ์ ๋๋ฉฐ, ๋์ ์๋ฆฌ์ ์ ์ ํ poc๋ฅผ ํตํด ๊ณ ๋ฏผํด ๋ณด์์ผ ํ๋ค.
Featured ones: