[React] horizontal scrolling 페이지 구현하기

바질·2022년 9월 30일
0

react-scrolling

목록 보기
2/2

사용하는 것
react.js, typescript, styled-components, framer-motion, react-scroll-wheel-handler

react에서 horizontal scrolling 페이지를 만들어보자.
(일명 횡/가로 스크롤)

라이브러리의 도움을 받았다. 구글링을 해본 결과, wheel 이벤트값이 들어오는 것보다 함수 실행이 빠르다고 하는 글이 있어서 handler 라이브러리를 install했다. 홈페이지는 이쪽이다.

라이브러리 사용 없이도 시도해보았는데 잘 된다. 그래서 라이브러리는 삭제했다.

npm i react-scroll-wheel-handler

사용 방법

<ReactScrollWheelHandler
  upHandler={(e) => console.log("scroll up")}
  downHandler={(e) => console.log("scroll down")}
>
  ...
</ReactScrollWheelHandler>

console.log에 실행할 함수를 대신 넣으면 된다. 나는 여기서 페이지의 진행바를 framer-motion으로 넣어주었는데, 아주 어려웠다. 시작해보자!

Wheel Event

라이브러리를 사용한 버전이다.

import ReactScrollWheelHandler from "react-scroll-wheel-handler";


  const [slide, setSlide] = useState(true);
  const containerRef = useRef<HTMLDivElement>(null);


const down = (e: any) => {
    e.preventDefault();
    const el = containerRef.current;
    const { deltaY } = e;
    if (!el) return;

    if (deltaY > 0 && slide === true) {
      setSlide(false);
      el.scrollTo({
        left: el.scrollLeft + deltaY * 5,
        behavior: "smooth",
      });
      setSlide(true);
    }
  };

  const up = (e: any) => {
    const { deltaY } = e;
    const el = containerRef.current;

    if (!el) return;

    if (deltaY < 0 && slide === true) {
      setSlide(false);
      el.scrollTo({
        left: el.scrollLeft + deltaY * 5,
        behavior: "smooth",
      });
      setSlide(true);
    }
  };

먼저, contents를 감싸는 div를 useRef로 찾아준다. react에서는 javascript에서 쓰던 Document.querySelector()를 쓰면 안 된다. 그러면 react를 사용하는 의미가 없다.

wheel event로 들어오는 deltaY의 값을 찾고, useRef로 들어오는 element를 정의해준다. (초기값은 null)

이때, typescript를 사용하기 때문에 useRef로 찾는 element가 null일 때 return 시켜주지 않으면 에러가 난다.

휠을 위로 올렸을 때, 음(-)의 숫자를 반환하고, 아래로 내렸을 때, 양(+)의 숫자를 반환한다.
따라서, 휠의 방향에 따라 스크롤을 컨트롤 하고 싶다면 if로 조건을 걸어주어야 한다.

라이브러리를 사용하지 않은 버전이다.

크게 이점이 없으면 되도록 라이브러리를 사용하지 않으려고 한다.

 const onWheel = (e: any) => {
    const { deltaY } = e;
    const el = containerRef.current;
    if (!el) return;

    if (deltaY > 0 && slide === true) {
      setSlide(false);
      el.scrollTo({
        left: el.scrollLeft + deltaY * 5,
        behavior: "smooth",
      });
      setSlide(true);
    }
    if (deltaY < 0 && slide === true) {
      setSlide(false);
      el.scrollTo({
        left: el.scrollLeft + deltaY * 5,
        behavior: "smooth",
      });
      setSlide(true);
    }
  };

설명은 위에 있는 것을 보면 된다. 라이브러리만 없어졌을 뿐, 들어오는 이벤트는 똑같다. 휠 다운과 휠 업은 구분할 수 있어서 if 조건문으로 걸러주면 정상적으로 작동한다.

<div onWheel={(e) => onWheel(e)}>
  	...
</div>
  

이벤트를 걸어줄 때는 휠 이벤트를 걸어줄 컴포넌트(html 태그)에 onWheel을 넣어주면 된다. (e)=>onWheel(e) 화살표 함수로 넣어주어야 매개변수로 받을 수 있으니 유의하자.



if (deltaY < 0 && slide === true) {
      setSlide(false);
      el.scrollTo({
        left: el.scrollLeft + deltaY * 5,
        behavior: "smooth",
      });
      setSlide(true);
    }

slide는 상태 변수라고 생각하면 된다. 반환하는 값은 true or false인데, 초기값은 false로 해둔다. 스크롤이 움직이고 있다면 휠을 올려도 함수를 실행하지 말라는 조건을 걸어두었다.

useRef로 잡은 element의 scroll을 움직이는데, 움직이는 값은 element의 scrollLeft + deltaY * 5 이다.

scrollLeft는 스크롤을 이동한 값
deltaY * 5 는 어느 정도의 속도로 움직일지 결정하는 것이다.
느리게 움직이고 싶다면 값을 줄이고 빠르게 움직이고 싶다면 값을 올리면 된다.

behavior: "smooth"
이 옵션을 설정하지 않으면 드득드득하고 움직이는 걸 볼 수 있으니 써두자.

모든 것이 실행되고 나서 slide의 상태를 true로 다시 변경한다. 그래야 휠을 움직였을 때 실행할 수 있다.

페이지 진행 바 애니메이션

이거 때문에... 고생을 꽤 했다. frame-motion 홈페이지에 들어가면 여러 예시들이 많으니 구경해보는 것도 좋다. 그럼, 시작.

  const containerRef = useRef<HTMLDivElement>(null);
  const { scrollXProgress } = useScroll({ container: containerRef });
  
   <AnimatePresence>
          <motion.svg
            key={"circleWrap"}
            style={{
              position: "fixed",
              left: "10px",
              bottom: "20px",
              transform: "rotate(-90deg)",
              stroke: "whitesmoke",
            }}
            initial={{ opacity: props.show === "true" ? 1 : 0 }}
            animate={{
              opacity: props.show === "true" ? 1 : 0,
              transition: { duration: 1 },
            }}
            width="100"
            height="100"
            viewBox="0 0 100 100"
          >
            <circle
              key={"bgCircle"}
              cx="50"
              cy="50"
              r="30"
              pathLength="1"
              style={{
                strokeDashoffset: 0,
                strokeWidth: "15px",
                fill: "none",
                stroke: "whitesmoke",
                opacity: 0.3,
              }}
            />
            <motion.circle
              key={"moveCircle"}
              cx="50"
              cy="50"
              r="30"
              pathLength="1"
              style={{
                pathLength: scrollXProgress,
                stroke: "whitesmoke",
                strokeWidth: "15px",
              }}
            />
          </motion.svg>
     </AnimatePresence>

frame-motion을 안다는 가정 하에 진행하므로, 해당 라이브러리를 모른다면 뒤로 가기 버튼을 누르길 바란다.

기본적으로 페이지가 얼마나 남았는지 알려주는 bar를 만든다면 페이지의 상단이나 하단에 넣는다. 그런 방법을 써도 괜찮지만, 적용하는 페이지가 horizontal scrolling 페이지이므로 circle을 선택했다.

코드를 보면 motion을 쓴 것과 쓰지 않은 것이 있는데, 쓰지 않은 것은 뒷배경이 될 circle이고 motion을 쓴 circle은 페이지의 scroll 진행에 따라 채워질 예정이다.

scrollXProgress를 얻기 위해 useRef로 element를 가져와 frame-motion이 지원하는 useScroll을 사용한다.
(홈페이지에 소스 코드도 있으니 보는 걸 추천, 나 또한 소스 코드를 보고 필요한 것만 가져온 것이다.)

만약, 내가 사용하는 것이 기본적인 width의 bar였다면 style의 width값에 scrollXProgress 값을 넣었을 것이다.
지금 작성한 코드는 svg 애니메이션과 동일하니 pathLenght에 scrollXProgress 값을 넣어준다.

간단하게 스크롤에 따라 circle이 채워지는 걸 볼 수 있다.
그렇다면, 당신은 운이 좋은 사람이다...

Error

나는 여기서 이슈가 발생했다. 이미 circle이 채워진 상태로 움직이지 않았다. 공식 홈페이지의 소스 코드를 까봐도, 에러가 났는지 console.log를 찍어도 나오지 않았다.

답은 css에 있었다. 기본적으로 wrap이 될 div의 width값을 정하면 안 된다. 해당 값이 고정되어 있으면 (px, %, vw 전부 동일) circle은 100%로 채워져있다.

이걸 해결하기 위해서는 width값을 생략하고 flex를 통해 레이아웃을 배치해주자. 그렇게 하면 scroll을 진행하면서 circle이 채워지는 걸 볼 수 있다.
내가 이거 때문에 시간 많이 잡아먹었다...

0개의 댓글