일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 비디오 스트리밍
- Image 컴포넌트
- 씨쁠쁠
- 웹크롤링
- React ssr
- git checkout
- 부스트캠프
- Next/Image 캐싱
- 프로그래머스
- 멘션 추천 기능
- react
- 자바스크립트
- 파이썬
- PubSub 패턴
- 자바스크립트 객체
- 네이버 부스트캠프 멤버십
- 파이썬 코딩테스트
- React.js
- Next.js
- c++
- 네이버 부캠
- 스택
- 자바스크립트 컴파일
- beautifulsoup
- 브라우저 동작
- 코딩테스트
- 자바 프로젝트
- Server Side Rendering
- 파이썬 웹크롤링
- 네이버 부스트캠프
- Today
- Total
코린이의 개발 일지
[mo:heyum] 스크롤 복원하기 본문
문제 상황
- 뉴스피드 무한스크롤이 게시물 페이지 들어갔다가 router.back으로 돌아오면 맨 위로 스크롤이 원상 복구되는 문제
- 사용자 경험 측면에서 상당히 불편한 문제라서 꼭 고치고 싶었다.
해결 방안
- 같은 팀원분이 이런식으로 해보는것이 어떻냐는 방법을 제시해 주셔서 그걸로 시도해봤다.
- 스크롤 y 위치와 , 불러온 전체 뉴스피드 목록 전역상태로 관리
- 뒤로가기 버튼 누를시, 전역 상태 불러와서 렌더링
- 뉴스피드는 현재 페이지네이션으로 동작한다 (usePaginator 훅이 pages(불러온 뉴스피드 목록)을 반환한다.)
처음 코드
export default function MainSection() {
const scrollRef = useRef<any>();
const [scroll, setScroll] = useRecoilState(scrollY);
const [currentNewsfeed, setCurrentNewsfeed] = useRecoilState(newsfeedList);
const { pages, lastFollowElementRef } = usePaginator(`/api/post/newsfeed`);
const onScroll = useCallback(() => {
setScroll(scrollRef.current.scrollTop);
}, []);
useEffect(() => {
if (pages.length !== 0) {
setCurrentNewsfeed(pages);
}
}, [pages]);
useEffect(() => {
scrollRef.current.scrollTo(0, scroll);
}, []);
return (
<Wrapper>
<MainTopBar>
<div>홈</div>
</MainTopBar>
<Newsfeed onScroll={onScroll} ref={scrollRef}>
...
<ArticlesSection>
{currentNewsfeed.map((item: any, index: number) => {
...
})}
</ArticlesSection>
</Newsfeed>
</Wrapper>
);
}
- 우선 현재까지 불러온 뉴스피드 목록과, scroll Y 값을 전역 상태로 저장했다.
- api 요청으로 불러온 pages가 바뀔때마다 뉴스피드 목록을 새로 저장
- 스크롤 이벤트 발생시, scrollTop값 전역 상태에 갱신
- 화면이 처음 렌더링 될 때, scroll Y 위치로 목록 보냄.
export default function usePaginator(url: string) {
const [nextCursor, setNextCursor] = useState(NEXT.START);
const [historyback, setHistoryBack] = useRecoilState(historyBack);
const { loading, error, pages, next } = fetchData(url, nextCursor, historyback);
const observer = useRef<any>();
const lastFollowElementRef = useCallback(
(node: any) => {
if (loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && next !== NEXT.END) {
setNextCursor(next);
setHistoryBack(false);
}
});
if (node) observer.current.observe(node);
},
[loading, next !== NEXT.END]
);
return { loading, error, pages, lastFollowElementRef };
}
그리고 paginator 훅에서, 등록해둔 historyback 상태를 체크해서, 뒤로가기를 눌러서 메인 페이지에 들어올 경우, fetch를 보내지 않는다.
단, 페이지네이션은 동작해야 하므로 interSectionObserver에서 스크롤이 끝에 도달할 경우 historyBack을 false로 바꿔준다.
이렇게 했더니 이런문제가 발생했다.
- 되돌아오면 위에서부터 스크롤이 내려가는게 보인다……
최종 해결방법
- 페이지 네이션 부분도 좀 수정했다.
- 10개 단위로 긁어와서, 누적 결과를 리턴하는 것이 아닌 10개 뉴스피드 목록만 리턴하도록 변환
- 전역 상태
- 스크롤 복원에 사용할 전역 상태를 한곳에 몰아둠.
export const scrollHandle = atom({
key: 'scrollHandle',
default: { scrollY: 0, historyBack: false, nextPageId: 'START' },
});
export const newsfeedList = atom({
key: 'newsfeedList',
default: [],
});
- 뉴스피드에서 페이지네이션이 발생할 때 마다
- nextPageId: 다음 페이지네이션때 가져올 게시글 id
- newsfeedList: 현재까지 불러온 뉴스피드 목록
useEffect(() => {
if (pages.length !== 0 && scrollhandler.nextPageId !== '') {
setCurrentNewsfeed((prevState) => prevState.concat(pages));
setScrollHandler((prevState) => ({ ...prevState, nextPageId: next }));
}
}, [pages]);
- 스크롤 이벤트 발생할 때 마다, scrollY 전역 상태 업데이트
const onScroll = useCallback(() => {
setScrollHandler((prevState) => ({ ...prevState, scrollY: scrollRef.current ? scrollRef.current.scrollTop : 0 }));
}, []);
- 뒤로가기로 인한 메인 페이지 렌더링인지 확인.
- 메인페이지 첫 렌더링 시, 뒤로가기로 인한 접근이 아닐 경우 scrollHandler 전부 초기화
- 뒤로가기로 인한 접근일 경우 스크롤 위치 변환, historyBack 초기화.
useEffect(() => {
if (!scrollhandler.historyBack) {
setScrollHandler((prevState) => ({ ...prevState, historyBack: false, scrollY: 0, nextPageId: 'START' }));
} else {
if (scrollRef.current) {
scrollRef.current.scrollTo(0, scrollhandler.scrollY);
}
setScrollHandler((prevState) => ({ ...prevState, historyBack: false }));
}
}, []);
- usePaginator훅에서 10개 단위로 끊어서 pages를 반환하므로, 다음 요청할 페이지도 usePaginator에 보내줘야 한다.
- 뒤로 가기를 통해서 온 경우 이미 현재까지 불러온 뉴스피드는 전역 상태로 저장해두었기 때문에, 그 이후부터 페이지네이션 하면된다.
- 첫 렌더링 일 경우는 처음 부터 페이지네이션
const { pages, next, loading, lastFollowElementRef } = usePaginator(
`/api/post/newsfeed`,
scrollhandler.historyBack ? scrollhandler.nextPageId : 'START'
);
결과물
버그 수정
- 문제상황: 다른 페이지에서 페이지네이션 할 때는 스크롤 복원이 안되게 (스크롤 초기화)하는 것이 목표였는데, 정상적으로 동작을 안함
function getFetchUrlWidthNext(fetchUrl: string, next: string) {
if (fetchUrl.includes('?')) return `${fetchUrl}&next=${next}`;
return `${fetchUrl}?next=${next}`;
}
function useFetchPage(fetchUrl: string, nextCursor: string, nextStart: string) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [pages, setPages] = useState<any>([]);
const [next, setNext] = useState(NEXT.START);
useEffect(() => {
setPages([]);
}, [fetchUrl]);
useEffect(() => {
const abortController = new AbortController();
setLoading(true);
setError(false);
let fetchUrlwithNext = fetchUrl;
if (next === NEXT.START && nextCursor !== '' && nextCursor !== NEXT.START) {
fetchUrlwithNext = getFetchUrlWidthNext(fetchUrl, nextCursor);
} else if (next !== NEXT.START && next !== NEXT.END && nextCursor !== 'START') {
fetchUrlwithNext = getFetchUrlWidthNext(fetchUrl, next);
}
fetch(`${fetchUrlwithNext}`, {
signal: abortController.signal,
method: 'GET',
credentials: 'include',
})
.then((res) => res.json())
.then((res) => {
if (res.data?.post === undefined)
// 데이터 끝
res.data = {
post: [],
next: NEXT.END,
};
if (nextStart === '') setPages((prevPages: any[]) => [...prevPages, ...res.data.post]);
else setPages([...res.data.post]);
setNext(res.data?.next ?? '');
setLoading(false);
})
.catch(() => {
if (abortController.signal.aborted) return;
setError(true);
});
return () => {
abortController.abort();
};
}, [fetchUrl, nextCursor]);
return { loading, error, pages, next, setPages };
}
바꾼 부분은 딱 두줄이다.
if (nextStart === '') setPages((prevPages: any[]) => [...prevPages, ...res.data.post]);
else setPages([...res.data.post]);
메인 페이지 페이지네이션 일 때랑, 다른 페이지 페이지네이션 할 때, 구분 지어주는 코드이다.
메인페이지는 usePaginator 훅에서 반환하는 pages에 뉴스피드 목록 누적 x (10개 단위로, 새 데이터만 반환)⇒ 뉴스피드 전체 목록은 상태로 관리
스크롤 복원이 필요 없는 page들은 pages에 목록 누적 o (팔로워,팔로잉 리스트) ⇒ 즉 누적된 pages 리스트를 계속해서 반환해줌.
이렇게 바꾸면 usePaginator 훅은 다음과 같이 호출 한다.
export default function usePaginator(url: string, nextStart: string = '') {
const [nextCursor, setNextCursor] = useState(nextStart);
const { loading, error, pages, next } = useFetchPage(url, nextCursor, nextStart);
const observer = useRef<any>();
const lastFollowElementRef = useCallback(
(node: any) => {
if (loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && next !== NEXT.END) {
setNextCursor(next);
}
});
if (node) observer.current.observe(node);
},
[loading, next !== NEXT.END]
);
return { loading, error, pages, next, lastFollowElementRef };
}
따라서 usePaginator를 사용할 때, 만약, 스크롤 복원을 해야하는 경우라면, 첫 렌더링시에는 두번째 매개변수로, “START” 를 넘기고, 스크롤 복원하는 렌더링일 경우 다음 페이지 네이션 요청할 page id를 넘기면 된다.
스크롤 복원을 안해야 하는 페이지네이션 경우라면, 두번째 매개변수에 아무것도 안넘기면 된다.
- 이로써 상황에 맞게 usePaginator를 사용할 수 있다.
최종 결과물
- 뒤로가기로 뉴스피드에 돌아올 경우 스크롤 복원
- 다른 페이지 갔다가 홈버튼 클릭 해서 들어올 경우 처음부터 렌더링
Refs:
https://velog.io/@ziyoonee/react-scroll-위치-복원-하기feat.-custom-hook
https://helloinyong.tistory.com/300
'웹 (web) > 모헤윰 프로젝트' 카테고리의 다른 글
[mo:heyum] 사용자 멘션 추천기능 구현하기 (0) | 2023.02.07 |
---|