본문 바로가기
css

무한 루프 React 구성 요소

by code-box 2022. 2. 22.
반응형

얼마 전, 제가 작업하던 프로젝트가 특이한 요구 사항을 제시했습니다. 기본적으로 콘텐츠의 한 부분이 화면을 무한정 미끄러져 지나가야 한다는 것입니다. 텍스트, 이미지 등 무엇이든 될 수 있으며 상황에 따라 왼쪽 또는 오른쪽으로 그리고 다른 속도로 미끄러져야 합니다. 무한 루프 구성 요소를 생성하는 것은 어떨까요?

이것은 어느 정도 보이는 대로다.

추가적인 요구사항은 콘텐츠를 상위 요소의 전체 너비(대개 뷰포트의 전체 너비)를 포괄하기 위해 필요한 만큼 수평적으로 반복해야 한다는 것이었다. 큰 이미지는 단지 몇 가지 예만 필요로 하는 반면 작은 이미지는 더 많은 예만 필요로 합니다. 저는 단지 일부 콘텐츠를 구성 요소에 넣고 속도와 방향을 전달한 후, 그것이 나머지를 처리하도록 할 수 있기를 원했습니다.

<InfiniteLooper speed="1" direction="left">
      // the stuff you want to loop
  </InfiniteLooper>
 

이 구성 요소는 애니메이션뿐만 아니라 콘텐츠를 화면 전체에서 반복하도록 하는 역할을 담당해야 합니다. 일단 애니메이션을 보도록 하겠습니다.

콘텐츠 애니메이션화

우리가 해야 할 일은 콘텐츠의 각 인스턴스를 100% 수평으로 번역하는 것입니다. 여러 인스턴스를 나란히 놓고 이 작업을 수행하면 각 인스턴스의 끝 위치가 다음 인스턴스의 초기 위치가 되고 다시 초기 상태로 스냅됩니다. 이것은 연속적인 수평 운동이라는 인상을 줍니다.

요소를 100% 변환하는 것은 상위 요소의 너비가 아니라 자체 너비의 100%를 의미합니다.

자, 시작하겠습니다.

 
function InfiniteLooper({
      speed,
      direction,
      children,
}: {
                             speed: number;
                             direction: "right" | "left";
                             children: React.ReactNode;
                         }) {
      const [looperInstances, setLooperInstances] = useState(1);
      const outerRef = useRef<HTMLDivElement>(null);
      const innerRef = useRef<HTMLDivElement>(null);

      return (
              <div className="looper" ref={outerRef}>
                <div className="looper__innerList" ref={innerRef}>
                  {[...Array(looperInstances)].map((_, ind) => (
                                <div
                                  key={ind}
                                className="looper__listInstance"
              style={
                                animationDuration: `${speed}s`,
                                animationDirection: direction === "right" ? "reverse" : "normal",
              }
            >
                {children}
            </div>
          ))}
                  </div>
      </div>
    );
}
@keyframes slideAnimation {
    from {
          transform: translateX(0%);
                                }
    to {
          transform: translateX(-100%);
                                }
}

.looper {
    width: 100%;
      overflow: hidden;
}

.looper__innerList {
    display: flex;
    justify-content: center;
    width: fit-content;
}

.looper__listInstance {
    display: flex;
    width: max-content;

    animation: slideAnimation linear infinite;
}

loperInstances는 내용이 반복될 횟수를 정의합니다. 시작하기 위해 하드코드만 하면 되지만, 더 나아가 동적으로 작동하는 방법도 알아보겠습니다. CSS는 0%에서 -100%까지 번역할 수 있는 키프레임 애니메이션이 있으며, 우리가 패스하는 소품에서 정해진 지속시간과 방향을 가지고 있습니다.

기본적으로 왼쪽에서 오른쪽으로 미끄러지면 내용이 -100%에서 0%로 번역되고, 오른쪽에서 왼쪽으로 그 반대 현상이 발생합니다.

우리가 제대로 여행하고 싶을 때 -100에서 0으로 가는 것은 이상하게 보일 수 있습니다. 0부터 시작해서 100까지 가는 게 어때? 그러나 그렇게 하면 콘텐츠의 맨 왼쪽 인스턴스는 100으로 변환되는 동안 빈칸을 남겨 루프에 대한 전체 인상을 깨트릴 것입니다. -100부터 시작하여 맨 왼쪽의 항목은 화면을 벗어나기 시작하며, 화면 뒤에 공백을 남기지 않습니다.

 

또한 스피드 프로펠은 애니메이션 지속 시간에 의해 직접 사용됩니다. 즉, 값이 높을수록 속도가 느려집니다.

파이어폭스에서 애니메이션이 때때로 약간 우스꽝스러울 수 있다는 것을 알 수 있습니다. 솔직히 아직 크게 문제가 되진 않았지만, 저는 이것을 개선할 방법을 찾지 못했습니다. 어느 쪽이든 결국엔 해결해야 할 문제죠

내용 반복

다음으로 우리가 배치하는 전체 영역을 포괄하기 위해 내용을 몇 번 반복해야 하는지 계산해야 합니다. 기본 아이디어는 InnerRef와 OuterRef의 폭을 비교하고 loper를 설정하는 것입니다.그에 따른 인스턴스. 이런 식입니다.

export default function InfiniteLooper({
      speed,
      direction,
      children,
}: {
                                            speed: number;
                                            direction: "right" | "left";
                                            children: React.ReactNode;
                                        }) {
      const [looperInstances, setLooperInstances] = useState(1);
      const outerRef = useRef<HTMLDivElement>(null);
      const innerRef = useRef<HTMLDivElement>(null);

      const setupInstances = useCallback(() => {
                if (!innerRef?.current || !outerRef?.current) return;

                const { width } = innerRef.current.getBoundingClientRect();

                const { width: parentWidth } = outerRef.current.getBoundingClientRect();

                const instanceWidth = width / innerRef.current.children.length;

                if (width < parentWidth + instanceWidth) {
                              setLooperInstances(looperInstances + Math.ceil(parentWidth / width));
                }
      }, [looperInstances]);

      useEffect(() => {
                setupInstances();
      }, []);

      return (
              <div className="looper" ref={outerRef}>
                <div className="looper__innerList" ref={innerRef}>
                  {[...Array(looperInstances)].map((_, ind) => (
                                <div
                                  key={ind}
                                className="looper__listInstance"
              style={
                                animationDuration: `${speed}s`,
                                animationDirection: direction === "right" ? "reverse" : "normal",
              }
            >
                {children}
            </div>
          ))}
                  </div>
      </div>
    );
}
 

설정인스턴스 함수는 외부 참조 폭과 내부 참조 폭을 비교합니다. innerWidth(모든 콘텐츠의 너비)가 상위 콘텐츠의 너비에 1개의 인스턴스를 더한 값보다 작으면 로퍼를 늘려야 합니다.인스턴스. parentWidth/width를 사용하여 필요한 인스턴스의 수를 대략적으로 계산합니다. 추가 instanceWidth를 사용하여 안전 여유도를 제공합니다. 그렇지 않으면 구성 요소의 가장자리에 "공백" 공간을 둘 수 있습니다.

반응성은요?

잘됐네요, 이제 작동하는 부품이 생겼네요! 하지만 아직 반응이 좋지 않아요. 다른 화면에서는 잘 작동하겠지만, 어떤 이유로 용기 요소 폭이 늘어나면 어떡하죠? (네, "어떤 이유"로 볼 때, 저는 대부분 개발자들이 강박적으로 화면의 크기를 조정하는 것을 의미합니다.)

이것은 setup을 호출하는 크기 조정 이벤트 수신기를 추가하여 해결할 수 있습니다.다시 인스턴스:

useEffect(() => {
      window.addEventListener("resize", setupInstances);

      return () => {
              window.removeEventListener("resize", setupInstances);
      };
}, []);
 

하지만 한가지 문제가 있다: 만약 로퍼가인스턴스 수가 증가하면 새로운 요소가 렌더링되지만 CSS 애니메이션은 동기화되지 않고 무작위로 겹치거나 깜박이는 것을 볼 수 있습니다. 이것을 고치려면 어떻게든 애니메이션을 리셋해야 합니다. useState로 다시 렌더링을 강제하는 것은 작동하지 않습니다. 이 경우 각 인스턴스의 애니메이션 속성을 상위 항목에 data-animate="false"를 설정한 다음 다시 "true"로 전환하여 애니메이션을 재설정합니다. 데이터를 토글할 때 약간의 지연이 필요합니다. 애니메이션을 적용하고 강제로 리플로우합니다.

function resetAnimation() {
      if (innerRef?.current) {
              innerRef.current.setAttribute("data-animate", "false");

              setTimeout(() => {
                        if (innerRef?.current) {
                                    innerRef.current.setAttribute("data-animate", "true");
                        }
              }, 50);
      }
}

function setupInstances() {
      ...

          resetAnimation();
}

CSS 업데이트:

.looper__innerList[data-animate="true"] .looper__listInstance {
    animation: slideAnimation linear infinite;   
}

.looper__listInstance {
    display: flex;
    width: max-content;

    animation: none;
}

여기서 저는 데이터 속성을 단일 요소(.looper_innerList)에만 설정하고 CSS를 통해 어린이 애니메이션을 변경하기로 했습니다. 각 하위 요소를 ResetAnimation 함수에서 직접 조작할 수도 있지만, 개인적으로는 이전 솔루션이 더 간단합니다.

 

마무리하기

그게 다야! 더 나아가 애니메이션 재생 상태 속성을 통해 애니메이션을 일시 중지하고 재생할 수 있는 소품을 전달하거나 애니메이션 지속 시간을 몇 초로 단축하는 대신 애니메이션 속도에 대한 보다 깔끔한 솔루션을 제공할 수 있습니다. 세로 애니메이션도 넣을 수 있을 것 같아요.

이를 통해 프로젝트에 필요한 이상한 시각적 요구 사항을 충족하기 위해 리액트 구성 요소에서 간단한 CSS 애니메이션을 사용하는 방법을 알 수 있기를 바랍니다.

안전하게 지내세요!

댓글