페이지를 이동했다가 다시 돌아왔을 때 스크롤 위치를 유지하는 코드이다.
새로고침을 하면 세션스토리지에서 스크롤 값을 삭제시킨다.
총 2번의 리팩토링을 진행했다.
기타 로직은 제외하고 스크롤 관련 로직으로 축약한 Component이다.
1.
처음에는 debounce로 사용자의 스크롤을 감지해서 스토리지에 담는다는 생각을 했다.
하지만 state도 자주 바뀌고, 스크롤을 하다가 클릭을 해버리면 엉뚱한 스크롤 위치로 이동하기 때문에 수정이 필요했다.
만들때에도 이 생각을 했었는데, 분리되어있는 SubComponent에 onClickHandler가 있어서 하지 못했다.
ref를 넘겨서 scroll target을 잡으면 됐었는데, 엉뚱하게 window를 scroll target으로 생각하고 작업해서 감지를 하지 못했었다.
function Component() {
const listRef = useRef<HTMLDivElement>(null);
const [scrollValue, setScrollValue] = useState(Number(sessionStorage.getItem('scrollY')) || 0);
const handleScroll = debounce((e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const target = e.target as HTMLDivElement;
setScrollValue(target.scrollTop);
}, 100);
useEffect(() => {
const scrollValue = sessionStorage.getItem('scrollY');
const { current } = listRef;
if (scrollValue && current) current.scrollTop = +scrollValue;
const handleRefresh = () => sessionStorage.clear();
window.addEventListener('beforeunload', handleRefresh);
return () => window.removeEventListener('beforeunload', handleRefresh);
}, []);
useEffect(() => sessionStorage.setItem('scrollY', `${scrollValue}`), [scrollValue]);
return (
<Wrapper ref={listRef} onScroll={handleScroll}>
<SubComponent />
</Wrapper>
);
}
export default Component;
2.
그래서 SubComponent를 합치고 리팩토링을 시도했다.
이미 target을 ref로 잡아둔 상황이었기 때문에 사실상 component는 그대로 분리한채 진행을 해도 됐었다.
하지만 data를 SubComponent로 넘겨주는데 data가 잠시 없을때 사용하는 early return이 보기 싫어서 합쳐서 진행했다.
function Component() {
const listRef = useRef<HTMLDivElement>(null)
const handleProductClick = () => {
const { current } = listRef
if (!current) return
sessionStorage.setItem('scrollY', `${current.scrollTop}`)
}
useEffect(() => {
const { current } = listRef
const scrollValue = sessionStorage.getItem('scrollY')
if (scrollValue && current) current.scrollTop = +scrollValue
const handleRefresh = () => sessionStorage.clear()
window.addEventListener('beforeunload', handleRefresh)
return () => window.removeEventListener('beforeunload', handleRefresh)
}, [])
return (
<Wrapper ref={listRef}>
<SubComponent>
...
</SubComponent>
</Wrapper>
)
}
export default Component
3.
자주 변하던 state가 사라지고 엉뚱한 위치로 이동하는 오류도 사라지다보니, 스크롤 로직을 공통으로 사용할 생각이 들었다.
그래서 custom hook으로 분리를 시도했다.
// useKeepScroll.ts
import { useEffect, useCallback } from 'react';
type ScrollRef = {
current: HTMLElement | null;
}
const useKeepScroll = (scrollRef: ScrollRef) => {
const setScroll = useCallback(() => {
if (!scrollRef.current) return;
sessionStorage.setItem('scrollY', `${scrollRef.current.scrollTop}`);
}, [scrollRef]);
useEffect(() => {
if (!scrollRef.current) return;
const scrollValue = sessionStorage.getItem('scrollY');
if (scrollValue) scrollRef.current.scrollTop = +scrollValue;
const handleRefresh = () => sessionStorage.clear();
window.addEventListener('beforeunload', handleRefresh);
return () => window.removeEventListener('beforeunload', handleRefresh);
}, [scrollRef]);
return setScroll;
};
export default useKeepScroll;
scroll target을 parameter로 받고, 해당 scroll 위치를 sessionStorage에 담는 함수를 return한다.
import useKeepScroll from 'hooks/useKeepScroll';
function Component() {
const listRef = useRef<HTMLDivElement>(null);
const setScroll = useKeepScroll(listRef);
const handleProductClick = (id: number) => {
setScroll();
};
return (
<Wrapper ref={listRef}>
<SubComponent>
...
</SubComponent>
</Wrapper>
);
}
export default Component;
최종적으로 원하던 기능과 custom hook, 간결한 코드로 수정되었다.
리팩토링을 진행해서 바뀌기 이전에, 한 걸음 멀어져서 전체 코드를 보는 습관의 필요성을 느꼈다.
엉뚱한 생각의 결과로 처음에 짤 수 있던 코드를 리팩토링으로 얻게 되었으니 말이다.