[npm] fullpage-scroll-component 구현기

Peter·2023년 5월 8일
1

npm 링크

문제의 시작

사이드 프로젝트를 진행하면서 정보 입력하는 화면이 필요했고, 입력폼 컴포넌트의 화면 구성을 typeform 의 입력창을 모방려고 했습니다. full page scroll 기능을 직접 구현하거나 라이브러리를 가져와 사용해야 하는데 일단 가장 유명하다고 할 수 있는 fullpage.js를 사용해보기로 했습니다.

기존 라이브러리의 문제점

fullpage.js

  • fullpage.js 는 a 링크를 통해 HTML 태그의 아이디를 활용해 페이지를 이동하는 방식으로 동작합니다.
  • 이 방식은 라우트에 history를 남겨 뒤로가기를 하게되면 이전 라우터로 이동하는 것이 아니라 #id 로 남겨진 이력을 다루기 때문에 스크롤을 했던 만큼 뒤로 눌러야 이전 라우트로 이동하게 됩니다. -> 입력창을 하나의 화면으로 취급하고 싶은 구현 방향과는 맞지 않는 부분이었습니다.
  • fullpage.js 는 모바일 터치를 제공하지 않습니다. 또 기본적으로 모바일 화면으로 전환하게 되면 가로 세로 비율이 깨져 다음 스크롤 화면이 살짝 보이는데 버그인지 미구현 부분인지는 모르겠습니다.

블로그들

  • 구현하고자 하는 화면과 fullpage.js 가 맞지 않음을 확인하고 다른 라이브러리와 코드들을 검색했습니다. fullpage scroll 에 대한 많은 구현들이 있었지만, 대부분 fullpage.js를 참고해서 만든 코드들이었습니다.
  • 대부분의 코드가 직접 dom에 접근해서 컨트롤 하는 js 코드들이기 때문에 jQuery를 품고 있거나 ssr을 지원하지 않아, 현재 진행하고 있는 프로젝트에 접목하기가 쉽지 않았고, 인터페이스를 제공하지 않고 있기 때문에 의도대로 동작할지 의문이 들었습니다.

직접 만들어 보자

인터페이스

  • 리액트에서 사용하기 때문에 컴포넌트로 만들 수 있었고 html에 친숙한 이들을 위한 사용 인터페이스가 필요했습니다.
function App() {
  return (
    <StepScroll>
      <Page>
        <FirstCustomComponent />
      </Page>
      <Page>
        <SecondCustomComponent />
      </Page>
      <Page>
        <ThirdCustomComponent />
      </Page>
    </StepScroll>
  );
}
  • StepScroll 컴포넌트 아래 Page 컴포넌트로 구분해 그 안에 어떤 컴포넌트가 들어가던지 풀페이지 안에서 동작하도록 설계했습니다.
  • FirstCustomComponent, SecondCustomComponent, ThirdCustomComponent 들은 Page 안쪽에서 밖이 어떻게 구현되어 있는지 몰라도 전체 화면에서 표현된다는 것을 직관적으로 알려주고 했습니다.
  • 다음 영상처럼 단순 스크롤로 동작하는 기능을 구현했습니다.

동작 영상

브라우저 화면반응형 화면

내부에서 컨트롤

  • PPT나, 모바일 청첩장 등 실제 사용상황에서 고려해보면 Page 안에서 구현한 컴포넌트는 그 안에서 버튼이 있든 특정 시간에 동작을 하든 페이지를 이동할 수 있는 기능이 필요했고
  • StepScroll 컴포넌트를 Context API로 제작해 해당 기능들을 구현하여 자식 컴포넌트들에서 사용할 수 있도록 구현했습니다.
function FirstCustomComponent() {
  const {
    currentPage,
    resetCurrent,
    hasNextPage,
    hasPrevPage,
    nextPage,
    prevPage,
    movePage,
  } = useStepScroll();
  return (
    <div>
      {hasPrevPage && <button onClick={prevPage}>Prev</button>}
      {hasNextPage && <button onClick={nextPage}>Next</button>}
    </div>
  );
}
  • StepScroll 내부에서 useStepScroll로 원하는 기능을 뽑아내 페이지를 조정할 수 있도록 구현했습니다.

외부에서 컨트롤

  • 풀페이지 단위로 스크롤되는 라이브러리지만 header, footer 가 필요하다는 수요가 있었고 풀페이지 안에 다른 컴포넌트가 위치할 수 있도록 조정해야 했는데
  • header 가 필요한 상황에서 navigation을 생각하지 않을 수 없었습니다.
  • 따라서 풀페이지 컴포넌트 외부에서 풀페이지를 컨트롤 할 수 있는 방법이 필요했는데, 그러려면 컴포넌트의 내부 상태나 콜백 함수들을 외부에 노출시켜했습니다.
  • class 컴포넌트로 구현했을때 외부로 내부 구현 함수를 노출 시키는 것은 어렵지 않았지만. 함수 컴포넌트는 외부 훅의 도움 없이 내부 함수를 외부로 노출시키는 것이 어렵기 때문에 useImperativeHandle 훅을 사용해 외부로 노출시키고자 하는 내부함수를 노출시켰습니다.
function App() {
  const ref = useRef<HandleScroll>(null);

  return (
    <>
      <StepScroll ref={ref}>
        <StepScroll.Page>
          <FirstCustomComponent />
        </StepScroll.Page>
        <StepScroll.Page>
          <SecondCustomComponent />
        </StepScroll.Page>
        <StepScroll.Page>
          <ThirdCustomComponent />
        </StepScroll.Page>
      </StepScroll>
      <button
        onClick={() => {
          ref.current.nextPage();
        }}
      >
        Next Page
      </button>
      <button
        onClick={() => {
          ref.current.prevPage();
        }}
      >
        Prev Page
      </button>
      <button
        onClick={() => {
          ref.current.movePage(2);
        }}
      >
        Move to 2page
      </button>
      <button
        onClick={() => {
          ref.current.resetCurrentPage();
        }}
      >
        To the first screen
      </button>
    </>
  );
}
  • 이렇게 밖으로 노출 시킨 메서드들을 useRef를 활용해 외부에서 내부 풀페이지 컴포넌트를 동제할 수 있도록 했습니다.

발생한 문제

브라우저 호환성

  • fullpage-scroll-component에서 구현에 사용하기로 한 핵심 로직은 Dom API의 scrollIntoView메소드인데 이 메소드가 호환하지 못하는 브라우저가 있는지 살펴봐야 했습니다.

  • mdn에 따르면 srollIntoView는 모든 브라우저에서 호환됐고, 각 브라우저에서 지원하는 버전 또한 나쁘지 않은것을 확인하고 구현에 사용했습니다.

리사이징 문제

  • 처음 구현했을 때 펼쳐진 페이지에서 브라우저를 리사이징하면 이전 페이지가 보인다던지 하는 문제가 발생했었고
  • resize 이벤트를 통해 페이지 변경을 감지해 풀페이지 사이즈를 재조정하도록 구현했습니다.
  • 이부분에서 debounce, throttling 사용을 고려했으나 화면 단위가 일그러지는걸 사용자에게 보여주는 것이 좋지 않다고 판단해 resize를 그대로 받아 처리했습니다. 성능적인 부분에서 도움을 받고자 react18에서 제공해주는 동시성을 적용시켜볼지 고민이 되는 부분입니다.

SSR 지원

  • react 라이브러리를 사용해 만든 컴포넌트기 때문에 ESM을 채택한 모듈로 컴파일되고 있었고 이러한 이유 때문에 Next.js 같은 SSR 단계에서 컴포넌트가 불러와지지 않는 문제가 있었습니다.
  • 이 부분은 rollup을 사용해 esm, cjs 양쪽 번들링을 실행했고 package.json에서 아래와 같은 옵션을 추가해 ssr에 대응할 수 있도록 수정했습니다.
...package.json

"exports": {
    ".": {
      "require": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.cjs"
      },
      "import": {![](https://velog.velcdn.com/images/peterpictor/post/797aef64-bcca-4c3a-924c-29a69a188036/image.gif)

        "types": "./dist/index.d.ts",
        "default": "./dist/index.mjs"
      }
    }
  },

후기

인터페이스

사용자의 재량

마우스 스크롤과 터치 스크롤의 방향을 나타내는 훅, 스크롤에 반응하는 시간을 설정하는 delay, 등 고민해야 하는 부분이 많았는데 어디까지의 설정을 사용자에게 허락할 것인지에 대한 고민이 많았습니다.

사용자가 delay를 1000000 처럼 큰 단위를 줄 수도 있는 상황이었고 특정 페이지로 이동하는 메소드인 movePage에 인자로 이동 불가한 숫자를 부여할 수도 있는 상황이었습니다.

이러한 이유로 특정 프로퍼티에 대해서 정해진 값을 선택할 수 있는 옵션을 부여하기도 했습니다.

npm 배포

npm 배포를 생각해서 만든 오픈소스 라이브러리이기 때문에 인터페이스에 대한 고민이 필요했고 각 프랍스들에 대한 주석 설명 또한 필요했습니다.

좀 더 직관적인 프로퍼티, 메소드 이름을 정하는 것이 중요했습니다. 개인적인 예상과 실제 사용하는 사용자의 이해가 매우 다를 수 있음을 동료들을 통해 알게됐습니다.

최초 npm 배포

인생 최초로 npm에 그럴싸한 라이브러리를 배포했습니다. 오픈소스의 도움을 받기만하다가 다른 사람을 위해 라이브러리를 만들 수 있는 아이디어와 기회를 얻게 됐고 생각보다 좋은 반응을 얻을 수 있어 뜻깊은 시간이었습니다.

profile
컴퓨터가 좋아

0개의 댓글