리액트로 로딩 상태를 나타내는 진행바 만들기

이나리·2022년 6월 25일
0

사용자의 로딩 상태 인지

메인 페이지에서 상품 리스트를 모두 가져올 때, 사용자에게 현재 데이터를 가져오는 중임을 표시하는 로딩상태 바를 만들고 싶습니다.

진행바를 만들기 위해서는 바를 나타내는 요소의 width 값이 필요합니다.
이 width 값은 로딩 상태의 %를 나타내는데, 문제는 로딩 상태값이 boolean 형태의 값이기 때문에 이걸 어떻게 %로 표현할 수 있을까요?

  • false: 0% → true: 100%
    단순하지만, 이 방법의 문제는 만약 데이터를 가져오는 데 시간이 어느 정도 소요된다면 과연 그만큼의 공백을 사용자가 로딩 중인 상태라고 인지할 수 있을까요?

  • false: 10% or 30% → true: 100%
    0%보다는 특정 %값을 주면 시각적으로 좀 낫지만, 역시 단계적으로 높아지는 %값이 아니기 때문에 이 방법 역시 로딩 시간이 길어지면 사용자는 로딩 상태임을 인지하지 못합니다.

  • 시각적인 움직임을 줄 수 있는 css 애니메이션을 추가하자.
    width 값을 단계적으로 나눌 수 없다면, 요소에 시각적인 움직임을 만들어보는 게 좋을 것 같습니다.
    무엇이든 움직여야 사용자는 최소한 이 페이지가 뭘 하고 있구나 생각할 것입니다.
    하지만, width 값을 나눠서 전달하지 못하는 2가지 단계에서 줄 수 있는 애니메이션이 뭐가 있을까요?


css keyframes 애니메이션

w3school의 css keyframes 애니메이션 예제를 보면, 이런 예제가 있습니다.

이 예제에 착안하여, 특정 width 값의 바를 생성하여 로딩 상태에서는 좌우로 계속 움직이고, 로딩이 완료되면 width 값을 100%로 변경하는 진행바를 만들어 보려고 합니다.

코드는 다음과 같습니다.

interface Props {
  isLoading: boolean;
}

function ProgressBar({ isLoading }: Props) {
  return (
    <div css={styles.wrapper}>
      <div css={styles.bar(isLoading)} />
    </div>
  );
}

// emotion css 적용 부분
const LeftAndRightMoving = keyframes({
  from: { left: 0 },
  to: { left: '90%' },
});

const styles = {
  wrapper: css({
  	width: '100%',
  	backgroundColor: '#f8f8f8',
  	overflow: 'hidden',
  }),
  bar: (isLoading: boolean) =>
    css({
      position: 'relative',
      width: isLoading ? '30%' : '100%',
      height: 3,
      backgroundColor: '#ff8080',
      animation: isLoading
        ? `${LeftAndRightMoving} 1s linear infinite alternate`
        : undefined,
    }),
};

로딩 완료의 표시

사용자가 인지할 수 있는 로딩 상태를 구현하고 보니, 로딩이 완료된 이후 이 로딩바가 100% width 값으로 멈춰 있습니다.
그런데 대부분 로딩이 완료되면, 로딩 상태를 나타내는 것들은 화면에서 사라집니다.

css 해결법

이 부분을 고려하여, 실제 로딩바를 감싸는 래퍼 바에 로딩 상태에 따른 opacity, visibility 속성을 추가하고 transition 속성을 부여하여 애니메이션 같은 효과를 추가합니다.
또한, 100%를 표시를 명확하게 보여주고 사라지기 위해 transitionDelay 속성을 추가하여 transition 을 조금 늦추도록 합니다.

const styles = {
  wrapper: (isLoading: boolean) =>
  	css({
      opacity: isLoading ? 1 : 0,
      visibility: isLoading ? 'visible' : 'hidden',
      transitionDelay: isLoading ? '0' : '0.5s',
      width: '100%',
      backgroundColor: '#f8f8f8',
      overflow: 'hidden',
      transition: 'opacity 1s, visibility 1s',
    }),
};

삽질의 기록

사실 css로 구현하는 간단한 방법을 찾기 전에, 다소 불필요한 코드를 하나 작성했었습니다.

조건부 렌더링이 되는 컴포넌트에 대해 언마운트시 애니메이션을 적용하는 방법이었는데요.
팝업창을 닫거나 어떤 요소를 삭제할 때, css 애니메이션을 적용하기 위해 사용하는 방법입니다.

조건부 렌더링의 경우에는 레이아웃에서 요소가 사라지기 때문에, css 스타일 속성을 적용할 수 없게 됩니다.
즉, 컴포넌트가 언마운트되면서, css transition duration 속성에 적용한 값만큼의 시간이 보장되지 않는 문제가 발생합니다.

이 경우에는 새로운 상태값을 하나 만들어서, 닫기나 삭제 버튼을 누를 때, setTimeout 함수를 이용해 그 시간만큼 컴포넌트의 언마운트를 지연시키면, 앞서 적용되지 않았던 css transition duration 속성이 적용이 되어 애니메이션을 실행할 수 있습니다.

interface Props {
  isLoading: boolean;
}

function ProgressBar({ isLoading }: Props) {
  const [isMounted, setIsMounted] = useState<boolean>(false);

  useEffect(() => {
    let timeoutId: number;
    if (isLoading) {
      setIsMounted(true);
    } else {
      timeoutId = window.setTimeout(() => {
        setIsMounted(false);
      }, 1000);
    }
    return () => {
      clearTimeout(timeoutId);
    };
  }, [isLoading]);

  return (
    <div isMounted={isMounted}>
      <div css={styles.bar(isLoading)} />
    </div>
  );
}

const styles = {
  wrapper: (isMounted: boolean) =>
    css({
      width: '100%',
      backgroundColor: '#f8f8f8',
      overflow: 'hidden',
      transition: 'opacity 1s',
      opacity: isMounted ? 1 : 0,
    }),
};

하지만 이 코드에서 간과하고 있었던 점이 있었습니다.

바로 제가 만든 로딩바 컴포넌트는 조건부 렌더링을 하지 않는다는 것입니다.
레이아웃에서 스타일만 사라질 뿐, 요소는 그대로 남아있기 때문에 css 스타일 적용이 가능한데, 불필요하게 상태값을 추가하여 쓸데없는 렌더링만 더 발생시키고 있던 것이었죠.

이 상황에서 로딩바 컴포넌트에 조건부 렌더링을 추가하면, 오히려 코드가 제대로 동작하지 않습니다.
왜일까요?

props로 전달받는 값에 isMounted 값이 의존하고 있는데, 전달받는 값이 true가 되는 순간, 이 컴포넌트는 언마운트되면서 setTimeout 함수가 실행될 시간을 주지 않기 때문입니다.

위 코드는 논리가 맞지 않는 이상한 코드였습니다.
조건부 렌더링을 위해 작성한 코드인데, 조건부 렌더링을 추가하면 오히려 코드가 원하는 대로 동작하지 않는...

0개의 댓글