이번글은 해당 영상 보고 정리한 글입니다.
아직까지도 웹 서비스 다루기에 가장 까다롭고 어려운 부분이 아무래도 비동기라고 말씀을 하신다. 이런 비동기 처리를 React에서 어떻게 효과적으로 비동기 처리 하는지 다루는 정말 감명 깊게 본 영상이어서 회고하는 글을 쓰기 시작했다.
비동기 선행학습이 필요함 (callback, Promise, async await, 체인메서드 then, try, catch, finally 등)
비동기 프로그래밍이란(Asynchronous):
비동기방식은 현재 작업의 요청과 해당작업의 응답이 동시에 진행되지 않아도 되는 것으로 응답에 관계없이 바로 다음 동작이 실행되는 것을 말한다. 다음 동작을 순차적으로 실행하면서 해당 비동기 요청에서 응답이 오면 다시 응답에 대한 처리를 해준다.
비동기 프로그래밍은 한 줄로 요약하자면 순서가 보장되지 않는 상황이라고 요약할 수 있다.
비동기 중에서 제일 기초적인 callback 함수로 비동기 로직:
function fetchAccounts(callback) {
fetchUserEntity((err, user) => {
if (err != null) {
callback(err, null);
return;
}
fetchUserAccounts(user.no, (err, accounts) => {
if (err != null) {
callback(err, null);
return;
}
callback(null, accounts);
});
});
}
해당 코드의 함수가 하는 일은 user의 정보를 바탕으로 accounts값을 반환하는 역할인데요.
해당 코드에서의 문제점:
- 에러핸들링, 및 성공한 로직이 분리되어서 작업되어있지 않고 섞여서 처리된다.
- 코드의 진짜 역할을 잃어버린 것.
- 비동기 작업을 할 때마다 에러처리를 해줘야 한다는 점이다.
async await 방식으로 짠 비동기 로직:
async function fecthAccounts(){
const user = await fecthUserEntity();
const accounts = await fetchUserAccounts(user.no);
return accounts;
}
이 코드에서는 성공한 경우만 다루고 있다는 것을 알 수 있습니다. 우리가 알던 동기적인 코드랑 큰 차이가 없습니다 덕분에 함수가 하는 역할이 명확하게 보임으로 가독성이 좋아졌습니다. 실패하는 경우에 대한 처리는 외부에 위임해서 감출 수 있습니다.
해당 코드의 좋은 점은 성공한 경우에 역할만 충실히 해주고. 다른 경우는 외부에 작성하여 외부에서 더 자세히 처리하게끔 설계했다는 점입니다.
해당 영상에서 말한
좋은 코드의 특징:
성공, 실패의 경우를 분리해 처리할 수 있다. 즉 위에 좋은 코드의 예에서는 성공한 코드만 작성하므로 해당 함수의 비즈니스 로직을 한눈에 파악할 수 있고 명확한 책임이 드러난다.
어려운 코드의 특징:
실패, 성공의 경우가 서로 섞여 처리된다. 그래서 비즈니스 로직을 한눈에 파악하기 힘들다.
저는 프런트엔드 개발자로서 정말 많은 비동기처리를 해왔는데요. 해당 영상을 보면서 나오는 안 좋은 예가 제가 비동기 처리하는 방식이랑 똑같아서 많이 반성을 하는 시간을 가진 거 같습니다.
안 좋은 예:
function Profile(){
const foo = useAsyncValue(() => {
return fetchFoo();
}) // 비동기처리를 리액트 훅 으로 작성.
if(foo.error) return <p>실패</p>
if(!foo.data) return <p>데이터 불러오는 중...</p>
return <div>{foo.data.name}님 안녕하세요.</div>
}
해당 코드를 보시면 아시겠지만 안 좋은 코드의 특징을 다 가지고 있습니다.
- 실패, 성공의 처리가 섞여 처리된다.
- 실패하는 경우에 대한 처리를 외부에 위임하기 어렵다.
이러한 문제는 여러 개의 비동기 작업이 동시에 실행될 때 더 심각해진다. 비동기 작업이 많으면 많을수록 코드를 파악하기가 힘들다는 점입니다.
안 좋은 이유 여러 가지를 알아봤으니깐 그럼 이제 어떻게 개선을 해야 할지 봐야 하는데 react로 작성한 코드가 아닌 일반 js로 작성한 비동기는 위에 나온 예시처럼 async, await 사용해서 성공한 로직, 실패한 로직 따로 분리해서 작성하면 되지만. react를 써본 사람들은 다 알겠지만 react는 성공하는 경우에만 집중해서 컴포넌트를 구성하기가 어렵습니다. 그래서 2개 이상의 비동기 로직이 개입할 때, 비즈니스 로직을 파악하기 점점 어려워집니다.
그런데 다행이 react에서는 이 문제를 해결해 주는 도구가 있다는 것 을 알게 배웠습니다. 바로 react 팀이 제안하는 React Suspense for data fetching입니다.
React Suspense for data fetching 쓰는 이유:
- 더욱더 읽기 편한 React를 만들기 위해서입니다.
- async await처럼 비동기 로직을 처리면서 성공한 경우에만 집중할 수 있도록 도와주기 위해서
- 로딩 상태와 에러 상태가 분리되도록 사용하기 위해서
- 동기와 구성이 같게 코드구성 하기 위해서
Suspense란?
React v18.0에 공식적으로 나온 기능입니다. Suspense라는 React의 신기술을 사용하면 컴포넌트의 렌더링을 어떤 작업이 끝날 때까지 잠시 중단시키고 다른 컴포넌트를 먼저 렌더링 할 수 있습니다. 이 작업이 꼭 어떠한 작업이 되어야 한다는 특별한 제약 사항은 없다 (현재 공식문서에서는 React.lazy()를 사용해서 동적으로 컴포넌트를 불러올 때 사용방법을 제시하고 있습니다) 아무래도 사용자들은 네트워크를 통해 비동기로(asynchronously) 데이터를 가져오는 작업을 가장 먼저 떠오르게 됩니다. 과거 리액트에서는 비동기 처리할 때의 pending, success, rejected 상태들을 다 하나의 컴포넌트에서 처리를 해야 하는 불편함이 있었습니다.
Suspense 기능은 현재 위에서 나온 안 좋은 코드 특징들은 다 없애고 필요한 로직처리만 작성할 수 있게 되었습니다. 가장 좋은 사용법은 로딩 지시기를 보여주고 싶은 지점에 <Suspense>를 작성하는 것이라고 공식문서에서 알려주고 있다.
Suspense가 비동기 컴포넌트가 아직 불러오지 않은 상태에서 대시 먼저 할당해 주는 컴포넌트면 간단하게 pending상태를 책임져주는 부분이라고 생각하면 된다.
이제 rejected 상태를 어떻게 처리해야 할지 고민을 해 봐야하합니다. 비동기 요청은 항상 예외처리도 처리해 줘야 된다고 생각합니다 그때는 React16 버전에서 소개된 ErrorBoundary 사용해서 처리해 주면 된다. ErroBoundary 대해서도 조만간 제대로 공부해서 정리해 볼 예정이다. 이번에는 ErroBoundary 대해서 간단하게만 알아보자.
ErrorBoundary란?
React는 16 버전부터 앱의 하위 컴포넌트 트리에서 발생하는 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 ErrorBoundary라는 개념을 도입하였다. ErrorBoundary를 통해 컴포넌트에서 에러가 발생했을 때 이를 캐치하여 사용자들에게 에러가 발생하여 앱이 중단되는 것이 아닌 다른 대체 화면을 보여줄 수 있다. ErrorBoundary를 잘 이용하여 앱단과 오류가 발생할 컴포넌트에 잘 감싸주고, 사용자에게 보여줄 화면을 잘 활용(ex-새로고침 화면 보여주기)한다면, 좋은 에러 핸들링이 될 것이다. 전체를 감싸줄 수 있고 한 컴포넌트만 감싸줘서 각각의 컴포넌트의 에러처리를 해 줄 수 있는 장점이 있다.
먼저 Suspense와 ErrorBoundary를 사용하기 전에 예제를 간단히 살펴보자.
import axios from "axios";
import { useQuery } from "react-query";
const fetchProduct = async () => {
try {
const response = await axios.get(
"http://makeup-api.herokuapp.com/api/v1/products.json?brand=maybelline"
);
return response;
} catch (err) {
throw err;
}
};
export default function ProductPage() {
return (
<>
<h1>Suspense Example</h1>
<ProductList />
</>
);
}
function ProductList() {
const { isLoading, data, isError } = useQuery("fetchProduct", fetchProduct, {
retry: false,
});
if (isLoading) {
return <h1>isLoading....</h1>;
}
if (isError) {
return <p>서버에러 발생.</p>;
}
if (data) {
return (
<>
{data.data.slice(0, 10).map((item, index) => (
<div key={item.id}>
<p>{index + 1}</p>
<img
width={"60px"}
height={"60px"}
src={item.api_featured_image}
alt={item.name}
/>
<p>{item.name}</p>
</div>
))}
</>
);
}
}
해당 코드를 보면 react-query 라이브러리 사용해서 fetchProduct 호출하여 isLoading, isError, data 이렇게 세 가지 상태관리를 쉽게 할 수 있습니다 현재 보시면 하나의 ProductList 안에서 모든 상태를 처리하고 있는 것을 알 수 있습니다 이건 해당 영상에서 지적하는 전형적인 안 좋은 코드입니다. 에러처리와 성공처리 pending 상태 모두 다 포함되어 있어서 코드 가독성이 떨어지고 컴포넌트의 역할도 불명확해지기 때문이라고 생각합니다. 이제 이런 문제점을 해결하기 위해서 먼저 Suspense를 적용해 보겠습니다.
Suspense 적용 후:
먼저 react-query 라이브러리에서는 suspense를 지원하는 option이 있습니다
const { isLoading, data, isError } = useQuery("fetchProduct", fetchProduct, {
retry: false,
suspense:true // 이렇게 option값에 suspense 값을 true로 설정하면 알아서 suspense와 호환이 됩니다.
});
import axios from "axios";
import { Suspense } from "react";
import { useQuery } from "react-query";
const fetchProduct = async () => {
...동일
};
export default function ProductPage() {
return (
<>
<h1>Suspense Example</h1>
<Suspense fallback={<p>Loading....</p>}> // suspense 추가
<ProductList />
</Suspense>
</>
);
}
function ProductList() {
const { data, isError } = useQuery("fetchProduct", fetchProduct, {
retry: false,
suspense: true,
});
if (isError) {
return <p>서버에러 발생.</p>;
}
if (data) {
...동일
}
}
해당 코드를 직접 실행해 보시면 아시겠지만 위에 예제랑 똑같이 작동한다는 것을 알 수 있습니다 pending 상태처리를 밖으로 빼줌으로써 코드는 한층 더 가독성이 좋아졌고 역할이 명확 해 졌습니다. 이제 ErrorBoundary 적용해 보겠습니다.
ErrorBoundary 적용 후:
ErrorBoundary는 나중에 자세히 한번 기록할 예정이다 이번에는 간단하게 사용하는 방법만 기록했습니다.
import axios from "axios";
import { Suspense } from "react";
import { useQuery } from "react-query";
import { ErrorBoundary } from "./errorboundary";
const fetchProduct = async () => {
try {
const response = await axios.get(
"http://makeup-api.herokuapp.com/api/v1/products.json?brand=maybelline"
);
return response;
} catch (err) {
throw err;
}
};
export default function ProductPage() {
return (
<>
<h1>Suspense Example</h1>
<ErrorBoundary> // errorBoudary 감싼다.
<Suspense fallback={<p>Loading....</p>}>
<ProductList />
</Suspense>
</ErrorBoundary>
</>
);
}
function ProductList() {
const { data } = useQuery("fetchProduct", fetchProduct, {
retry: false,
suspense: true,
useErrorBoundary: true, // errorBoundary사용시 추가하는 option
});
return (
<>
{data.data.slice(0, 10).map((item, index) => (
<div key={item.id}>
<p>{index + 1}</p>
<img
width={"60px"}
height={"60px"}
src={item.api_featured_image}
alt={item.name}
/>
<p>{item.name}</p>
</div>
))}
</>
);
}
이렇게 해주면 fetchProduct 비동기 처리에서 에러 발생할 때 errorBoundary에서 처리를 해준다 이제 ProductList 컴포넌트를 보면 아시겠지만 좋은 코드랑 똑같이 자기 역할이 명확해지고 성공한 상황만 보이게 되고 다른 상태들은 밖으로 빼 주는 것을 볼 수가 있다.
결론:
비동기 컴포넌트를 다루는 일은 까다롭지고 손이 많이 가는 작업이지만 이를 Suspense와 ErrorBoundary를 적절히 조합하여 만들어봤는데 사용자 경험 입장에서도 코드 가독성 생산성에서도 좋은 효과를 보이고 있다. 실제 프로젝트에서도 많이 적용해 볼 예정이다.
코드는 제 깃허브에서 보실 수 있습니다: https://github.com/sungmin-choi/react18/tree/main/src/suspense
'React' 카테고리의 다른 글
기존 프로젝트 React 17에서 React 18로 업그레이드하기 (0) | 2023.02.02 |
---|---|
[React 공식문서 공부] React로 생각하기 (1) | 2023.01.29 |
[React 공식문서 공부] JSX란? (0) | 2023.01.29 |
React 모바일(safari, chrome)에서 100vh 네비바 로 인한 화면 가려지는 현상 해결하기 (0) | 2022.12.09 |
댓글