[Reat+TS] 스크롤 이동시키기+이동감지하기 : useRef + IntersectionObserver + ScrollIntoView

에구마·2023년 5월 2일
0

FrontEnd

목록 보기
20/25
post-thumbnail

⬆️⬆️

구현할 것

  • ➀ 스크롤을 움직이면 그에 맞는 헤더배너에 선택효과가 나타나도록
  • ➁ 헤더배너인 [’키워드분석’, ‘카테고리별키워드분석’, ‘폐업가게분석’] 을 선택하면 해당 되는 곳으로 스크롤이 옮겨지도록

검색 해본 결과, ➀ 스크롤에 따라 효과를 주는 건 IntersectionObserver , ➁ 클릭해서 스크롤위치를 옮기는건 ScrollIntoView 였다! 적용해보자!!

감지할 요소와 이동하는 부분에 접근하기 위하여 useRef와 함께 사용하기로 한다.

useRef


돔의 요소에 접근하기 위하여 사용한다.

  • 문서에서도 getElementById를 사용하던데 무슨 차이일까?
    : JS에선 document.getElementById의 기능이지만, id 중복 위험이 없고 정해진 스코프내의 ref에만 접근 가능하여 유지보수에 좋은 장점이 있다.

🤨 사용방법

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 조건으로 존재 확인 후 접근해야 한다 ! ( 참고 )


IntersectionObserver


➀ 스크롤을 움직이면 그에 맞는 헤더배너에 선택효과가 나타나도록

🤨 사용방법

//관찰자 초기화
const observer = new IntersectionObserver(callback, options);


//관찰할 대상 등록
observer.observe(refElement.current <-대상요소)

💡 뷰포트와 관찰 대상이 교차할때(즉, 관찰 대상이 뷰포트에 등장하거나 사라질 때), 등록한 콜백함수를 실행시킨다.

callback 함수

(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이 된다.
  </>
  );

공식 문서 및 참고 자료


ScrollIntoView


이벤트가 발생하면 지정한 위치로 스크롤을 옮긴다.

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로 가지고 있는 '여기로 오세요' 요소로 스크롤이 움직인다!

파라미터

  • alignToTop : boolean
    • true (디폴트) : 스크롤 화면의 가장 위쪽에 요소를 맞춘다 block:’start’와 같음
    • false : 스크롤 화면의 가장 아래쪽을 기준으로 요소를 맞춘다.
  • scrollIntoViewOptions : object
    • behavior : ‘smooth’ , ‘instant’ , ‘auto’
    • block : 수직기준 : ‘start’ , ‘center’ , ‘end’ , ‘nearest’
    • inline : 수평기준 : ‘start’ , ‘center’ , ‘end, ‘ nearest’

완성 🩷

  • 전체코드
    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([]) 쓰면되는데, 나는 커스텀훅을 썼다 … !!
profile
코딩하는 고구마 🍠 Life begins at the end of your comfort zone

0개의 댓글