티스토리 뷰

Suspense란?

React 문서에 따르면 Suspense를 사용하면 컴포넌트가 렌더링되기 전까지 기다렸다가

해당 컴포넌트에서 데이터를 불러오게 되면 로딩상태였던 컴포넌트를 렌더링합니다.

쉽게말해 로딩상태 처리를 쉽게해주는 리엑트 컴포넌트입니다.

 

 

 

기존 접근 방식 vs Suspense

기존의 데이터 불러오는 방식과 Suspense를 사용하여 처리하는 방식을 비교해보겠습니다.

 

 

접근방식 1. 렌더링 직후 불러오기

리엑트에서 흔히들 사용하는 데이터를 불러오는 방식으로는 useEffect, useState, fetch를 활용해 처리하게 됩니다.

 

코드로 보면 다음과 같습니다.

( 스타일링은 styled-components 를 사용하였습니다. )

function ProfileRenderingThenGetData() {
  /* 
  접근 방식 1
  렌더링 직후에 불러오기
  */
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      // 유저정보를 불러오고
      const getUserData = await getUserDataFetch();
      // 불러왔으면 유저정보를 state에 넣어줍니다.
      setUserData(getUserData);
    }
    fetchData();
  }, []);

  if (userData === null) {
    // userData가 null 이면 로딩처리
    return <Loading text="유저 정보 불러오는 중..." />;
  }
  return (
    <Container>
      <InfoHeader>유저 정보(렌더링 직후에 불러오기)</InfoHeader>
      <UserInfo>이름 : {userData.user_name}</UserInfo>
      <UserInfo>이메일 : {userData.user_email}</UserInfo>
      <UserInfo>나이 : {userData.user_age}</UserInfo>
    </Container>
  );
}

const Container = styled.div`
  padding: 25px;
`;
const InfoHeader = styled.h1`
  font-size: 24px;
  padding: 0 0 16px;
`;
const UserInfo = styled.h2`
  font-size: 20px;
`;

 

위의 방식은 컴포넌트가 렌더링이 되면 useEffect를 통해서 유저 데이터를 fetch하고 state를 넣어주어 렌더링합니다.

유저 정보의 state 초기값은 null로 해당 state가 null이라면 로딩 컴포넌트를 렌더링합니다.

 

워터폴

이 방식은 컴포넌트가 다 렌더링이 되어야 그 이후에 데이터를 요청하므로 불필요하게 시간을 소비됩니다.

이것은 리엑트에서 "워터폴"이라는 문제로 부른다고 합니다.

( 워터폴이란 프로젝트 방법론으로 특정 순서에 따라 프로젝트를 순차적으로 실행 및 완료하는 과정을 말합니다. )

 

컴포넌트 렌더링과 데이터 불러오기를 병렬적으로 실행하면 더 좋은 사용자 경험을 이끌수 있어 위의 렌더링 직후 데이터 불러오는 방법은 지양해야합니다.

 

 

접근방식 2. 불러오기 이후 렌더링

이 방식은 먼저 데이터 불러오는 함수를 실행시키고

컴포넌트가 렌더링이 된 직후 데이터 불러오기가 성공하면 유저 정보 state에 데이터를 담아줍니다.

// 먼저 유저 정보 데이터 불러오기를 발동합니다.
const getUserDataPromise = getUserDataFetch();

function ProfileGetDataThenRendering() {
  /* 
  접근 방식 2
  불러오기 이후 렌더링
  */
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    // 불러오기가 실행되고, 컴포넌트가 렌더링이 된후에 불러오기가 성공했다면
    // 유저 정보를 state에다 담아줍니다.
    getUserDataPromise.then((res) => {
      setUserData(res);
    });
  }, []);

  if (userData === null) {
    // userData가 null 이면 로딩처리
    return <Loading text="유저 정보 불러오는 중..." />;
  }
  return (
    <Container>
      <InfoHeader>유저 정보(불러오기 이후 렌더링)</InfoHeader>
      <UserInfo>이름 : {userData.user_name}</UserInfo>
      <UserInfo>이메일 : {userData.user_email}</UserInfo>
      <UserInfo>나이 : {userData.user_age}</UserInfo>
    </Container>
  );
}

const Container = styled.div`
  padding: 25px;
`;
const InfoHeader = styled.h1`
  font-size: 24px;
  padding: 0 0 16px;
`;
const UserInfo = styled.h2`
  font-size: 20px;
`;

 

위의 경우에서 하나의 데이터를 더 불러오는 경우가 있다고 하면 코드가 다음과 같아집니다.

 

async function getUserDataFetch(user) {
  return fetch(`https://jikor1st.github.io/FakeApi/userData/${user}.json`)
    .then((res) => {
      return res.json();
    })
    .then((data) => {
      return data;
    })
    .catch(() => {
      return undefined;
    });
}

async function getUserAllFetch() {
  // 유저의 정보 2개를 같이 불러오기
  return Promise.all([
    getUserDataFetch("user"),
    getUserDataFetch("user1"),
  ]).then(([user, user1]) => {
    return { user, user1 };
  });
}

// 먼저 유저 정보 데이터 불러오기를 발동합니다.
const getUserAllPromise = getUserAllFetch();

export default function ProfileGetDataThenRendering() {
  /* 
  접근 방식 2
  불러오기 이후 렌더링
  */
  const [userData, setUserData] = useState(null);
  const [user1Data, setUser1Data] = useState(null);

  useEffect(() => {
    // 불러오기가 실행되고, 컴포넌트가 렌더링이 된후에 불러오기가 성공했다면
    // 유저 정보를 state에다 담아줍니다.
    getUserAllPromise.then((res) => {
      setUserData(res.user);
      setUser1Data(res.user1);
    });
  }, []);

  if (userData === null || user1Data === null) {
    // userData가 null 이면 로딩처리
    return <Loading text="유저 정보 불러오는 중..." />;
  }
  return (
    <Wrap>
      <Container>
        <InfoHeader>유저 정보(불러오기 이후 렌더링)</InfoHeader>
        <UserInfo>이름 : {userData.user_name}</UserInfo>
        <UserInfo>이메일 : {userData.user_email}</UserInfo>
        <UserInfo>나이 : {userData.user_age}</UserInfo>
      </Container>
      <Container>
        <UserInfo>이름 : {user1Data.user_name}</UserInfo>
        <UserInfo>이메일 : {user1Data.user_email}</UserInfo>
        <UserInfo>나이 : {user1Data.user_age}</UserInfo>
      </Container>
    </Wrap>
  );
}
const Wrap = styled.div``;
const Container = styled.div`
  padding: 25px;
`;
const InfoHeader = styled.h1`
  font-size: 24px;
  padding: 0 0 16px;
`;
const UserInfo = styled.h2`
  font-size: 20px;
`;

 

 

두 데이터를 Promise.all()을 사용하여 한번에 받아왔습니다.

유저 정보 두개가 모두 반환되기 전까지는 정보가 렌더링이 되지 않습니다.

병렬적 구조가 아닌 직렬적으로 두 데이터를 모두 받아오기를 기다려야합니다.

 

위의 현상은 각각 따로 데이터를 불러와서 해결하면 되지만 더 복잡한 컴포넌트 구조를

만났을 때 코드가 복잡해질수 있다고합니다.

따라서 데이터를 모두 불러오고 렌더링 하는 선택지가 좋은 방향성일때가 있을 수 있다고

리엑트 문서에서 설명하고 있습니다.

 

 

접근방식 3. 불러올 때 렌더링 Suspense 사용

 

이전의 방식을 살펴보면

1. 데이터 불러오기 시작

2. 데이터 불러오기 완료

3. 렌더링 시작

과 같이 불러오기가 완료가 되었을 때 렌더링을 시작하게 됩니다.

 

Suspense를 사용하면 데이터 불러오기를 완료하기 전에 렌더링을 시작합니다.

1. 데이터 불러오기 시작

2. 렌더링 시작

3. 데이터 불러오기 완료

const userDataResource = getUserDataPromise(); // 유저 정보 미리 받아오기

function ReactSuspensePage() {
  return (
      <Suspense fallback={<Loading text="유저정보 불러오는 중..." />}>
        <ProfileSuspense />
      </Suspense>
  );
}

function ProfileSuspense() {
  // 유저 정보를 다 받아왔다면 read를 통해 데이터를 읽습니다.
  const { user_name, user_email, user_age } = userDataResource.read();
  return (
    <Container>
      <InfoHeader>유저 정보(Suspense)</InfoHeader>
      <UserInfo>이름 : {user_name}</UserInfo>
      <UserInfo>이메일 : {user_email}</UserInfo>
      <UserInfo>나이 : {user_age}</UserInfo>
    </Container>
  );
}

const Container = styled.div`
  padding: 25px;
`;
const InfoHeader = styled.h1`
  font-size: 24px;
  padding: 0 0 16px;
`;
const UserInfo = styled.h2`
  font-size: 20px;
`;

Suspense의 코드 구조를 살펴보면

유저를 불러오고 렌더링 할 컴포넌트 <ProfileSuspense/><Suspense> 컴포넌트로 감쌉니다.

그리고 이전 방식에서는 if문의 분기처리로 Loading을 처리했다면

Suspense에서는 fallback 파라미터에 로딩 컴포넌트를 넘기게됩니다.

 

 

위의 화면을 렌더링할 때에 다음과 같은 과정이 수행된다고합니다.

Suspense 렌더링 과정

1. 데이터 요청

먼저 getUserDataPromise() 를 통해 데이터 요청을 시작합니다.

 

2. 컴포넌트 렌더링

<ProfileSuspense/> 의 렌더링을 시도하게됩니다.

렌더링을 시도하는 동안 getUserDataPromise() 통해 받아온 데이터를 read() 하는 과정에서

데이터가 다 불러오지 않았다면 컴포넌트의 렌더링을 정지합니다.

그 다음으로 구조상의 다른 컴포넌트의 렌더링을 시도하게됩니다.

 

3. fallback 찾고 로딩처리

위 코드상 렌더링을 시도할 다른 컴포넌트가 없습니다.

그 후에는 트리상 정지된 컴포넌트인 <ProfileSuspense/>의 부모요소중

제일 가까운 <Suspense> 컴포넌트의 fallback을 찾는다고 합니다.

fallback으로 넘긴 컴포넌트는 로딩상태를 렌더링합니다.

 

userDataResource 는 로딩이 이루어질 데이터, 그 안의 매소드 read()를 호출하면

데이터 불러오기가 완료가 되었다면 데이터를 바인딩 해주고

완료되지 않았다면 컴포넌트 렌더링을 정지합니다.

 

데이터가 계속 오면서 React는 렌더링을 다시 시도하고

userDataResource 불러오기가 성공하게 되면 <ProfileSuspense/>의 렌더링이 완료되며

fallback에 넘겨줬던 로딩상태가 화면상에서 사라지게 됩니다.

 

Suspense가 가지는 이점은 데이터 불러오기를 시작하고 컴포넌트를 렌더링하며

불러오는 도중에 fallback을 통해 로딩을 표시하고 불러오기가 완료가되면 바로 데이터를 보여줍니다.

반면의 이전의 불러오기 성공 직후 렌더링을 시작하는 방식에서도 워터폴이 나타나게 됩니다.

데이터를 불러오고 불러오기를 성공한 시점 이후에 렌더링을 시작하기 때문입니다.

 

또한 이전의 로직에서는 if문의 검사를 통해서 로딩을 처리했던걸 제거함으로써

보일러플레이트 코드를 제거할 수 있고 간단한 절차로 최선의 결과물을 나타낼 수 있습니다.

또한 두 정보를 함께 동일한 시점에 나와야하게 하는 컴포넌트들이 있다면 두 컴포넌트를 다음과 같이

<Suspense fallback={<h1>로딩중...</h1>}>
	<Profile/>
	<PostList/>
</Suspense>

<Suspense> 하나로 묶으면 두 데이터를 다 받아오는 시점에 렌더링이 되어서

필요한 디자인 변화를 적용할 수 있습니다.

 

*보일러플레이트 코드: 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말합니다.

 

 

마치며

지금까지 React의 Suspense에 대해서 공부해봤습니다.

미리 데이터를 불러오고 불러온 시점을 알아서 바인딩을 해줄 수 있는 방법,

이벤트 핸들러를 통해서 페이지 이동시 일찍 불러오기 시작하기 적용법 등은

다음 글에서 작성하려합니다.

 

 

공부 자료

 

데이터를 가져오기 위한 Suspense (실험 단계) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

댓글
최근에 올라온 글