React Suspense 알아보기
Suspense는 React 16.6에서 추가된 기능이에요. 처음에는 코드 스플리팅(lazy)을 위해서만 사용됐으나 나중에는 데이터 Fetching을 기다릴수도 있고 SSR Streaming이라는 기능도 제공되게 기능이 확장되는 중이에요.Suspense란 무엇일까 ?
- Lazy
- Data Fetching
- waterfall
- Suspense 안써도 똑같은데요 ? 안쓰면 안댐 ?
- 관심사 분리와 선언형
- 단순히 코드만 깔끔해지나 ?
- Suspense를 써도 Waterfall이 발생하는데요 ?
- useQueries
- prefetch
- SSR Streaming
Suspense란 무엇일까 ?
React Beta 문서에 나온 내용을 보면
Suspense lets you display a fallback until its children have finished loading.
Suspense는 자식의 로딩이 끝날때까지 fallback을 보여준다.
라고 써있네요.
여러분들은 로딩이라고 하면 뭐가 떠오르시나요 ? 저는 data fetching이 제일 먼저 떠올랐어요. 또 코드 스플리팅을 한 컴포넌트를 받아오면서도 로딩이 있을 수 있겠죠. 우선 Suspense를 처음 활용할 수 있었던 lazy부터 살펴볼게요.
Lazy
React Beta 문서에 나와있는 내용을 보면
lazy lets you defer loading component’s code until it is rendered for the first time.
lazy는 컴포넌트가 처음 렌더링될 때까지 컴포넌트의 코드 로딩을 지연시킬 수 있다.
lazy를 사용하면 코드 스플리팅을 할 수 있어요. 이 글은 Suspense에 관한 글이기 때문에 코드 스플리팅,lazy에 대해서는 따로 설명하지는 않을게요. 바로 코드랑 그 결과를 볼게요.
import { lazy, Suspense } from "react";
const LazyComponent = lazy(() => import("../Component/LazyComponent"));
export const LazyPage = () => (
<Suspense fallback={<h1>Lazy 컴포넌트 로딩</h1>}>
<LazyComponent />
</Suspense>
);
이런식으로 작성해봤어요. 크롬 개발자도구 성능 측정텝에서 결과를 한번 볼게요 !
이렇게 Suspense의 fallback
을 보여주고
안에 있는 LazyComponent
를 보여주네요.
Data Fetching
여기서는 Waterfall
현상이 뭐고 언제 일어나고 useEffect
를 사용한 data fetch방법과 Suspense
를 사용한 방법을 비교하고 Suspense
를 사용했을때도 Waterfall
현상이 발생하고 그것을 해결하는 방법에 대해서 알아볼거에요.
Waterfall
DataFetchEffect.js
export const ArticleList = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const init = async () => {
const result = await getArticleList();
setData(result);
setLoading(false);
};
init();
}, []);
if (loading) return <h1>Article Loading...</h1>;
return (
<>
<ul>
{data?.map((item) => (
<li key={item.title}>
<span>{item.title}</span>
</li>
))}
</ul>
<UserListEffect />
</>
);
};
UserListEffect.js
export const UserList = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const init = async () => {
const result = await getUserList();
setData(result);
setLoading(false);
};
init();
}, []);
if (loading) return <h1>User List Loading</h1>;
return (
<ul>
{data?.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
);
};
이런식으로 코드가 작성되면
DataFetchEffect
컴포넌트가 loading중에 UserListEffect
컴포넌트가 실행되지 않으니까 당연히 이런식으로 실행이 되겠죠 ??
이 Waterfall 현상을 해결해볼게요.
우선 Suspense
를 써서 해결해볼게요.
export const DataFetchSuspensePage = () => (
<>
<Suspense fallback={<h1>User List Loading...</h1>}>
<UserList />
</Suspense>
<Suspense fallback={<h1>ArticleList Loading...</h1>}>
<ArticleList />
</Suspense>
</>
);
컴포넌트 안에는 react-query
라이브러리를 사용해서 구현했어요. 이런식으로 코드를 짜면
한번에 불러와져서 waterfall
현상이 사라졌네요 !
근데 이게 Suspense때문에 해결된게 맞을까요 ?Suspense
를 사용안하고 병렬적으로 fetch할 수 있게 코드를 짜면
export const DataFetchEffectPage = () => (
<>
<UserList />
<ArticleList />
</>
);
이렇게 Waterfall
이 해결돼요 !
Suspense 안써도 똑같은데요 ? 안쓰면 안댐 ?
우선 방금 위에서 봤던거처럼 Suspense를 안쓰고 사용해도 waterfall을 막을 수 있었어요.
그러면 Suspense
를 왜 사용할까요 ?
관심사 분리와 선언형
export const UserList = () => {
const { data, isLoading } = useQuery(["userList"], getUserList);
if (isLoading) return <h1>User List Loading...</h1>;
return (
<>
<ul>
{data.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
</>
);
};
Suspense
를 사용하지 않으면 보통 이런식으로 코드를 짤거 같아요. react query를 사용안하고 useEffect
안에서 fetch하는 방법도 loading state, data state
를 만들고 비슷한 방법으로 보여줄거에요.
우선 이 코드를 보면 관심사 분리가 잘안됐다고 느껴져요. 왜냐하면 UserList
컴포넌트의 역활은 User의 List를 보여주는 역활인데 여기서는 User List를 불러오는 loading
도 같이 관리하고 있어요. 그리고 선언형
인 React인데 명령형
인 구조가 나오기도 하고요.
Suspense를 사용하면
export const UserList = () => {
const { data } = useQuery(["userList"], getUserList, {
suspense: true,
});
return (
<ul>
{data.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
);
};
export const DataFetchSuspensePage = () => (
<Suspense fallback={<h1>User List Loading...</h1>}>
<UserList />
</Suspense>
);
이런식으로 UserList
컴포넌트는 데이터를 받아와서 보여주는 부분만 집중하고 loading
관련 상태는 밖으로 빼니 코드가 더 간결해지고 목적이 분명하게 보이는 거 같아요 그리고 선언적이고요.
단순히 코드만 깔끔해지나 ?
그러면 Suspense는 단순히 코드만 깔끔해지는 Syntactic sugar
일까요 ?useEffect
을 이용한 data fetch와 Suspense
과 비교해볼게요
각각 user list와 article list를 fetch할때와 데이터를 받았을때 console.log을 찍어서 타이밍을 확인 해볼거에요.
useEffect를 사용한 fetch 방법
export const DataFetchEffectPage = () => (
<>
<UserListEffect />
<ArticleListEffect />
</>
);
export const UserListEffect = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log("UserListEffect mount");
const init = async () => {
const result = await getUserList();
setData(result);
setLoading(false);
};
init();
}, []);
if (loading) return <h1>User List Loading</h1>;
return (
<ul>
{data?.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
);
};
export const ArticleListEffect = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log("ArticleListEffect mount");
const init = async () => {
const result = await getArticleList();
setData(result);
setLoading(false);
};
init();
}, []);
if (loading) return <h1>Effect Loading...</h1>;
return (
<ul>
{data?.map((item) => (
<li key={item.title}>
<span>{item.title}</span>
</li>
))}
</ul>
);
};
이렇게 코드를 구성하고 실행을 시켜볼까요 ?
이러한 순서대로 콘솔이 찍히네요. 이렇게 useEffect
를 사용한 fetch 방법을 Fetch-on-render 이라고 해요. 컴포넌트가 mount되고 useEffect가 실행되니까
mount => fetch 시작 => fetch 종료
이 순서로 진행되겠죠 ??
이번에는 Suspense
를 사용해서 만들고 콘솔을 찍어볼게요.
Suspense를 사용한 fetch 방법
export const DataFetchSuspensePage = () => (
<>
<Suspense fallback={<h1>User List Loading...</h1>}>
<UserListSuspense />
</Suspense>
<Suspense fallback={<h1>Article List Loading...</h1>}>
<ArticleListSuspense />
</Suspense>
</>
);
export const UserListSuspense = () => {
const { data } = useQuery(["userList"], getUserList, {
suspense: true,
});
useEffect(() => {
console.log("mount userList");
}, []);
return (
<ul>
{data.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
);
};
export const ArticleListSuspense = () => {
const { data } = useQuery(["articleList"], getArticleList, {
suspense: true,
});
useEffect(() => {
console.log("mount ArticleList");
}, []);
return (
<ul>
{data?.map((item) => (
<li key={item.title}>{item.title}</li>
))}
</ul>
);
};
이제 콘솔을 확인해볼게요
오....! 순서가 위에 useEffect
를 다르네요. 이 방법을 Render-as-you-fetch 방법이라고 불러요. data fetch를 먼저 시작하고 data를 받고 컴포넌트를 렌더링하는 것처럼 보이네요.
Suspense를 써도 Waterfall이 발생하는데요 ?
코드를 한번 볼까요 ?
export const DataFetchSuspensePage = () => (
<Suspense fallback={<h1>User List Loading...</h1>}>
<UserListSuspense />
</Suspense>
);
export const UserListSuspense = () => {
useEffect(() => {
console.log("mount userList");
}, []);
const { data } = useQuery(["userList"], getUserList, {
suspense: true,
});
return (
<>
<ul>
{data.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
<Suspense fallback={<h1>Article List Loading...</h1>}>
<ArticleListSuspense />
</Suspense>
</>
);
};
export const ArticleListSuspense = () => {
const { data } = useQuery(["articleList"], getArticleList, {
suspense: true,
});
useEffect(() => {
console.log("mount ArticleList");
}, []);
return (
<ul>
{data?.map((item) => (
<li key={item.title}>{item.title}</li>
))}
</ul>
);
};
이런식으로 코드를 짜면
이런식으로 응답이 오네요. 물론 이런 경우에는 ArticleListSuspense
컴포넌트를 밖으로 빼서UserListSuspense
컴포넌트와 병렬적으로 배치하면 해결될 문제입니다. 하지만 만약에 병렬적인 배치가 힘든 코드 구조가 있고 그런 구조에서는 어떻게 해결하는게 좋을까요 ?
저는 이렇게 해결해볼거 같아요
1. useQueries
export const UserListSuspense = () => {
const results = useQueries({
queries: [
{
queryKey: ["userList"],
queryFn: getUserList,
suspense: true,
},
{
queryKey: ["articleList"],
queryFn: getArticleList,
suspense: true,
},
],
});
useEffect(() => {
console.log("mount userList");
}, []);
return (
<>
<ul>
{results[0].data.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
<Suspense fallback={<h1>Article List Loading...</h1>}>
<ArticleListSuspense />
</Suspense>
</>
);
};
이런식으로 react query 라이브러리의 useQueries
를 사용해서 처리하면
이런식으로 다시 돌아와요 ! 하지만 이런 경우에는 Article api
의 응답이 많이 느린 경우 User List
를 받아왔음에도 불구하고 loading만 보여줘야하는 경우도 생겨요.
2. prefetchprefetch
를 이용해서 코드를 짜면
const prefetchTodos = async () => {
console.log("prefetch");
await queryClient.prefetchQuery(["articleList"], getArticleList);
};
prefetchTodos();
export const UserListSuspense = () => {
const { data } = useQuery(["userList"], getUserList, {
suspense: true,
});
useEffect(() => {
console.log("mount userList");
}, []);
return (
<>
<ul>
{data.map((item) => (
<li key={item.name}>
<span>이름은 {item.name}</span>
<span>나이는 {item.age}</span>
</li>
))}
</ul>
<Suspense fallback={<h1>Article List Loading...</h1>}>
<ArticleListSuspense />
</Suspense>
</>
);
};
이런식으로 결과가 나오네요. 근데 이 방법을 사용하면 제가 잘못짠거인지는 모르겠는데 다른 페이지에서도 prefetch
를 시도하더라고요. 저는 다른 페이지말고 저 컴포넌트를 사용하는 페이지에서만 prefetch
를 적용하고 싶어서 고민해본 결과
const LoadingComponent = () => {
const prefetchTodos = async () => {
console.log("prefetch");
await queryClient.prefetchQuery(["articleList"], getArticleList);
};
prefetchTodos();
return <h1>User List Loading...</h1>;
};
export const DataFetchSuspensePage = () => (
<Suspense fallback={<LoadingComponent />}>
<UserListSuspense />
</Suspense>
);
이런식으로 코드를 짜서 확인해본결과 다른 페이지들에서 prefetch
를 하지 않고 해당 로딩 컴포넌트
를 사용한 페이지에서만 prefetch
를 하는식으로 동작을 해요.
근데 이렇게 fallback에 들어가는 컴포넌트에 prefetch로직을 넣어도 괜찮나 궁금하네요 ㅎㅎ
여기까지 제가 Suspense에 대해서 알고 있는것들에 대해서 작성해봤어요. 혹시 잘못된 내용이 있으면 피드백 부탁드립니다 감사합니다 :)