React

useBodyScrollLock

TypeScript
TypeScript
React
React
브라우저 스크롤을 잠그는 훅입니다. 잠금 해제 시 이전 스크롤 위치를 복구합니다. 스크롤바 너비만큼 여백을 추가해 레이아웃 시프트를 방지합니다. 전역 스크롤을 제어하므로 안전하게 컴포넌트 언마운트 시 자동으로 스크롤을 복구합니다.

구현

import { useCallback, useEffect, useRef } from 'react';

export function useBodyScrollLock(): {
  lock: () => void;
  unlock: () => void;
} {
  const scrollYRef = useRef<number | null>(null);

  const lock = useCallback(() => {
    // 페이지에 스크롤이 있는지 확인
    const hasScroll = document.documentElement.scrollHeight > document.documentElement.clientHeight;
    if (!hasScroll) return;

    // 이미 잠긴 경우 중복 실행 방지
    if (scrollYRef.current !== null) return;

    const scrollY = window.scrollY;
    const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;

    document.body.style.position = 'fixed';
    document.body.style.top = `-${scrollY}px`;
    document.body.style.overflowY = 'hidden';
    document.body.style.paddingRight = `${scrollbarWidth}px`;

    scrollYRef.current = scrollY;
  }, []);

  const unlock = useCallback(() => {
    if (scrollYRef.current === null) return;

    const scrollY = scrollYRef.current;
    scrollYRef.current = null;

    document.body.style.position = '';
    document.body.style.top = '';
    document.body.style.overflowY = '';
    document.body.style.paddingRight = '';

    window.scrollTo({ top: scrollY, behavior: 'instant' });
  }, []);

  useEffect(() => {
    return () => unlock();
  }, [unlock]);

  return { lock, unlock };
}

사용법

function Modal({ isOpen }: { isOpen: boolean }) {
  const { lock, unlock } = useBodyScrollLock();

  useEffect(() => {
    if (isOpen) lock();
    else unlock();
  });

  if (!isOpen) return null;
  return <div>모달</div>;
}