리액트 스크롤 막기 - liaegteu seukeulol maggi

웹사이트를 이용하다 보면 가끔 모달 창을 띄었을 때 뒷배경이 스크롤되는 경우가 있습니다.

한번 같이 보시죠.

난이도: ★☆☆☆

(개발 관련 게시글 난이도 정리 참고)


1. 현상

See the Pen scroll_block-1 by chansoo (@chansoo1280) on CodePen.

(맨 위에 있는 '모달 창 열기'버튼을 누르시면 모달 창이 열립니다.)

문제는 오른쪽의 스크롤입니다.

콘텐츠의 크기 때문에 자동으로 body에 스크롤이 생겼습니다.

모달 창을 열었을 때 모달 창의 콘텐츠만 작동하고 body스크롤은 멈추게 하고 싶습니다.

2. 여러 가지 방법

1. body에 overflow-y주기

가장 먼저 떠올린 방법입니다.

overflow가 스크롤을 만드니까 그냥 hidden으로 설정해서 스크롤이 안 생기도록 하는 방법이죠.

한번 적용해보겠습니다.

See the Pen scroll_block-2 by chansoo (@chansoo1280) on CodePen.

보시면

스크롤이 사라졌군요!

높이도 유지돼서 정말 좋습니다.

하지만 오른쪽의 스크롤만큼 밀려있던 콘텐츠가 원래대로 돌아오면서 약간 왔다 갔다 합니다.(0.5배로 봐보세요!)

이 이슈는 overflow-y:overlay;를 통해 기존 스크롤바의 영역을 없애서 해결 가능하네요.

또는 body에 `padding:16px;`을 줘서 스크롤바의 영역을 유지할 수도 있습니다.

2. 전체를 div로 한번 감싸기

겉을 div로 감싸고 그 wrap에 `overflow:auto;`를 주었습니다.

이러면 모달을 fixed로 띄었을 때 wrap을 가려서 스크롤을 자연스럽게 막을 수 있습니다.

See the Pen scroll_block-3 by chansoo (@chansoo1280) on CodePen.

잘 막혔나요?

3. 마무리

스크롤 막는 방법은 이것 이외에도 다양하게 있을 수 있습니다.

(좋은 아이디어가 있다면 댓글로!)

일단 저는 div가 하나 늘어나더라도 2번 방법이 최선인 것 같습니다.

무엇보다 스크립트가 간단한 점이 마음에 듭니다.


이상으로 마치겠습니다.

궁금한 점이나 피드백은 언제나 환영합니다!

감사합니다.

모달 컴포넌트 스크롤을 막으면서 발생한 문제에 대한 기록입니다.

첫 번째 문제

e.preventDefault()와 e.stopPropagation()을 통해 이벤트를 막는 방법으로 스크롤을 막아야겠다는 생각을 했습니다.

하지만 어림도 없이 스크롤은 잘 되었습니다.

고민을 하다 실행 시점에서의 문제가 있다는 것을 깨닫게 되었습니다.

mdn한글 문서를 보게 된다면 scroll은 view나 element가 스크롤될 때 scroll이벤트가 발생한다라고 되어있습니다.

정말 모호한 말이 아닐 수가 없습니다. 영어로 된 mdn을 보게 된다면 scroll event라고 명시가 되었는데, The scrollevent fires when an element has been scrolled. 즉, 요소가 스크롤된 다음 이벤트가 발생한다.라고 명시되어 있습니다. 때문에 이미 스크롤이 일어난 곳에서 이벤트를 막아봤자 동작하지 않았던 것입니다.

따라서 계속 scroll을 대체할 만한 이벤트를 찾던 와중 wheel과 mousewheel을 발견하게 됩니다.

mousewheel의 경우 비표준이며 지원하지 않는 브라우저도 있으며, deprecated 되었다고 합니다.

이에 따라 대체된 것이 wheel이벤트입니다.

wheel이벤트를 본다면 Thewheelevent fires when the user rotates a wheel button on a pointing device (typically a mouse). 즉, 사용자가 휠 버튼을 돌릴 때 발생한다고 합니다.

따라서 wheel이벤트에 이벤트 전파를 방지하고, 기본 동작을 막는다면, 잘 동작하게 됩니다.

두 번째 문제

하지만 이렇게 하다 보니 휠 이벤트 자체를 막게 되었고, Modal컴포넌트에서 overflow가 발생했을 때도 scroll이 되지 않았습니다.

따라서 결국 body에 overflow:hidden속성을 주는 것으로 해결을 했습니다.

또한 touch-action:none을 한다면 모바일에서도 막을 수 있습니다.

세 번째 문제

하지만 모바일의 특징상 끝까지 아래로 내린다면 url창이 보이며 위로 올리면 hidden이 되었던 요소들이 보이게 됩니다.

저는 모달의 Dim에 height:100vh와 background색을 변경하였는데, 끝까지 overflow 된 모달을 위로 스크롤한다면 화면에서 숨겨져 있던 여백이 흰색으로 보이게 됩니다.

따라서 모달의 height를  100%로 수정하여 추가적인 여백이 위로 올라오더라도 background색이 변하도록 했습니다.

결론

overflow는 reflow를 일으키기 때문에 이벤트로 막으려고 했지만...  복잡도를 생각했을 때 이것이 최선의 방법인것 같습니다.

overflow로 인해 성능적 이슈가 발생하면 다른 방법을 다시 생각해보아야할것 같습니다.

크롬 / Safari  web/mobile에서 잘 동작하는 것을 확인할 수 있습니다.

  useEffect(() => {
    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, []);

별도로 빼두어 훅으로 사용할 수도 있습니다.

참고

https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event

https://developer.mozilla.org/en-US/docs/Web/API/Element/scroll_event

https://developer.mozilla.org/en-US/docs/Web/API/Element/mousewheel_event

body 태그의 css를 변경합니다. positionfixed로 하고, top의 위치를 현재 스크롤 위치로 설정한 뒤 overflow-y: scroll; width: 100%; 을 추가 지정하면 스크롤바로 인해 배경의 위치가 움직이지 않고도 스크롤 방지를 할 수 있습니다.

useEffect를 사용해 css를 변경하며, 모달이 사라질 때에는 useEffectreturn을 사용해 bodycssText를 리셋시킨 다음 window,scroll을 이용해 현재 스크롤 위치로 강제 이동시킵니다. 참고로 useEffectreturn 절은 컴포넌트가 unmount 될 때 호출됩니다.

스크롤 방지 모달 스크롤 방지 스크롤 안되게 리액트 모달 스크롤 안되게

import React, { useEffect } from 'react';

export const Modal = (props) => {

  // 모달 오버레이에서 스크롤 방지
  useEffect(() => {
    document.body.style.cssText = `
      position: fixed; 
      top: -${window.scrollY}px;
      overflow-y: scroll;
      width: 100%;`;
    return () => {
      const scrollY = document.body.style.top;
      document.body.style.cssText = '';
      window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
    };
  }, []);

  return (
    <>
      {isDisplay && (
        `<Container>
          <ModalOverlay />
          <ModalWindow />
        </Container>`
      )}
    </>
  );
};

리액트 스크롤 막기 - liaegteu seukeulol maggi

참고
  • 효율적인 리액트 모달 만들기
  • https://stackoverflow.com/questions/8701754/just-disable-scroll-not-hide-it
  • https://stackoverflow.com/questions/9280258/prevent-body-scrolling-but-allow-overlay-scrolling/