useRef로 잡은 DOM이 화면에 보이도록 scroll을 하는 hook입니다.
일반 HTML에서는 <a> 태그를 이용하여 같은 페이지의 요소로 연결하는 Fragment를 사용할 수 있습니다.
그런데 React에서는 이게 잘 안 먹기도하고,
* 왜 안먹히는가? => Component는 native html 태그가 아니기 때문입니다. 따라서 div로 한 번 묶어주면 작동은 합니다.
더 나아가 애니메이션적인 요소도 없이 단순 jump로 이동합니다.
따라서, useRef가 보일 때까지 smooth하게 스크롤을 당겨보기로 했습니다. scrollIntoView를 이용하면 되겠군요.
developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
개념 증명 및 문제점
최대한 단순하게 짜면 아래와 같이 사용할 수 있습니다.
ref로 잡고, 클릭하면 scrollIntoview로 이동시키는 것이 끝입니다.
* HTML이 아닌 Component에 직접 사용하면 작동하지 않습니다. <div>로 한번 wrapping 해주세요.
* class에서는 createRef, 함수형에서는 useRef를 사용한다고 알고 있지만 React 코드를 보면 createRef는 리렌더링될 때마다 ref 값이 null로 초기화됩니다. 그러니 useRef를 사용합시다.
function CourseSpecificBox({ courseInfomation }) {
// 어디로 이동시킬지 DOM으로 잡기 위해 useRef
const ReviewRef = useRef();
// 클릭하면 이동시키는 로직
const handleIndexClick = () =>
ReviewRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
return (
<>
// 이곳을 클릭하면
<div className="index" onClick={handleIndexClick}>
<div>Reviews</div>
</div>
// 여기로 이동시킨다.
<div className="Review" ref={ReviewRef}>
<Review courseInfomation={courseInfomation} />
</div>
</>
);
}
export default CourseSpecificBox;
그러나 버튼이 많아진다면 똑같이 ref 달고, 이벤트 달고를 반복해야하는데 index가 3개만 되어도 이런 반복 작업을 3개나 해야 하는 겁니다.
ref만 3개를 잡아야 한다는 것... 똑같은 함수도 3개 생길 생각하니 현기증납니다.
// 이야 프로그래머 실격이다.
const LectureInfoRef = useRef();
const CurriculrumInfoRef = useRef();
const ReviewRef = useRef();
개선하기
조금 더 개선해보자면 다음과 같이 만들 수 있습니다.
* 주의할 점은, HTMLCollection은 배열이 아닌 유사 배열이기 때문에 forEach, map 등의 배열 메서드를 사용할 수 없다는 겁니다. 따라서, deconstructuring으로 배열로 만든 후에 map을 돌려야 합니다.
devsoyoung.github.io/posts/js-htmlcollection-nodelist
* IndexRef와 focusRef의 children의 갯수는 똑같아야 합니다. 로직을 보시면, IndexRef에 담긴 children 각각에 click 이벤트를 달았으며, 각 이벤트는 indexRef의 children과 대응되는 focusRef children으로 이동하는 식으로 짰기 때문입니다.
만약 indexRef와 focusRef의 갯수가 다르다? 아쉽지만 일일히 Ref를 달아주어야 합니다.
function CourseSpecificBox({ courseInfomation }) {
const IndexRef = useRef();
const focusRef = useRef();
useEffect(() => {
let nodes = [];
const IndexClickEvent = (node, i) => {
// removeEvent를 위해 외부 배열에 담아두자
nodes.push(node);
node.addEventListener('click', () => {
focusRef.current.children[i].scrollIntoView({
behavior: 'smooth',
block: 'start',
});
});
};
if (IndexRef.current) {
console.log(IndexRef.current.children);
// HTMLCollection 유사 배열이기 때문에, 배열 메서드 중 일부를 지원 안함. forEach나 .map 사용 불가
// 따라서 destucturing으로 배열에 다시 담아줬음
[...IndexRef.current.children].map((node, i) => IndexClickEvent(node, i));
}
return nodes.map((node, i) => node.removeEventListener('click', IndexClickEvent(node, i)));
}, []);
return (
<StyledCourseSpecificBox>
{/* index */}
<StyledCourseSpecificBoxIndex ref={IndexRef}>
<div className="index">
<div>Lecture Information</div>
</div>
<div className="index">
<div>Curriculrum</div>
</div>
<div className="index">
<div>Reviews</div>
</div>
</StyledCourseSpecificBoxIndex>
{/* 실직적인 내용 */}
<StyledCourseSpecificContents ref={focusRef}>
<LectureInfo />
<CurriculrumInfo />
<Review courseInfomation={courseInfomation} />
</StyledCourseSpecificContents>
</StyledCourseSpecificBox>
);
}
export default CourseSpecificBox;
* scrollIntoView를 React에서 사용할수 있도록 누가 package로 만들어 배포했는데
코드가 단순합니다. src/index.ts에 들어가보면 몇 줄 되지도 않습니다. 참고해보도록합시다.
(그리고 이런거 다운로드 받아서 사용하는 버릇들이면 좋지 않습니다. 직접 구현합시드...)
github.com/dominikbulaj/react-scroll-into-view
'React, Next, Redux > 🚀 React with Hooks' 카테고리의 다른 글
custom hook에서의 Array vs Tuple 반환 (0) | 2021.11.29 |
---|---|
custom hook (4) : intersection observer를 활용한 useScrollFadeIn hook (0) | 2021.01.24 |
custom hook (2) : setInterval wrapping하기 (0) | 2021.01.21 |
custom hook (1) : Detect click outside of DOM (0) | 2021.01.21 |
useState의 지연 초기화 (Lazy initial state) (0) | 2020.10.29 |