카심
264
2021-01-31 09:56:10 작성 2021-01-31 10:09:48 수정됨
8
683

리액트 useRef 관련해서 질문합니다.


스크롤 박스에 접근하려고 합니다.

useRef를 선언했습니다.


문제는 TypeError: Cannot read property 'scrollTop' of null

가 뜹니다.

이거 해결해서 작동시키려면 어떻게 해야하나요?


useRef를 제대로 연결한 것인지???
스크롤 설정을 제대로 한것인지???
null인 경우 에러를뿜고,

숫자 0을 기본 값으로 할 경우 함수가 작동하지 않습니다.

useEffect(() => {
      scrollBox.current.scrollTop();
  }, [location]);

에서 scrollTop을 속성이 null값이라고 오류가 뜹니다.
그런데, scroll을 쓰려면 null을 선언해야하지 않나요???

TypeError: Cannot read property 'scrollTop' of null

import React, { useState, useEffect, useRef } from "react";
import { Link } from 'react-router-dom';
import styled from "styled-components";
import Nav from './Nav';

const Images = () => {
  const [data, setData] = useState();
  const [location, setLocation] = useState();
  const scrollBox = useRef(null);

  // 이미지 자료 긁어오기.
  useEffect(()=> {
    fetch(`http://localhost:3000/data/MockupData.json`)
      .then(res => res.json())
      .then(res => {
        setData(res)
      })
      .catch (error => {
        console.log(error);
      });
  }, []);

  //마우스 스크롤 이벤트 작동
  useEffect(() => {
      scrollBox.current.scrollTop();
  }, [location]);

  // const activeOverScrollEvent = () => {
  //   if(window.scrollTop > 400) {
  //     window.scrollTopView(window.scrollTopView(0, 1600))
  //   }
  //   if(window.scrollTop > 1800) {
  //     window.scrollTopView(window.scrollTo(0, 3400))
  //   }
  //   if(window.scrollTop > 3600) {
  //     window.scrollTopView(window.scrollTo(0, 5100))
  //   }
  //   if (window.scrollTop > 5300) {
  //     window.scrollTopView(window.scrollTo(0, 6800))
  //   }
  // };

  return(
    <>
      <Nav />
      <ImagesWrap>
        <ImagesBox>
          {data && data.car.map((el, i)=> {
            console.log(i);
            return (
              <>
                <picture className="images" key={i} ref={scrollBox}>
                  <MiddleBox>
                    <ModelTitle>{el.name}</ModelTitle>
                    <SubTitle>
                      <Link className='subLink' to="">{el.riding}</Link>
                    </SubTitle>
                  </MiddleBox>
                  <UnderBox>
                    <OrderLink>{el.order}</OrderLink>
                    <DetailLink>{el.detail}</DetailLink>
                  </UnderBox>
                  <Imgtag src={el.img_src} alt={el.img_alt} />
                </picture>
              </>
            )
          })
          }
        </ImagesBox>
      </ImagesWrap>
    </>
  )
}

export default Images;

const ImagesWrap = styled.div``; // 최상단 랩핑

const ImagesBox = styled.section``;

const Imgtag = styled.img`
  display: grid;
  width: 1fr;
  height: 1fr;
  max-width: 2048px;
  justify-content: center;
`;

const MiddleBox = styled.div`
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  flex-direction: flex-end;
  align-items: center;
  z-index: 1;
  position: absolute;
  left: 50%;
  margin-top: calc(16vh + 20px);
`;

const ModelTitle = styled.h1`
  display: inline-block;
`;
const SubTitle = styled.h2`
  .subLink {
    
  }
`;

const UnderBox = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: flex-end;
  flex-flow: 1;
`;

const OrderLink = styled(Link)``;
const DetailLink = styled(Link)``;
0
  • 답변 8

  • 킁킁탐정
    844
    2021-01-31 12:13:22 작성 2021-01-31 12:14:38 수정됨

    1. 에러의 이유는 useEffect()로 데이터를 받기 전에 이미 한번 렌더링이 진행되고 useRef(null)로 인해 scrollBox의 초기값도 null입니다. 그러므로 useEffect()가 처음 실행될때 null.scrollTop()를 찾게 됩니다.

    2. 데이터를 받아오고 data.car.map()을 이용하여 받은 데이터를 화면에 표시할때 ref={scrollBox}를 사용하신 방법은 잘못되었습니다. car 배열에 100개의 요소가 있다면 마지막 요소의 ref만 scrollBox에 저장되게 됩니다.

    3. scrollTop() 메소드는 없습니다. scrollTop 프로퍼티만 존재합니다.



  • 카심
    264
    2021-02-01 10:46:39

    킁킁탐정 // 어떻게 해야할까요??

    <picture className="images">
      <MiddleBox>
        <ModelTitle>{el.name}</ModelTitle>
        <SubTitle>
           <Link className='subLink' to="/">{el.riding}</Link>
        </SubTitle>
      </MiddleBox>
      <UnderBox>
        <OrderLink>{el.order}</OrderLink>
        <DetailLink>{el.detail}</DetailLink>
      </UnderBox>
      <Imgtag key={i} src={el.img_src} alt={el.img_alt} ref={scrollBox}/>
    </picture>

    이렇게 2번의 조언을 듣고 위치를 변경했습니다. 

    3번은 프로퍼티이므로 ()를 제거하였습니다.

    1번은 useRef에다가 document 직접 쓰는건 안좋은 선택인것으로 알아서 어찌해야할지 모르겠습니다.

  • 킁킁탐정
    844
    2021-02-01 19:21:43

    근본적으로 질문 내용이 에러 처리에 관한것만 작성되어있어서 무엇을 목적으로 하는 컴포넌트인지에 대한 설명이 없어서 적절한 답변을 드리기가 어렵습니다. 마지막 질문 내용만 고려해서 알려드리면 다음과 같습니다.

    1. 무엇을 말씀하시는지 모르겠습니다. document는 얼마든지 사용될수 있습니다. useEffect와 useRef를 이용하여 document를 제어하는건 나쁜일이 아닙니다.


    2. 변경한 위치도 마찬가지로 scrollBox는 반복문의 마지막 요소만 들어가게 됩니다. 이 문제를 해결하는 간단한 방법은 새 컴포넌트로 만드는 방법입니다. 컴포넌트하나에 useRef()와 함께 반복문에 들어가는 내용을 넣습니다. 그리고 반복문에서 그 컴포넌트를 사용하는 방식입니다. 대략적인 스케치를 해보면 다음과 같습니다. 이 방식으로하면 각각의 picture 마다 고유하게 useRef를 이용할 수 있습니다.

    function SomeComponent() {
      const scrollBox = useRef(null)
      useEffect(() => {
         scrollbox...
      })
      return <picture ref={scrollBox}> ... </picture>
    }

    function OtherComponent() {
      return (
        <div>
          {data.car.map((el, i) => {
            return <SomeComponent key={i} />
          })}
       </div>
    }


  • 카심
    264
    2021-02-01 20:18:22 작성 2021-02-01 20:49:30 수정됨

    킁킁탐정 // 제가 작성중인 코드는 테슬라 홈페이지 메인페이지(첫 화면)입니다.

    스크롤을 내리면 해당 화면 중앙으로 이동하고, css가 뜨는 걸 구현하려고 하는데

    스크롤이벤트부터 구현이 안됩니다...

    https://www.tesla.com/ko_kr

    말씀하신대로 조언에 따라서 map이 돌아가는 부분 하위에 컴포넌트를 새로 팠습니다.

    문제는 처음 딱 한번만 작동하고 useEffect가 해당 값의 변화를 감지하지 못합니다.

    이를 해결하려면 어떻게 해야하나요???

    import React, { useEffect, useRef } from 'react';
    import { Link } from 'react-router-dom';
    import styled from 'styled-components';
    
    function ScrollBox ({el, i}) {
      const scroll = useRef(null);
      useEffect(() => {
        let point = scroll.current.scrollTop
        console.log('위치 ' + point);
      }, [scroll]);
    
      return (
        <picture className="images" key={i} ref={scroll}>
          <MiddleBox>
            <ModelTitle>{el.name}</ModelTitle>
            <SubTitle>
              <Link className='subLink' to="">{el.riding}</Link>
            </SubTitle>
          </MiddleBox>
          <UnderBox>
            <OrderLink>{el.order}</OrderLink>
            <DetailLink>{el.detail}</DetailLink>
          </UnderBox>
          <Imgtag src={el.img_src} alt={el.img_alt} />
        </picture>
      );
    };
    
    function Image1 ({el, i}) {
      return(
        <ScrollBox el={el} i={i} />
      );
    };
    
    export default Image1;
    
    const Imgtag = styled.img`
      display: block;
      width: 1fr;
      height: 1fr;
      max-width: 2048px;
      position: relative;
      justify-content: center;
    `;
    
    const MiddleBox = styled.div`
      display: flex;
      flex-flow: column nowrap;
      justify-content: center;
      flex-direction: flex-end;
      align-items: center;
      z-index: 1;
      position: absolute;
      left: 50%;
      margin-top: calc(16vh + 20px);
    `;
    
    const ModelTitle = styled.h1`
      display: inline-block;
    `;
    const SubTitle = styled.h2`
      .subLink {
        
      }
    `;
    
    const UnderBox = styled.div`
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: flex-end;
      flex-flow: 1;
    `;
    
    const OrderLink = styled(Link)``;
    const DetailLink = styled(Link)``;


  • 킁킁탐정
    844
    2021-02-01 21:28:19

    useEffect(() => {}, []) 형식으로 [] (deps) 부분이 언제 useEffect가 실행되는지를 나타냅니다. deps를 []로 지정하면 최초 렌더링시에 한번만 실행하고 그 이후에 실행하지 않습니다.

    작성하신 새로운 코드를 보면 [scroll]를 지정하셨지만 scroll은 처음 렌더링시에 ref={scroll}로 인해서 DOM ref를 가지고 있습니다. 그러므로 더 이상 변경되지 않습니다. deps로 이를 지정하여도 아무런 일이 발생하지 않는 이유입니다.

    scroll 값은 scroll 이벤트로 인해 변경되므로 이벤트를 바인딩하셔야합니다. 대략 짜보면 다음과 같습니다. useEffect()를 최호 1회만 실행하고 scroll이벤트가 발생할때마다 이벤트 함수를 실행합니다. useEffect()의 마지막 리턴 함수는 컴포넌트가 unmount될때 자동으로 실행됩니다.

    useEffect(() => {

    const handler = (event) => {
     // scroll 관련 코드
    }

    scroll.current.addEventListener("scroll", handler)
    () => scroll.current.removeEventListener("scroll", handler)

    }, [])


  • 카심
    264
    2021-02-01 22:46:10 작성 2021-02-01 23:06:19 수정됨

    킁킁탐정 // 답변 감사합니다.

    클래스형에서는 해당 코드로 쓰는걸 알고 있는데, useRef도 결국 해당 이벤트 리스너로 가져와야하는군요.

     useEffect(() => {
        componentDidMount = () => {
          window.addEventListener("scroll", this.listenScrollEvent)};
        componentWillUnmount = () => {
          window.removeEventListener("scroll", this.listenScrollEvent)};
      })
    
      const listenScrollEvent = () => {
        if (window.scrollY > 220) {
          setFirstFeed({ firstFeed: true });
        }
    
        if (window.scrollY > 1000) {
          setSecondFeed({ secondFeed: true });
        }
    
        if (window.scrollY > 1750) {
          setThirdFeed({ thirdFeed: true });
        }
    
        if (window.scrollY > 2300) {
          setFourthFeed({ fourthFeed: true });
        }
      };

    이렇게 짜는건 동료가 짠 코드 덕분에 저도 알고 있습니다.

    하지만 useRef랑 연동해서 짜는 것을 잘 모르다보니 질문중입니다.

    지금 말씀해주신 이벤트 리스너부분이 막혀서 어떻게 해야할지 모르겠습니다.

    즉, componentDidmount와 componentWillMount 부분을 잘 모르겠습니다.

      useEffect(() => {
        setPoint(scroll.current.scrollTop)
        scroll.current.addEventListener("scroll", scrollHandler)
        scroll.current.removeEventListener("scroll", scrollHandler)
      });
      console.log(point);
      const scrollHandler = (e) => {
        if(point.scrollTop > 400) {
          point.scrollTopView(point.scrollTopView(0, 1600))
        }
        if(point.scrollTop > 1800) {
          point.scrollTopView(point.scrollTo(0, 3400))
        }
        if(point.scrollTop > 3600) {
          point.scrollTopView(point.scrollTo(0, 5100))
        }
        if (point.scrollTop > 5300) {
          point.scrollTopView(point.scrollTo(0, 6800))
        }
      };


  • 킁킁탐정
    844
    2021-02-02 08:40:34 작성 2021-02-02 08:43:44 수정됨

    클래스형 컴포넌트와 다르게 함수형 컴포넌트는 라이프사이클 함수가 없습니다. 그렇다보니 useEffect()로 비슷하게 구현하여 사용합니다. 보통은 다음 처럼 사용합니다. 반드시 deps가 빈 배열 ([]) 이여야합니다.

    useEffect(() => {
      console.log('componentDidMount')
      return () => console.log('componentWillMount')
    }, [])

    위와 같은 형식으로 document 또는 window 이벤트를 사용할 경우 보통은 다음 처럼 사용합니다.

    useEffect(() => {
      const handler =  (event) => {
        // scroll event
      }
      window.addEventListener('scroll', handler)
      return () => window.removeEventListener('scroll', handler)
    }, [])

    handler 함수를 useEffect() 내부에 선언하는 이유는 react의 함수형 컴포넌트는 상태가 변경될때마다 컴포넌트 함수 전체를 다시 실행합니다. 그러므로 함수형 컴포넌트 내부에서 선언한 변수/함수는 상태 변경시 마다 새로운 함수를 정의합니다.  예를 들면 다음과 같습니다.

    function TestComponent() 
      const [message, setMessage] = useState('initial message')
      const handler = () => console.log('recreate')
      return <button type="button" onClick={() => setMessage("nothing")}>{message}</button>
    }

    위의 TestComponent는 처음 렌더링시에 handler 함수가 생성되고 버튼을 클릭해서 상태가 변경되면  다시 렌더링되면서 또 다시 handler 함수가 생성됩니다. 즉 TestComponent가 2번 실행되고 handler 함수도 두번 만들어집니다. 그러므로 handler 함수의 레퍼런스 값이 처음과 달라지게 됩니다.

    useState, useRef, useCallback, useMemo, useEffect 등과 같은 대다수의 use로 시작하는 함수들은 렌더링마다 함수/값을 실행 또는 만들지 않고 이전 함수/값을 되돌려주거나 특정 조건에 의해서만 다시 만들거나 실행합니다.

    이 같은 이유로 addEventListener, removeEventListener에 전달하는 handler 함수는 반드시 useEffect 내부에 선언하거나 useCallback를 사용하거나 하는등 늘 처음에 정의한 함수 그대로여야하고 다시 정의된 함수가 아니여야합니다.

    보통 실수로 외부로 빼면 문제가되는건 addEventListener가 아닌 removeEventListener가 됩니다. 컴포넌트가 ummount되었는데도 scroll 이벤트가 제거되지 않아 에러가 발생할 수 있습니다.




  • 킁킁탐정
    844
    2021-02-02 09:36:43 작성 2021-02-02 09:37:16 수정됨

    예제를 하나 만들어드립니다. 참고하시면 도움이 되실겁니다. 그리고 scroll event는 너무 빈번하게 자주 일어나기 때문에 적절하게 발생 주기를 조절해야합니다. 이 관련은 직접 하시길 바라면서 관련 사이트 주소를 첨부해드립니다.


    * Scroll event throttling

    https://developer.mozilla.org/ko/docs/Web/API/Document/scroll_event#%EC%8A%A4%ED%81%AC%EB%A1%A4_%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%9D%98_%EC%A1%B0%EC%A0%88


    * Example (예제를 스크롤 해보세요)

    https://codesandbox.io/s/quizzical-liskov-oxs4w?file=/src/App.js



    function ContentA(props) {
    const ref = useRef(null);
    const [match, setMatch] = useState(false);

    useEffect(() => {
    const rectTop = ref.current.offsetTop;
    const rectBottom = ref.current.offsetTop + ref.current.offsetHeight;
    const isMatch = rectTop <= props.scrollY && rectBottom >= props.scrollY;
    console.log("ContentA", rectTop, rectBottom, props.scrollY, isMatch);

    setMatch(isMatch);
    }, [props.scrollY]);

    return (
    <div ref={ref} className={match ? "Content Match" : "Content"}>
    ContentA
    </div>
    );
    }

    function ContentB(props) {
    const ref = useRef(null);
    const [match, setMatch] = useState(false);

    useEffect(() => {
    const rectTop = ref.current.offsetTop;
    const rectBottom = ref.current.offsetTop + ref.current.offsetHeight;
    const isMatch = rectTop <= props.scrollY && rectBottom >= props.scrollY;
    console.log("ContentB", rectTop, rectBottom, props.scrollY, isMatch);

    setMatch(isMatch);
    }, [props.scrollY]);

    return (
    <div ref={ref} className={match ? "Content Match" : "Content"}>
    ContentB
    </div>
    );
    }

    function ContentC(props) {
    const ref = useRef(null);
    const [match, setMatch] = useState(false);

    useEffect(() => {
    const rectTop = ref.current.offsetTop;
    const rectBottom = ref.current.offsetTop + ref.current.offsetHeight;
    const isMatch = rectTop <= props.scrollY && rectBottom >= props.scrollY;
    console.log("ContentC", rectTop, rectBottom, props.scrollY, isMatch);

    setMatch(isMatch);
    }, [props.scrollY]);

    return (
    <div ref={ref} className={match ? "Content Match" : "Content"}>
    ContentC
    </div>
    );
    }

    export default function App() {
    const [scrollY, setScrollY] = useState(0);

    useEffect(() => {
    const handler = () => {
    setScrollY(window.scrollY);
    };
    window.addEventListener("scroll", handler);
    return () => window.removeEventListener("scroll", handler);
    }, []);
    return (
    <div className="App">
    <ContentA scrollY={scrollY} />
    <ContentB scrollY={scrollY} />
    <ContentC scrollY={scrollY} />
    </div>
    );
    }

  • 로그인을 하시면 답변을 등록할 수 있습니다.