• 기술
  • React

React 로딩처리와 Suspense의 등장

작성자 프로필이미지문정민
··10분 읽기

사용자에게 서비스를 제공하는 개발자라면, 사용자 경험을 중요하게 생각할것입니다. 사용자 경험은 디자인을 넘어 사용자와 상호작용하게 되는 그 이전부터 시작됩니다. 웹의 경우 URL을 입력하고 엔터를 치는 순간부터 시작됩니다. 데이터를 효과적으로 불러오고, 그 과정을 사용자에게 명확하게 전달하는 것도 중요한 부분입니다. 그리고 이는 비동기 처리로 이루어지는 경우가 대부분입니다.

웹에서 데이터를 불러오는 작업은 거의 항상 비동기적입니다. 서버로부터 데이터를 받아오는 동안, 우리는 그 시간 동안 사용자에게 어떤 피드백을 제공해야 할지 고민해야 합니다. 사용자에게 '데이터가 로딩 중임'을 알리는 로딩 인디케이터는 가장 일반적인 방법입니다. 이렇게 해서 앱이 여전히 살아있고, 곧 필요한 데이터를 보여줄 것임을 알려줄 수 있습니다.

이런 상황을 효과적으로 구현하기위해 최근 React에 Suspense라는 새로운 개념이 등장했습니다. Suspense는 비동기 작업을 더욱 효율적으로 처리할 수 있게 해주어, 사용자 경험을 향상시키는 데 도움을 줍니다.

이 글에서는, React의 Suspense에 대해 좀 더 자세히 알아보겠습니다. 이 글을 통해 Suspense가 무엇인지, 어떻게 사용하는지, 그리고 이를 통해 어떤 이점을 얻을 수 있는지에 대해 전달할 수 있으면 좋겠습니다. 또한 Suspense가 기존의 비동기 처리 방식과 어떻게 다른지, 그리고 왜 이전 방식에서 벗어나 이런 새로운 개념을 도입하게 되었는지에 대해서도 살펴보겠습니다.

Suspense 없이 React내에서 비동기 데이터 표시하기🔗

React에서는 데이터를 불러오는 것과 같은 비동기 작업을 처리하기 위해 여러 가지 방법을 사용할 수 있습니다. 가장 일반적인 방법은 컴포넌트 내에서 API 호출을 하고 그 결과를 컴포넌트 내부상태에 저장하는 것입니다. 이 방식은 useEffect Hook을 이용해 구현할 수 있습니다.

예를 들면 아래와 같은 방식입니다.

import React, { useState, useEffect } from 'react';

function DataComponent() {
const [data, setData] = useState(null);

useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();

setData(data);
}

fetchData();
}, []);

if (data === null) {
return <div>로딩중...</div>;
}

return <div>{data}</div>;
}
import React, { useState, useEffect } from 'react';

function DataComponent() {
const [data, setData] = useState(null);

useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();

setData(data);
}

fetchData();
}, []);

if (data === null) {
return <div>로딩중...</div>;
}

return <div>{data}</div>;
}

이 코드에서는 useEffect Hook 안에서 API를 호출하여 데이터를 불러옵니다. 만약 데이터가 아직 로드되지 않았다면, 컴포넌트는 "로딩중..."을 표시하고, 그렇지 않다면 로드된 데이터를 표시합니다. 이 방법은 상당히 일반적이며 잘 동작합니다.

이렇게 사용하던중 React에서 Suspense를 새로운 길을 제시해준 것입니다.

기존 방식의 한계🔗

이전에 살펴본 React에서의 비동기 작업 처리 방식은 많은 상황에서 잘 동작합니다. 그러나 이 방식은 대표적으로 아래의 문제를 갖고 있습니다.

개별컴포넌트에서 데이터로딩을 위한 상태가 필요🔗

컴포넌트의 핵심 책임의 데이터를 표현하는 것입니다. 그러나 개별컴포넌트에 데이터로딩 상태가 추가되면서 관리해야할 상태가 늘어났습니다. 이것은 컴포넌트의 본래의 책임에서 조금 벗어난 것입니다.

이처럼 데이터가 로딩이 되었는지 되지 않았는지를 컴포넌트 내부에서 판단하고 처리해야 합니다. 중요한 프로그래밍 원칙인 단일 책임원칙을 벗어나게되고 결국에는 코드의 복잡성으로 자연스럽게 이어지게 될것입니다. 더 좋은 구조가 필요합니다.

각각의 로딩상태를 가짐으로 로딩상태표시가 남발될 수 있음🔗

앱 전체의 입장에서는 원하는 단위에서 통합적으로 로딩상태를 표시해야 할 수 있습니다. 그러나 컴포넌트 개별적으로 로딩상태를 가지게 되면 이것을 통합적으로 관리하기 위한 상위 관리체계가 추가로 필요하게 됩니다.

이런 문제 역시 앱의 상태를 복잡하게 만들 수 있습니다.

아니라면 앱의 구석구석에도 로딩중…이 남발하게 되는 현상이 발생 할 수도 있습니다. 이게 오히려 사용자 경험에는 독이 될 수도 있겠지요.

Suspense의 등장🔗

React Suspense는 React의 비동기 작업 처리 방식을 간소화하고 기존문제점을 개선하는 새로운 기능입니다.

Suspense는 컴포넌트가 비동기 데이터를 필요로 하는 상황을 "일시중단"시킬 수 있게 해줍니다. 즉, 컴포넌트가 데이터를 기다리는 동안 다른 작업을 수행하거나 로딩 인디케이터를 표시할 수 있습니다.

기본적으로 Suspense는 다음과 같은 형태로 사용합니다. 여기서는 react-query 라이브러리를 예제로 사용하겠습니다.

import { useQuery } from 'react-query';

function DataComponent() {
const { data } = useQuery('fetchData', fetchSomeData, {
suspense: true,
});

return <div>{data}</div>;
}

function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<DataComponent />
</Suspense>
);
}
import { useQuery } from 'react-query';

function DataComponent() {
const { data } = useQuery('fetchData', fetchSomeData, {
suspense: true,
});

return <div>{data}</div>;
}

function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<DataComponent />
</Suspense>
);
}

이 코드에서 DataComponentuseQuery Hook을 사용하여 데이터를 불러옵니다. 이때 suspense 옵션을 true로 설정하여 Suspense와 호환되도록 합니다. 그러면 DataComponent가 데이터를 기다리는 동안 자동으로 "로딩중..." 메시지를 보여주게 됩니다.

Suspense가 기존문제를 해결했나요?🔗

간단히 대답하면 그렇다입니다. React Suspense는 위에서 언급된 문제들을 효과적으로 해결합니다.

개별 컴포넌트에서 데이터 로딩을 위한 상태 관리가 불필요🔗

Suspense는 개별 컴포넌트에서 데이터 로딩을 위한 상태 관리를 제거합니다. Suspense는 비동기 로딩을 자체적으로 처리하기 때문에, 개별 컴포넌트가 데이터 로딩 상태를 판단하거나 관리할 필요가 없습니다.

위에서 들었던 예시 중 Suspense를 사용하지 않는 경우, 이 컴포넌트는 데이터 로딩 상태를 직접 관리했습니다.

그러나 Suspense의 사용으로 이 책임을 외부로 돌려, 컴포넌트는 이 책임에서 벗어나, 단순히 데이터를 렌더링하는 데 집중할 수 있습니다.

function ComponentWithSuspense() {
// 내부에서 로딩상태를 고려하지 않음.
const { data } = useQuery("data", fetchAuthor, {
suspense: true,
});
return <div>{data}</div>;
}

function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<ComponentWithSuspense />
</Suspense>
);
}
function ComponentWithSuspense() {
// 내부에서 로딩상태를 고려하지 않음.
const { data } = useQuery("data", fetchAuthor, {
suspense: true,
});
return <div>{data}</div>;
}

function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<ComponentWithSuspense />
</Suspense>
);
}
react-query를 활용한 Suspense 사용예제

이런 방식은 각 컴포넌트가 자신의 책임에 집중하도록 해주어서 코드의 복잡성을 줄이고 단일 책임 원칙을 더 잘 지킬 수 있게 합니다.

앱 전체의 입장에서 통합적으로 로딩 상태를 표시🔗

Suspense는 컴포넌트 트리 전체에 걸쳐 로딩 상태를 통합적으로 관리할 수 있게 합니다. Suspense는 여러 개의 비동기 작업이 동시에 수행되는 경우에도, 로딩 표시자를 단 한 번만 표시하도록 할 수 있습니다.

따라서, Suspense는 앱 전체의 입장에서 통합적으로 로딩 상태를 관리할 수 있게 해주며, 이는 더욱 일관된 사용자 경험을 제공합니다.

아래와 같이 각 컴포넌트에서 각각 비동기 호출을 하는 경우에도, 하나의 Suspense에서 통합적으로 처리하여 로딩상태를 한 번만 표시할 수 있습니다.

function ArticleAuthor() {
const { author } = useQuery("author", fetchAuthor, {
suspense: true,
});

return <div>{author}</div>;
}

function ArticleContent() {
const { content } = useQuery("content", fetchContent, {
suspense: true,
});
return <div>{content}</div>;
}

function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<ArticleAuthor />
<ArticleContent />
</Suspense>
);
}
function ArticleAuthor() {
const { author } = useQuery("author", fetchAuthor, {
suspense: true,
});

return <div>{author}</div>;
}

function ArticleContent() {
const { content } = useQuery("content", fetchContent, {
suspense: true,
});
return <div>{content}</div>;
}

function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<ArticleAuthor />
<ArticleContent />
</Suspense>
);
}
비동기 데이터를 로딩하는 두개의 컴포넌트를 감싸는 Suspense 예제

도입하기 전에🔗

Suspense는 최근에 추가된 기능인 만큼 자리잡힌 서비스일수록 그 영향을 고려하며 적용해야 할 것입니다. 아래 내용은 Suspense 도입시 생각해보아야 할점을 언급하며 이 글을 마무리 하도록 하겠습니다.

  • Suspense 동작원리에 대한 이해

    Suspense는 비동기 데이터를 기다리는 동안 컴포넌트 렌더링을 "일시 중단"하는 방식으로 동작합니다. 추후 다른글에서 Suspense의 내부동작 방식을 살펴보면서 조금 더 자세히 살펴 볼 예정입니다.

  • Suspense와 호환되지 않는 비동기 작업

    Suspense는 특정한 동작 방식을 준수하는 비동기 작업에 대해서만 정상적으로 동작합니다. 그렇지 않다면 예상치 못한 에러나 문제가 발생할 수 있습니다. Suspense는 Promise를 캐치하는 방식으로 동작하기 때문에 데이터 로딩 함수가 Promise를 반환하지 않는다면 Suspense는 이를 "일시중단"할 수 없고, 따라서 정상적으로 동작하지 않을 것입니다. 이 문제를 해결하기 위해선, Suspense와 함께 사용할 모든 비동기 작업이 Suspense의 동작 방식을 준수하도록 해야 합니다. 위 예제에서 보였던 react-query는 이러한 관점에서 Suspense를 지원하도록 설계되어 있습니다.