⬆️⬆️
검색 해본 결과, ➀ 스크롤에 따라 효과를 주는 건 IntersectionObserver , ➁ 클릭해서 스크롤위치를 옮기는건 ScrollIntoView 였다! 적용해보자!!
감지할 요소와 이동하는 부분에 접근하기 위하여 useRef와 함께 사용하기로 한다.
돔의 요소에 접근하기 위하여 사용한다.
import React, { useRef } from 'react'; //useRef를 사용한다 const TEST = () => { //useRef를 이용하여 변수 선언 const refElement = useRef<HTMLDivElement>(null); return ( <div ref={refElement}> 요소 </div> //DOM요소에 ref속성으로 추ㅏㄱ ) } // 접근 사용은 if (refElement.current) { refElement.current.~~ }
렌더링 끝나기 전엔 해당 요소가 존재하지 않아 'refElement.current' is possibly 'null'.ts(18047)
에러가 발생한다. 위 코드에서처럼 if 조건으로 존재 확인 후 접근해야 한다 ! ( 참고 )
➀ 스크롤을 움직이면 그에 맞는 헤더배너에 선택효과가 나타나도록
//관찰자 초기화 const observer = new IntersectionObserver(callback, options); //관찰할 대상 등록 observer.observe(refElement.current <-대상요소)
💡 뷰포트와 관찰 대상이 교차할때(즉, 관찰 대상이 뷰포트에 등장하거나 사라질 때), 등록한 콜백함수를 실행시킨다.
(entries, observer) ⇒ { }
위 코드에 적용하면,const observer = new IntersectionObserver( (entries, observer) ⇒ { } , options)
콜백함수에서 entries는 인스턴스 배열이다. 콘솔에 출력해보면,
Intersection Observer API 공식문서에서 entries의 각 속성들에 대해 다음과 같이 설명한다.
내가 개발하려는 기능은 “➀ 스크롤 위치에 따라 배너에 선택됨 효과주기 “ 이므로, 속성들 중 isIntersecting 속성을 이용하려고 한다.
osv라는 관찰자를 만들어 적용하는 코드는 다음과 같다.
const osv = new IntersectionObserver((entries, observer) => { console.log(entries[0].isIntersecting); //혹은 entries.forEach((entry) => { console.log(entry.isIntersecting); }); // entries는 IntersectionObserverEntry 인스턴스의 `배열` 이므로 [0]선택 }, options); if (refElement.current) { osv.observe(refElement.current); } // 실행하려면 관찰대상에 ref속성등록 ... <div ref = {refElement} > 관찰대상 </div> ... // -> 실행하면 그 대상이 뷰포트에 보이면 true , 안보이거나 사라질 때 false가 찍힌다 !!!
아이디어 : 이 TF값을 state로 저장하면서 헤더에 효과를 주면 되겠다 !
여기까지 코드
import React, { useEffect, useRef, useState } from "react"; import { STconceptSlideReportWrap, STconceptSlideReportDoor, } from "../../styles/ConceptSlideReportST"; import ConceptSlideReportStyle from "../../styles/ConceptSlideReport.module.css"; interface btnActiveProps { isBtnClicked: boolean; setIsBtnClicked: React.Dispatch<React.SetStateAction<boolean>>; reportDoorVisible: boolean; setReportDoorVisible: React.Dispatch<React.SetStateAction<boolean>>; } const ConceptSlideReport = (props: btnActiveProps) => { const options = {}; const refElement = useRef<HTMLDivElement>(null); const [isFocused, setIsfocused] = useState(false); useEffect(() => { const osv = new IntersectionObserver((entries, observer) => { console.log(entries[0].isIntersecting); if (entries[0].isIntersecting) { setIsfocused(true); } else { setIsfocused(false); } console.log("obser", observer); }, options); console.log("refElement", refElement); if (refElement.current) { osv.observe(refElement.current); } }, []); const element = useRef<HTMLDivElement>(null); const onMoveBox = () => { if (refElement.current) { refElement.current.scrollIntoView(); } }; return ( <> <STconceptSlideReportWrap slideOpen={props.reportDoorVisible}> <div> 스무디 리포트 </div> <div className={ConceptSlideReportStyle.reportNav}> <div>키워드분석</div> <div>카테고리별키워드분석</div> <div>폐업가게분석</div> </div> <div className={ConceptSlideReportStyle.reportContentWrap}> 본문 <div onClick={onMoveBox} ref={refElement}> 여기누르면 </div> <div ref={refElement}> 이 요소가 화면에 있는지 콘솔에 확인 ################# </div> </div> </STconceptSlideReportWrap> </> ); }; export default ConceptSlideReport;
헤더배너가 3개라 observe등록하는 useEffect 세개를 만들기는 길고 낭비 같다.
역시 , 커스텀훅을 만들자... !
useObserver.tsx
import { useEffect, useRef } from "react"; export const useObserver = ( navNumber: number, setNavNumber: React.Dispatch<React.SetStateAction<number>> ) => { const options = {}; const refElement = useRef<HTMLDivElement>(null); useEffect(() => { const osv = new IntersectionObserver((entries, observer) => { console.log(entries[0].isIntersecting); if (entries[0].isIntersecting) { setNavNumber(navNumber); // 뷰포트에 나타난 배너가 무엇인지 갱신해준다. } }, options); if (refElement.current) { osv.observe(refElement.current); } return () => osv.disconnect(); }, []); return refElement; // 관찰하는 대상 요소의 ref를 return 한다. };
사용하는 컴포넌트에선
const [isFocused, setIsfocused] = useState(0); // 뷰포트에 나타나고 있는 배너 인덱스 저장 const refElementhere = useObserver(1, setIsfocused); // 커스텀 훅 호출하여 관찰할 요소 ref 저장 ... return ( <> ... <div ref={refElementhere}> 여기 관찰 </div> // 이 div가 뷰포트에 나타나면 isFocused가 1이 된다. </> );
공식 문서 및 참고 자료
이벤트가 발생하면 지정한 위치로 스크롤을 옮긴다.
element.scrollIntoView - Web API | MDN
➁ 헤더배너인 [’키워드분석’, ‘카테고리별키워드분석’, ‘폐업가게분석’] 을 선택하면 해당 되는 곳으로 스크롤이 옮겨지도록
const refMoveHere = useRef<HTMLDivElement>(null); const onMove = () => { if (refMoveHere.current) { refMoveHere.current.scrollIntoView(); // ref 요소에 대해 속성 지정 } }; return ( <> <div onClick={onMove}> 여기누르면</div> ... <div ref={refMoveHere}> 여기로 오세요 </div> </> )
'여기누르면'을 누르면 scrollIntoView 속성이 있는 onMove가 발생하여 지정한 ref요소인
refMoveHere를 ref로 가지고 있는 '여기로 오세요' 요소로 스크롤이 움직인다!
import React, { useEffect, useRef, useState } from "react";
import {
STconceptSlideReportWrap,
STconceptSlideReportDoor,
} from "../../styles/ConceptSlideReportST";
import ConceptSlideReportStyle from "../../styles/ConceptSlideReport.module.css";
import ic_arrow from "../../assets/ic_arrow.png";
import StoreModal from "../StoreModal";
import { useObserver } from "../../hooks/useObserver";
interface btnActiveProps {
isBtnClicked: boolean;
setIsBtnClicked: React.Dispatch<React.SetStateAction<boolean>>;
reportDoorVisible: boolean;
setReportDoorVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
const ConceptSlideReport = (props: btnActiveProps) => {
const [modalOpen, setModalOpen] = useState(false);
const [isFocused, setIsfocused] = useState(0);
const refKeyword = useObserver(1, setIsfocused);
const refCategory = useObserver(2, setIsfocused);
const refClosed = useObserver(3, setIsfocused);
const onScroll = (
refcurrent: React.RefObject<HTMLDivElement>,
e: number
) => {
if (refcurrent.current) {
refcurrent.current.scrollIntoView({ behavior: "smooth" });
setIsfocused(e);
}
};
return (
<>
<STconceptSlideReportWrap slideOpen={props.reportDoorVisible}>
<div
className={`${ConceptSlideReportStyle.h50center} ${ConceptSlideReportStyle.reportHeader}`}
>
스무디 리포트
</div>
<div className={ConceptSlideReportStyle.reportNav}>
<div
ref={refKeyword}
onClick={() => onScroll(refKeyword, 1)}
className={
isFocused == 1
? `${ConceptSlideReportStyle.reportNavEach} ${ConceptSlideReportStyle.reportNavEachSelected}`
: `${ConceptSlideReportStyle.reportNavEach}`
}
>
키워드분석
</div>
<div
ref={refCategory}
onClick={() => onScroll(refCategory, 2)}
className={
isFocused == 2
? `${ConceptSlideReportStyle.reportNavEach} ${ConceptSlideReportStyle.reportNavEachSelected}`
: `${ConceptSlideReportStyle.reportNavEach}`
}
>
카테고리별키워드분석
</div>
<div
ref={refClosed}
onClick={() => onScroll(refClosed, 3)}
className={
isFocused == 3
? `${ConceptSlideReportStyle.reportNavEach} ${ConceptSlideReportStyle.reportNavEachSelected}`
: `${ConceptSlideReportStyle.reportNavEach}`
}
>
폐업가게분석
</div>
</div>
<div
style={{
width: "100%",
background: "white",
boxShadow: " 0 2px 4px 0 rgba(0,0,0,.1)",
}}
className={ConceptSlideReportStyle.h50center}
>
선택한것들 보여줘야지
</div>
<button onClick={() => setModalOpen(true)}>모달오픈!</button>
<div className={ConceptSlideReportStyle.reportContentWrap}>
본문
<div ref={refKeyword}>키워드분석</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div ref={refCategory}>카테고리별키워드분석</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div ref={refClosed}>폐업</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
<div>.</div>
</div>
{modalOpen && (
<>
<StoreModal
modalOpen={modalOpen}
setModalOpen={setModalOpen}
/>
</>
)}
<STconceptSlideReportDoor
style={{ display: props.isBtnClicked ? "" : "none" }}
onClick={() =>
props.setReportDoorVisible(!props.reportDoorVisible)
}
>
<img
src={ic_arrow}
style={{
rotate: props.reportDoorVisible
? "90deg"
: "-90deg",
width: "35px",
}}
/>
</STconceptSlideReportDoor>
</STconceptSlideReportWrap>
</>
);
};
export default ConceptSlideReport;
... 아직 수정중인 것은
- onMove를 재활용하기 위해서 ref를 배열로 어떻게 해야 좋을까 ??
: 이럴 땐 아래처럼 배열로 초기화 한 뒤 ref 콜백에서 DOM요소를 인수로 받아 current 프로퍼티 배열에 넣어주면 된다.
근데ISSUE
useRef([]) 쓰면되는데, 나는 커스텀훅을 썼다 … !!