특정 위치에서 가로스크롤 만들기

장세진·2024년 11월 17일

React

목록 보기
4/4
post-thumbnail

들어가며

웹 사이트를 탐험해보다가 특정 위치에서 가로스크롤이 되는 페이지를 봤던 기억이 있습니다. 어떻게 구현했을지 생각해보며 리액트로 구현해봤습니다.

설계

태그구조 설계

  • 가로로 배열된 이미지 리스트를 만들기 위해 imageItemListEl(c)라는 요소를 설정했습니다.
  • 이 리스트를 sticky 속성을 가진 imageItemListStickyContainerEl(b)로 감싸야겠다고 생각했습니다. 이를 통해 스크롤 시 고정된 위치에 있도록 했습니다.
  • 전체 구조를 relative로 설정된 containerEl(a)로 감싸면서, sticky가 제대로 작동할 수 있도록 설계했습니다.
  • 이제 imageItemListEl(c)가 translateX 로 움직여야하는 값 만큼 containerEl(a)의 높이를 계산하면 됩니다.

containerEl(a) 의 높이 계산

  • containerEl(a)의 높이를 어떻게 설정할지 고민했습니다. imageItemListEl(c)의 clientWidth를 사용하여, 이 리스트를 세로로 세웠을 때 필요한 높이를 상상했습니다. 이 값을 높이를 계산하는 시작점으로 삼았습니다.
  • imageItemListStickyContainerEl(b)의 offsetWidth만큼의 imageItem들은 이미 화면에 노출되고 있으니 스크롤에 포함되지 않게 제외했습니다.
  • imageItemListStickyContainerEl(b)의 offsetHeight는 스크롤이 모두 일어날 때까지 imageItemListEl(c)가 화면에 노출되어야 하므로 포함시켰습니다. 이 값을 더해주지 않으면 imageItemListEl(c) 리스트가 모두 보임과 동시에 스크롤 영역 밖으로 이동합니다.

imageItemListEl(c) 의 translateX 계산

  • translateX 값이 적용 되는 시점은 containerElRef(a)의 offsetTop이 window.scrollTop보다 작아질 때입니다. 이때부터 스크롤된 양에 따라 translateX가 1:1 비율로 변환되도록 했습니다.
  • translateX 값의 적용이 끝나는 시점을 찾기보다 Math.max와 Math.min을 사용하여 imageItemListElRef(c)가 보이는 영역을 넘어가지 않도록 했습니다.

리사이즈 이벤트 처리

  • 창 크기가 변경될 때, containerElRef(a)의 높이를 재설정해야겠다고 판단했습니다. 이를 통해 적절한 스크롤 양을 유지할 수 있겠다고 생각했습니다.

빠른 구현을 위해 인라인 스타일을 사용하여 구현하였습니다.

결과코드

import { useEffect, useRef } from 'react';

export const HorizontalScrolling = () => {
  const containerElRef = useRef<HTMLDivElement | null>(null);
  const imageItemListStickyContainerElRef = useRef<HTMLDivElement | null>(null);
  const imageItemListElRef = useRef<HTMLUListElement | null>(null);

  const setContainerElHeight = () => {
    if (
      !containerElRef.current ||
      !imageItemListElRef.current ||
      !imageItemListStickyContainerElRef.current
    )
      return;

    containerElRef.current.style.height =
      imageItemListElRef.current.offsetWidth -
      imageItemListStickyContainerElRef.current.offsetWidth +
      imageItemListStickyContainerElRef.current.offsetHeight +
      'px';
  };

  const setImageItemListElTranslateX = () => {
    if (
      !containerElRef.current ||
      !imageItemListElRef.current ||
      !imageItemListStickyContainerElRef.current
    )
      return;

    const scrollOffset = window.scrollY - containerElRef.current.offsetTop;
	const maxTranslateX = imageItemListElRef.current.offsetWidth - imageItemListStickyContainerElRef.current.offsetWidth;
	const translateX = Math.min(maxTranslateX, Math.max(0, scrollOffset));

	imageItemListElRef.current.style.transform = `translateX(-${translateX}px)`;
  };

  useEffect(() => {
    const handleScroll = () => {
      setContainerElHeight();
      setImageItemListElTranslateX();
    };

    const handleResize = () => {
      setContainerElHeight();
    };

    setContainerElHeight();

    window.addEventListener('scroll', handleScroll);
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('scroll', handleScroll);
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      <div>
        <div>text...</div>
        <div>text...</div>
        // 이하 생략
      </div>
      <div ref={containerElRef} style={{ position: 'relative' }}>
        <div
          ref={imageItemListStickyContainerElRef}
          style={{
            position: 'sticky',
            left: 0,
            top: 0,
            overflowX: 'hidden',
          }}
        >
          <ul
            ref={imageItemListElRef}
            style={{
              display: 'inline-grid',
              gridTemplateColumns: 'repeat(10, calc(50vh - 32px))',
              gridTemplateRows: 'repeat(2, 1fr)',
              gap: 16,
              padding: 16,
              height: '100vh',
              backgroundColor: '#4740f9',
              alignItems: 'center',
            }}
          >
            <li
              style={{
                width: '100%',
                height: '100%',
                borderRadius: 12,
              }}
            >
              <img
                width={'100%'}
                height={'100%'}
                src="/오둥이프사모음/수면바지.png"
                alt="수면바지"
                style={{ objectFit: 'cover', borderRadius: 12 }}
              />
            </li>
            <li
              style={{
                width: '100%',
                height: '100%',
                borderRadius: 12,
              }}
            >
              <img
                width={'100%'}
                height={'100%'}
                src="/오둥이프사모음/집 가고싶둥.jpg"
                alt="집 가고싶둥"
                style={{ objectFit: 'cover', borderRadius: 12 }}
              />
            </li>
            // 이하 생략
          </ul>
        </div>
      </div>
      <div>
        <div>text...</div>
        <div>text...</div>
        // 이하 생략
      </div>
    </div>
  );
};

결과

profile
4년차 프론트엔드 개발자 장세진

0개의 댓글