[React] Fade-in Fade-out 하는 Text 컴포넌트 만들기

삔아·2023년 8월 12일
0

React

목록 보기
2/2
post-thumbnail

개요

개인적으로 진행하고 있는 프로젝트중에 위 사진처럼 fade-in , fade-out 의 효과가 있는 컴포넌트를 만들었습니다.
처음 기획을 할 때, 해당 컴포넌트는 라이브러리를 쓰는게 좋을 것 같았지만 리액트와 친해지는 것이 목표이기 때문에 직접 만들어보고자 시작하게 되었습니다.

많이 부족한 부분이 있는 컴포넌트지만 이 컴포넌트를 만들면서 어떤 고민을 했는지 어떻게 만들게 되었는지를 기록하고자 작성하게 되었습니다. (완전 우당탕탕 기록 입니다)

진행하고 있는 프로젝트는 이러한 환경에서 진행되고 있습니다.
1. React, TypeScript를 이용합니다.
2. emotion 라이브러리로 CSS-In-JS 형식을 이용 합니다.


첫번째 생각

가장 먼저 숫자를 어떻게 하나씩 보여줄까에 대해 중점으로 생각하였습니다.

(1) 들어온 숫자의 값을 number Array 로 바꾸어준다.
ex) 456 -> [4,5,6]
(2) useState 를 이용하여 showNumberfor 문을 이용하여 해당 배열의 idx로 숫자를 셋팅해준다.
ex) setShowNumber(numbers[i]);
(3) for문 에서 순회하는 부분을 setTimeout 혹은 setInterval 을 이용하여일정 시간마다 setShowNumber 가 되도록한다.

setTimeout VS setInterval

여기서 setTimeoutsetInterval 두가지 중 어떤 함수를 이용해야 더 적절할지에 대해 생각을 했습니다.

setTimeout : 일정 시간이 지난 후에 함수를 실행


일정 시간이 지난 후 다른 함수를 호출하는 데 사용되는 시간 이벤트 함수이지만 함수를 한 번만 실행합니다.

위의 그림은 중첩된 setTimeout 을 이용한 그림이지만 setInterval 과 차이가 확연히 보여지는 좋은 그림이라 가져왔습니다.
그림처럼 setTimeout 은 지연 간격을 보장합니다. 이 함수를 실행하는데에 몇초가 걸리든 꼭 지연 시간 이후에 해당 함수가 실행됩니다.

주로 사용자 입력 후 일정 시간 동안 입력 대기 후 동작을 실행하거나, 애니메이션 시작을 지연시키고 나중에 시작해야 할 때 해당 함수를 사용합니다.

setInterval: 일정 시간 간격을 두고 함수를 실행

특정 시간 후에 함수를 호출하며 지정된 시간 간격에 따라 실행이 연속적으로 발생합니다.

사진처럼 100ms 라는 지연 간격을 가지고 있지만, 함수가 실행되는데에 소모한 시간 까지도 100ms 에 포함시키기 때문에 지연 간격은 실제 100ms 보다 짧아집니다.
만약, 함수를 실행하는 데 걸리는 시간이 명시한 지연 간격보다 길다면 엔진이 함수의 실행이 종료될 때 까지 기다려준 후 스케줄러를 확인하고, 그 다음 호출을 바로 시작합니다.
따라서 함수 호출에 걸리는 시간이 지연 간격보다 길다면 지연 간격이 따로 없이 계속 호출하게 되는 현상을 가지게 될 수 있습니다.

주로 실시간 데이터 업데이트, 주기적으로 화면 갱신을 위한 애니메이션 루프, 실시간 채팅 시스템 에서 해당 함수를 많이 사용합니다.

최신 웹 개발에서는 setTimeout 을 활용하여 재귀적인 호출을 통해 반복적인 작업을 수행하는 방식을 더 선호 합니다. 작업이 시작된 후 일정시간이 지난후에 다음 작업이 스케줄링 되기 때문에 사이드이펙트를 방지하는데에 이점을 가져올 수 있습니다.

setTimeout 을 채택하다.

우선 해당 컴포넌트는 일정한 시간을 가지고 숫자를 보여주어야 한다. (지연 간격 보장 필요) 분명한 목적이 있기 때문에 더 깊게 고민하지 않고 setTimeout 을 사용하여 컴포넌트를 작성을 시작합니다.

React Hook useEffect에 불필요한 종속성인 'ref.current'가 있습니다. 이를 제외하거나 의존성 배열을 제거하세요. 'ref.current'와 같은 변경 가능한 값은 변경해도 컴포넌트가 다시 렌더링되지 않으므로 유효한 의존성이 아닙니다. (react-hooks/exhaustive-deps)

처음 코드를 작성할 때, 단순히 값을 저장하는 용도의 useRef 의 값도 useEffect 의 종속성에 넣어야하나 고민을 했다가 해당 페이지에서 좋은 답변을 얻었습니다. (비록 페이지에서의 질문의 의도와는 다르지만요)

fade-in , fade-out 을 적용하자.

어찌저찌 숫자를 하나씩만 보여지도록 작성을 하였습니다.
그럼 이 숫자가 바뀔때 마다 fade-in , fade-out 효과를 주기만 하면 끝이겠네요. (진짜 여기서 너무 너무 너무.... 너무 꼬아서 생각했는지 생각보다 시간이 많이 들었습니다 ㅠㅠ)

const fadeStyle = css`
  .fade-in {
    opacity: 1;
    animation: fadeIn ease-in-out 1s;
  }

  .fade-out {
    opacity: 0;
    animation: fadeOut ease-in-out 1s;
  }

  @keyframes fadeIn {
    0% {
      opacity: 0;
    }

    100% {
      opacity: 1;
    }
  }

  @keyframes fadeOut {
    0% {
      opacity: 1;
    }

    100% {
      opacity: 0;
    }
  }
`;

이런식으로 css를 작성해서 숫자가 바뀔 때 마다 fade-in , fade-out 효과를 적용해주면 되지 않을까 했었습니다.

즉, 내가 원하는 순서대로 fade-in , fade-out 을 적용해야하는데, 이 부분에 있어서 Promise 를 사용하기로 합니다.

Promise & async/await

PromisesetTimeout 비동기 작업을 순차적으로 실행하고 그 작업이 완료 되어야 다음 작업으로 넘어갈 수 있습니다.
따라서 Promiseasync / await 을 사용하여 비동기 흐름을 관리하기 위해 채택 하였습니다.

 useEffect(() => {
    ...
    const setTimeNumber = async () => {
      for (let i = 0; i < numbers.length; i++) {
        await new Promise<void>((resolve) => {
          timeouts.current[i] = setTimeout(() => {
            setShowNumber(numbers[i]);
            setFadeInOut((prevFadeInOut) =>
              prevFadeInOut === 'fade-in' ? 'fade-out' : 'fade-in'
            );
            resolve();
          }, 1500);
        });
      }
    };

    setTimeNumber();
    return () => {
      timeoutsCopy.forEach((val) => {
        clearTimeout(val);
      });
    };
  }, []);
  ...
  1. Promise 객체를 생성합니다. resolve 를 통해 Promise 가 완료 됨을 알릴 수 있습니다.
  2. await 키워드를 사용하여 Promise 가 완료될 때 까지 기다립니다.
  3. setTimeout 을 사용하여 숫자를 배치하고 fadeInOut 을 업데이트 합니다. 그 후 다음 숫자로 넘어갑니다.
  4. resolve() 가 호출되면 해당 Promise 는 완료 됩니다.

상상력은 풍부했으나.... 😭 의도한 바가 아닌 결과물이 계속 나오게 됩니다.

 setFadeInOut((prevFadeInOut) =>
	prevFadeInOut === 'fade-in' ? 'fade-out' : 'fade-in'
);

사실은 이 부분의 코드가 잘못 되었다고 생각하는데, 어떻게 맞추어야 할지에 대해 고민을 많이 했습니다. ㅠㅠ

CSSTransition

그러던 중 react-transition-group 을 발견하게 됩니다.

리액트의 컴포넌트에 transition 효과를 쉽게 줄 수 있는 공식 라이브러리 라니, 해당 컴포넌트 뿐 아니라 프로젝트 내에서도 유용하게 쓸 것 같아 해당 라이브러리를 바로 설치 했었습니다.

React-transition-group 에서 제공 해주는 컴포넌트 중 CSSTransition 을 사용해보고자 하였습니다.
이 컴포넌트는 CSS 애니메이션을 적용할 수 있도록 지원하며 CSS 클래스를 추가, 제거 또는 변경하여 애니메이션을 적용 할 수 있습니다.

export function InstantNumber({ value }: Props) {
  ...
  return (
    <div>
      <CSSTransition
        in={showNumber !== null}
        timeout={500}
        classNames='fade'
        unmountOnExit
      >
        <Txt css={fade} typography={'h2'}>
          {showNumber}
        </Txt>
      </CSSTransition>
    </div>
  );
}

const fade = css`
  &.fade-enter {
    opacity: 0;
  }

  &.fade-enter-active {
    opacity: 1;
    transition: opacity 0.5s ease-in;
  }

  &.fade-exit {
    opacity: 1;
  }

  &.fade-exit-active {
    opacity: 0;
    transition: opacity 0.5s ease-out;
  }
`;

중간에 코드를 살짝 변경하긴 했으나 CSS Transition 을 이용하여 코드 작성을 하였습니다.
하지만 당연히 제가 의도한 결과가 나오지 않았습니다. (신나서 공부를 제대로 안하고 적용한 자의 최후 입니다)

이 라이브러리는 React 컴포넌트의 라이프사이클과 함께 동작하여 특정 상태 변화에 따라 CSS 클래스를 토글함으로써 CSS 애니메이션을 적용 할 수 있습니다.
즉, 컴포넌트가 마운트 ~ 언마운트 ~ 이런 형식이 되어야 제가 의도한대로 나오게 될 것 같았습니다.

그러나, 여기서 컴포넌트를 마운트, 언마운트 시킬 정도인가? 라는 의문점이 들게 됩니다.

진행하고 있는 프로젝트에서 이 컴포넌트를 만들게 된 이유는 숫자를 하나씩 보여주고 그 후에 Input 창에 해당 숫자를 역순으로 입력 하는 것 입니다.
따라서 이 숫자의 길이는 최대로 14자로 생각하고 있으며, 12번에 걸쳐서 문제를 보여지게 할 예정인데, 그 숫자의 길이마다, 매 문제마다 마운트, 언마운트 되는 것은 렌더링 성능을 저하시킬 수 있을 것 같아 좋은 방법이 아니라고 생각했습니다.

다시 작성한 코드를 전부 지우고 다른 방법을 생각해봅니다.

두번째 생각

useEffect 를 두번 쓸까?

개인적으로 저는 기본 for문 을 좋아하는 편은 아닙니다. 여기서부터 생각을 다시 시작하게 됩니다.
그러면, Props 로 받는 숫자를 배열로 만들면, 그 idx 값 을 보여주면 되지 않을까?
예를들어 { numberArray[showNumberIdx] } 처럼 만들면 for문을 안써도 될 것 같단 생각이 들었습니다.

1) idx를 +1 씩 업데이트도 해야하고....
2) fadeStyle 적용을 위해 fade-in / fade-out 상태 변경 해주어야하고...
3) 근데 이 idx는 배열의 길이보다 길면 안되고...

이 부분을 각각 useEffect 를 사용하여 코드를 작성 하였습니다.

 useEffect(() => {
    const fadeTimeout = setTimeout(() => {
      setFadeProp((prevFadeProp) => ({
        fade: prevFadeProp.fade === "fade-in" ? "fade-out" : "fade-in"
      }));
    }, FADE_INTERVAL_MS);

    if (showNumberIdx >= showNumberArrayLength) {
      clearTimeout(fadeTimeout);
      return;
    }

    return () => clearTimeout(fadeTimeout);
  }, [fadeProp, showNumberIdx, showNumberArrayLength]);

  useEffect(() => {
    const numberTimeout = setTimeout(() => {
      setShowNumberIdx((prev) => prev + 1);
    }, FADE_INTERVAL_MS * 3);

    if (showNumberIdx >= showNumberArrayLength) {
      clearTimeout(numberTimeout);
      return;
    }

    return () => clearTimeout(numberTimeout);
  }, [showNumberIdx, showNumberArrayLength]);

동작 순서를 fade-in/out -> showNumber -> fade-in/out 이렇게 잡아놓고, 첫번째 useEffect 부분을 fade-in/out 업데이트 되도록 진행 하였습니다.
그리고 두번째는 showNumber 의 숫자를 바꾸도록 진행 했습니다.

여기서 FADE_INTERVAL_MS 는 1500으로 설정하였습니다.
fade-in/outtransition 을 1.5s 로 잡아놓은 부분도 있고, 자연스럽게 fade-in/out 후 숫자가 보여질때 까지는 FADE_INTERVAL_MS * 3 이 적절하다고 생각하여 지연 간격을 설정하였습니다.

완성..?

어찌저찌 의도한 바 대로 잘 굴러가네요.
하지만, 아쉬운 부분이 너무 많이 남는 컴포넌트 입니다. 지식의 부족으로 여기서 더 발전을 못한다니, 너무 아쉬워요. (ㅠㅠ)
꼭 리액트와 절친이 되어 이 컴포넌트를 다시 리팩토링 하고 싶은 욕심이 가득합니다.

끝으로..

사실은 너무나도 부끄러운 제 코드를 공개해야하지만... 앞서 말씀 드렸듯이 컴포넌트를 하나 만들때마다 많은 생각을 하는데 한번쯤은 저의 생각 순서를 글로 정리해보고 싶었습니다.
이 내용보다 더~ 많은 고민을 하는데(수많은 에러도 만나고..) 줄일려고 나름대로 애를 써봤는데, 줄여진게 맞나 생각이 드네요.

리액트와 친해지고싶어서 애는 쓰는데 마음대로 되지 않아서 너무 슬프지만, 열심히 나아가려고 합니다. 나중에는 이 컴포넌트를 리팩토링 할만큼 실력을 갖추고 다시 한번 글을 쓰면 재밌을 것 같네요. 😄

profile
Frontend 개발자 입니다, 피드백은 언제나 환영 입니다

0개의 댓글