티스토리 뷰
🚀 Suspense ?
Suspense는 컴포넌트의 렌더링을 어떤 작업이 끝날 때까지 잠시 중단시키고 다른 컴포넌트를 먼저 렌더링할 수 있습니다. 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다고 리액트에게 알려주는 방법이 되겠습니다. 쉽게 말해서, 우리가 서버에서 데이터를 받아올 때 클라이언트의 상태는 요청 → 대기 → 응답 순이 될 겁니다. 이 대기할 때 Suspense의 fallback이라는 속성에 따로 지정해둔 컴포넌트를 화면에 보여주고 응답이 왔을 때는 원래대로 보여주고 싶었던 데이터를 보여주는 방법이라고 볼 수 있습니다.
기본 문법
<Suspense fallback={<Loading />}>
<UserList />
</Suspense>
컴포넌트를 위와 같이 Suspense로 감싸주면 컴포넌트의 렌더링을 특정 작업 이후(위에서는 데이터를 받아오기 전까지) 미루고, 그 작업이 끝날 때까지는 fallback 속성으로 넘긴 컴포넌트를 대신 보여주게 됩니다.
Suspense 사용 전
그 동안 우리가 리액트에서 비동기 데이터를 읽어와야하는 컴포넌트를 어떻게 작성해왔는지 되돌아봅시다.
아마도 현재 가장 흔하게 사용하는 건 useEffect() 훅 함수를 호출하는 방법일텐데, API를 호출하여 네트워크를 통해 데이터를 가져오는 처리는 컴포넌트에서 발생할 수 있는 대표적인 Side Effect이기 때문입니다. 쉽게 말하면, useEffect 안에서 데이터를 받아와서 분명히 return 문 안에서 그 데이터를 map을 돌리거나 할 텐데 네트워크 문제 때문에 데이터를 받아오지 못하게 되면 바로 그 페이지는 먹통이 될 겁니다.
간단한 예로, useEffect() 훅으로 API를 호출하여 가져온 사용자의 글목록을 보여주기 위한 전형적인 함수형 컴포넌트를 작성해보겠습니다.
(공개된 REST API 서비스인 JSONPlaceholder를 사용하였고, 로딩 컴포넌트를 유관으로 확인할 수 있도록 임의로 3초의 지연을 주었습니다.)
📜 Main.js 파일
import User from './User';
function Main() {
return (
<main>
<h2>Suspense 미사용</h2>
<User userId="1" />
</main>
);
}
export default Main;
📜 User.js 파일
import { useEffect } from 'react';
import { useState } from 'react';
import Posts from './Posts';
function User({ userId }) {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState([]);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then((res) => res.json())
.then((user) => {
setTimeout(() => {
setUser(user);
setLoading(false);
}, 3000);
});
}, []);
if (loading) return <p style={{ color: 'red' }}>사용자 정보 로딩중...</p>;
return (
<div>
<p>{user.name}님이 작성한 글</p>
<Posts userId={userId} />
</div>
);
}
export default User;
📜 Posts.js 파일
import { useEffect, useState } from 'react';
function Posts({ userId }) {
const [loading, setLoading] = useState(true);
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
.then((res) => res.json())
.then((posts) => {
setTimeout(() => {
setPosts(posts);
setLoading(false);
}, 3000);
});
}, []);
if (loading) return <p style={{ color: 'red' }}>글목록 로딩중...</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.id}. {post.title}
</li>
))}
</ul>
);
}
export default Posts;
React에서 이처럼 비동기 데이터를 읽어오는 컴포넌트를 작성하면 몇 가지 고질적인 문제가 발생하는 것으로 알려져있어서 useEffect에 대한 사용을 주의하고 있는데요.
우선 최종 사용자(end user) 경험 측면에서 UI가 마치 폭포(waterfall)처럼 순차적으로 나타나는 현상이 나타날 수 있습니다. 이 waterfall 현상은 특히 한 페이지 상의 여러 컴포넌트에서 동시에 비동기 데이터를 읽어오는 경우 자주 발생하는데요. 상위 컴포넌트의 데이터 로딩이 끝나야지만 하위 컴포넌트의 데이터 로딩이 시작될 수 있기 때문에 주로 발생하게 됩니다.
뿐만 아니라 이렇게 초기 렌더링 후에 데이터 로딩 후 렌더링을 수행하는 방법은 경쟁 상태(race conditions)에도 취약한 것으로 알려져있습니다. 비동기 통신은 반드시 요청한 순서래도 데이터가 응답된다는 보장이 없기 때문에 의도치 않게 싱크가 않은 데이터를 제공할 수도 있습니다.
Suspense 사용 후
동일한 코드를 이번에는 Suspense를 이용해서 작성해 봅시다.
먼저 API를 호출하여 비동기로 데이터를 가져오는 코드를 별도의 함수로 빼내겠습니다.
📜 fetchData.js 파일
const fetchUser = (userId) => {
let user = null;
const suspender = fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
)
.then((res) => res.json())
.then((data) =>
setTimeout(() => {
user = data;
}, 3000)
);
return {
read() {
if (user === null) {
throw suspender;
} else {
return user;
}
},
};
};
const fetchPosts = (userId) => {
let posts = null;
const suspender = fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
)
.then((response) => response.json())
.then((data) => {
setTimeout(() => {
posts = data;
}, 3000);
});
return {
read() {
if (posts === null) {
throw suspender;
} else {
return posts;
}
},
};
};
const fetchData = (userId) => {
return {
user: fetchUser(userId),
posts: fetchPosts(userId),
};
};
export default fetchData;
위 함수는 컴폰넌트에서 필요한 데이터를 제공하는 user와 posts 속성을 담고 있는 객체를 반환하는데요. read() 함수를 데이터 수신 중에는 suspender 변수에 저장되어 있는 API를 호출하는 코드를 반환하고, 데이터 수신이 완료되면 데이터를 반환합니다.
이제 <Main /> 컴포넌트 안에서 <User /> 컴포넌트를 <Suspense /> 컴포넌트로 감싸주겠습니다. 기존에 <User /> 컴포넌트 안에 있던 로딩 시 보여줄 컴포넌트가 fallback 속성으로 넘어갑니다. 그리고 <User /> 컴포넌트에는 prop으로 사용자 아이디 대신에 데이터를 가져오기 위함 함수의 호출이 사용됩니다.
📜 Main.js 파일
import { Suspense } from 'react';
import fetchData from './fetchData';
import User from './User';
function Main() {
return (
<main>
<h2>Suspense 사용</h2>
<Suspense
fallback={<p style={{ color: 'red' }}> 사용자 정보 로딩중...</p>}
>
<User resource={fetchData('1')} />
</Suspense>
</main>
);
}
export default Main;
이제 <User /> 컴포넌트 안에서는 prop으로 넘어온 resource로부터 사용자 데이터를 읽어올 수 있습니다. 그리고 <Posts /> 컴포넌트를 사용할 때 마찬가지로 <Suspense />로 감싸줍니다.
📜 User.js 파일
import { Suspense } from 'react';
import Posts from './Posts';
function User({ resource }) {
const user = resource.user.read();
return (
<div>
<p>
{user.name}({user.email}) 님이 작성한 글
</p>
<Suspense fallback={<p>글목록 로딩중...</p>}>
<Posts resource={resource} />
</Suspense>
</div>
);
}
export default User;
<Posts /> 컴포넌트 안에서도 마찬가지로 resource로 부터 글목록 데이터를 읽어올 수 있습니다.
📜 Posts.js 파일
function Posts({ resource }) {
const posts = resource.posts.read();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.id}. {post.title}
</li>
))}
</ul>
);
}
export default Posts;
코드 측면에서도 데이터 로딩과 UI 렌더링이 완전히 분리되어 코드 가독성과 유지 보수성이 향상되겠죠?
'프론트엔드 > React' 카테고리의 다른 글
[React] Axios interceptors와 Refresh token을 이용한 토큰 관리 (0) | 2023.04.04 |
---|---|
[React] React-Query 낙관적 업데이트 (0) | 2023.03.28 |
[React] React에서 에러 핸들링(React-Error-Boundary, React-Query) (0) | 2023.03.22 |
[React] React-Query로 서버 데이터의 상태를 전역으로 관리하자 (0) | 2023.03.20 |
[React] Recoil로 전역 상태 관리를 하자 (0) | 2023.03.20 |
- Total
- Today
- Yesterday
- rtl
- 머신러닝
- 스타일 컴포넌트 styled-components
- 타입스크립트
- 데이터분석
- 프론트엔드
- CSS
- HTML
- 리액트
- next.js
- 자바
- 디프만
- 자바스크립트 기초
- JSP
- 인프런
- 딥러닝
- 자바스크립트
- react-query
- Python
- 리액트 훅
- 프로젝트 회고
- react
- 프론트엔드 공부
- 프론트엔드 기초
- TypeScript
- styled-components
- jest
- testing
- 파이썬
- frontend
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |