모달 관리를 위해 리덕스 툴킷을 사용하여 상태를 저장해 주었다.
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 값을 저장해두고 모달이 닫힐 때 사용한다.
const dispatch = useDispatch();
const handleScroll = () => {
dispatch(setPrevScrollPosition(window.scrollY));
};
useEffect(() => {
dispatch(setPrevScrollPosition(window.scrollY));
// 혹시 여기에 Intersection Observer 의 사용이 필요할까요?
window.addEventListener('scroll', handleScroll);
}, []);
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
동작을 한 파일에서 사용가능하니까 훨씬 사용하기 편해진다.
아무튼 나는 이번에는 리덕스 툴킷을 공부할 생각이니까 툴킷으로 갑니다.
문제는 스크롤 될 때 마다 상태가 업데이트 된다는 것.
굳이 설명을 길게 하지 않아도 매 순간 업데이트 된다는게 안좋다는건 다들 알거다.
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밀리초의 대기 시간 후에 마지막으로 호출된 이벤트만 처리된다.
깔끔하다.