모달 생성 및 스크롤 이벤트에 디바운싱 적용

0
post-thumbnail

🎀 모달 만들기

모달 관리를 위해 리덕스 툴킷을 사용하여 상태를 저장해 주었다.

export type RootState = ReturnType<typeof store.getState>;

export interface ModalState {
  openModalName: string | number | null;
  prevScrollPosition: number;
  currentScrollPosition: number;
}

const initialState: ModalState = {
  openModalName: null,
  prevScrollPosition: 0,
  currentScrollPosition: 0,
};

const modalSlice = createSlice({
  name: 'modal',
  initialState,
  reducers: {
    setOpenModalName: (state, action: PayloadAction<string | number>) => {
      state.openModalName = action.payload;
    },
    openModal: (state, action: PayloadAction<string | number>) => {
      state.currentScrollPosition = state.prevScrollPosition;
      document.body.style.position = 'fixed';
      document.body.style.width = '100%';
      document.body.style.top = `-${state.prevScrollPosition}px`;
      document.body.style.overflowY = 'scroll';
      state.openModalName = action.payload;
    },
    closeModal: (state) => {
      document.body.style.position = '';
      document.body.style.width = '';
      document.body.style.top = '';
      document.body.style.overflowY = '';
      window.scrollTo(0, state.currentScrollPosition);
      state.openModalName = null;
    },
    setPrevScrollPosition: (state, action: PayloadAction<number>) => {
      state.prevScrollPosition = action.payload;
    },
    setCurrentScrollPosition: (state, action: PayloadAction<number>) => {
      state.currentScrollPosition = action.payload;
    },
  },
});

setOpenModalName : 열려있는 모달의 이름/ID를 저장한다. 이 값을 이용해서 페이지에서 열어주고 닫아줄 수 있다.

openModal : 현재 모달이 열리는 스크롤 위치 값을 저장해서 currentScrollPosition에 넘겨준다. 모달이 열려있을 때에는 스크롤되지 못하도록 막는다.

closeModal : currentScrollPosition에 저장된 위치값을 확인하고 모달이 닫혔을 때 해당 위치값으로 강제로 끌어내려 위치시켜준다. 원래는 모달 닫히면 페이지 제일 상단으로 끌어올려졌는데, 그것을 막아주는 것이다.

setPrevScrollPosition : 모달 열기 전의 스크롤 위치 값을 저장한다.

setCurrentScrollPosition : setPrevScrollPosition 값을 저장해두고 모달이 닫힐 때 사용한다.

📌 useEffect를 사용해서 적용.

const dispatch = useDispatch();

 const handleScroll = () => {
   dispatch(setPrevScrollPosition(window.scrollY));
 };

 useEffect(() => {
   dispatch(setPrevScrollPosition(window.scrollY));
   // 혹시 여기에 Intersection Observer 의 사용이 필요할까요?
   window.addEventListener('scroll', handleScroll);
 }, []);

📌 context API에서 활용하면?


type ModalProviderProps = PropsWithChildren;

interface ModalContextProps {
  isScroll?: boolean;
  openModal: (modalName: string | number) => void;
  closeModal: (e: MouseEvent<HTMLButtonElement | HTMLDivElement>) => void;
  openModalName: string | number | null;
}

const ModalContext = createContext<ModalContextProps | undefined>(undefined);

export const ModalProvider = ({ children }: ModalProviderProps) => {
  const [openModalName, setOpenModalName] = useState<string | number | null>(
    null
  );

  const [prevScrollPosition, setPrevScrollPosition] = useState<number>(0);
  const [currentScrollPosition, setCurrentScrollPosition] = useState<number>(0);

  const handleScroll = () => {
    setPrevScrollPosition(window.scrollY);
  };

  useEffect(() => {
    setPrevScrollPosition(window.scrollY);
    window.addEventListener("scroll", handleScroll);
  }, []);

  const openModal = (modalName: string | number) => {
    setCurrentScrollPosition(prevScrollPosition);
    document.body.style.position = "fixed";
    document.body.style.width = "100%";
    document.body.style.top = `-${prevScrollPosition}px`;
    document.body.style.overflowY = "scroll";
    setOpenModalName(modalName);
  };

  const closeModal = () => {
    document.body.style.position = "";
    document.body.style.width = "";
    document.body.style.top = "";
    document.body.style.overflowY = "";
    window.scrollTo(0, currentScrollPosition);
    setOpenModalName(null);
  };

  return (
    <ModalContext.Provider value={{ openModal, closeModal, openModalName }}>
      {children}
    </ModalContext.Provider>
  );
};

export const useModal = () => {
  const context = useContext(ModalContext);
  if (!context) {
    throw new Error("useModal must be used within a ModalProvider");
  }
  return context;
};

useEffect 동작을 한 파일에서 사용가능하니까 훨씬 사용하기 편해진다.
아무튼 나는 이번에는 리덕스 툴킷을 공부할 생각이니까 툴킷으로 갑니다.

🎀 디바운싱 적용

문제는 스크롤 될 때 마다 상태가 업데이트 된다는 것.
굳이 설명을 길게 하지 않아도 매 순간 업데이트 된다는게 안좋다는건 다들 알거다.

📌 lodash 라이브러리 사용

npm install lodash
npm i --save-dev @types/lodash

📌 코드 작성

const dispatch = useDispatch();

const handleScroll = debounce(() => {
  dispatch(setPrevScrollPosition(window.scrollY));
}, 100); // 디바운싱 대기 시간

useEffect(() => {
  dispatch(setPrevScrollPosition(window.scrollY));
  window.addEventListener('scroll', handleScroll);

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

손 쉽게 lodash 라이브러리 사용해서 해결했다.

🎀 결과

📌 디바운싱 전

스크롤 할 때 마다 상태 업데이트가 일어나고 있다.

📌 디바운싱 후

스크롤 이벤트가 발생할 때마다 바로 호출되지 않고, 100밀리초의 대기 시간 후에 마지막으로 호출된 이벤트만 처리된다.
깔끔하다.

profile
일단 해. 그리고 잘 되면 잘 된 거, 잘 못되면 그냥 해본 거!

0개의 댓글