[회고록] - select 컴포넌트 / window객체의 scroll 이벤트로 무한스크롤

유선향·2025년 1월 26일
0

<부트캠프 회고록>

목록 보기
5/11

지난 팀프로젝트 1 기획글
에 따라 나는 select 컴포넌트, 무한 스크롤 페이지 구현 을 일단 진행했다.

select 공통 컴포넌트

이번에는 좀더 본질적으로 select의 기능을 이해하기 위해 ul ,li 태그를 이용해서 만들어 보기로 했다.

설명

  • select에 들어갈 oprtion을 배열로 받고, 전달 받은 배열을 맵핑해서 option 으로 보여준다.
  • isOpen state로 그 값에 따라서 매핑된 li태그는 display: none, block 값을 조건연산자로 렌더링 된다.
function SelectDropDown({ options }) {
  const [selected, setSelected] = useState([]);
  const [isOpen, setIsOpen] = useState(false);

  const handleOptionClick = (option) => {
    setSelected(option);
    setIsOpen(false); 
  };

  return (
    <>
      <DropDown>
        <Selected isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
          {selected}
          {isOpen ? (
            <img src={arrowOpen} alt="^" />
          ) : (
            <img src={arrowClose} alt="\/" />
          )}
        </Selected>
        <Options isOpen={isOpen}>
          {options.map((option, index) => (
            <OptionList key={index} onClick={() => handleOptionClick(option)}>
              {option}
            </OptionList>
          ))}
        </Options>
      </DropDown>
    </>
  );
}
export default SelectDropDown;
....

export const DropDown = styled.div`
 ....
  position: relative;
`;

export const Selected = styled.div`
/*isOpen 값에 따라 boder, color 등 스타일링 */
...
...
  border: ${({ isOpen, theme }) =>
    isOpen
      ? `2px solid ${theme.color.Grayscale500}`
      : `1px solid ${theme.color.Grayscale300}`};
  color: ${({ isOpen, theme }) =>
    isOpen ? theme.color.Grayscale900 : theme.color.Grayscale500};
`;

export const Options = styled.ul`
  display: ${({ isOpen }) => (isOpen ? "block" : "none")};
	// => isopen 에따라 display 변경 
  .....
`;

export const OptionList = styled.li`
 ....
`;



무한 스크롤

구현

  1. 일단 쿼리의 id 값을 test로 만든 recipient (=> 이프로젝트에서는 롤링페이퍼를 받는 수신자의 id 이다) 으로 고정
  2. 초기 렌더링시에 + 박스와 해당 Id의 limit=8 데이터를 렌더링한다.
  3. 일단 더보기 버튼을 만들어 클릭시 limit=9 데이터를 렌더링한다.
  4. 더보기 버튼을 눌렀을때 사용한 핸들러를 scroll 이벤트에 핸들러로 등록한다.

코드 작성

일단 3번까지는 무리 없이 착착 진행되었다.

const handleLoad = async () => {
    setIsLoading(true);
    let limit = offset === 0 ? 8 : 9;
    const { results, next } = await getMessage(limit, offset, id);
    if (!results) return;
    setMessages((prevMessages) => [...prevMessages, ...results]);
    setOffset(offset + limit);
    setIsLoading(false);
    setHasNext(next);
  };
  • offset 상태값을 초기에는 0으로 하여 handleLoad 안에서 초기 렌더링시 자연스럽게 limit=8로 리퀘스트를 보내고,
  • 데이터를 받아온후 offset 상태값은 offset+limit 가 되어 다음 handleLoad 작동시 limit는 9가 되게끔 구현했다.

잘 안되었던 부분

자이제 본격적으로 무한스크롤을 지정해야하는 4번부터 막히기 시작했다.

일단 무한 스크롤 참고 블로그

❗️useRef를 사용하여 DOM 의 current로 무한스크롤 구현하기 ❗️

  • 리액트에서 DOM 에 직접적으로 접근하는것은 권장 하지 않기 때문에, DOM에 접근하기위해서는 useRef 훅의 current 프로퍼티를 이용한다.
function InfiniteScroll() {
  const scrollRef = useRef(null);
  // ...

  return <div ref={scrollRef} />;
}

  • 이렇게 접근한 DOM에서 current 프로퍼티를 이용해서 clientHeight, scrollHeight, scrollTop 값을 구조분해 하여 가져온다.
const { clientHeight, scrollHeight, scrollTop } = scrollRef.current;
  • clientHeight(사용자의 모니터에 보이는 화면 ) + scrollTop(수직방향으로 얼마나 스크롤 되었는지 나타냄) >= scrollHeight (화면의 전체 높이) 일때 스크롤이 끝까지 내려갔다고 볼수 있다.

그러나 뜻대로 되지 않는 무한 스크롤...

콘솔 디버깅을 해본결과 나의 경우에는 이벤트의 등록과 삭제 모두 잘 되었지만, 스크롤이 끝까지 닿아도 event 핸들러 함수 내부로 들어가지 않았다.

1. ❗️ ref가 지정된 html 요소에 높이 지정 확인하기

  • 가장 간단한 해결책이다. ref가 지정된 요소에서 스크롤 이벤트가 동작하려면 높이가 있어야 한다.
  • 또한 내용이 화면을 벗어날만큼 많을때 스크롤이 생길수 있게 overflowY 를 지정해 주어야 한다.
function InfiniteScroll() {
  const scrollRef = useRef(null);
  // ...

  return <div ref={scrollRef} style={{ overflowY: "auto" }}  />;
}

그러나 나의 경우에는 사용자가 지정한 색깔이 롤링페이퍼의 배경으로 적용되어야 하고, 높이를 지정하니 지정한 높이만큼만 배경 스타일이 적용되어서 이방법을 사용할수 없었다.


2. ✅ 이벤트를 window 에 직접 등록하기

  • 리액트에서 DOM에 직접 접근하는것을 비권장 하긴 하지만, 스크롤 이벤트의 경우에는 일반적으로 접근이 허용되기도 한다.

언제 document나 window에 접근해도 괜찮은가?

  • React로도 처리하기 어려운 브라우저 전역적인 동작(예: 스크롤, 리사이즈, 키보드 이벤트 등)을 다룰 때는 document와 window 접근이 일반적이고 허용된다.
  • React의 철학은 컴포넌트의 상태 관리에 집중된 것이지, DOM의 모든 제어를 금지하는 것은 아니기에 적절한 사용은 괜찮다!

대표적인 예시

  1. 스크롤 이벤트: 페이지 단위의 무한 스크롤 구현.
  2. 뷰포트 크기 확인: 리사이즈 이벤트를 감지해 UI를 변경.
  3. 외부 클릭 감지: window.addEventListener("click", ...)로 모달 외부 클릭 처리.

그리하여 완성된 코드 !

  • 스크롤 이벤트의 경우 스크롤을 내릴때마다 이벤트 등록과 삭제, 핸들러가 발생하고, 핸들러 내부에서 if 문으로 스크롤이 다 내려갔는지를 확인하는것이 반복되기에,
    lodash 라이브러리를 이용해서 throttle을 사용하였다.

  • 그리고 간혹 사용자중에 (나같은 경우가 그렇다.) 스크롤을 거칠게 내려서 사용환경에 따라 스크롤이 다시 튕겨 올라오는 경우를 대비해서
    스크롤이 다내려갔는지 확인하는 if문에서 조건을
    (clientHeight + scrollTop >= scrollHeight - 1) 라고 작성하여 스크롤이 다내려가기 1px 전에 이벤트가 발생하게끔하였다.

import { throttle } from "lodash";
...

  const handleLoad = async () => {
    setIsLoading(true);
    let limit = offset === 0 ? 8 : 9;
    const { results, next } = await getMessage(limit, offset, id);
    if (!results) return;
    setMessages((prevMessages) => [...prevMessages, ...results]);
    setOffset(offset + limit);
    setIsLoading(false);
    setHasNext(next);
  };

  useEffect(() => {
    if (!isLoading) {
      handleLoad();
    }
    if (hasNext) {
      console.log("이벤트 등록");
      window.addEventListener("scroll", infiniteScroll);
    }
    return () => {
      infiniteScroll.cancel();
      window.removeEventListener("scroll", infiniteScroll);
      console.log("이벤트 삭제");
    };
  }, [isScrollEnd]);

  const infiniteScroll = useCallback(
    throttle(() => {
      if (!isLoading) {
        const { clientHeight, scrollHeight, scrollTop } =
          document.documentElement;
        if (clientHeight + scrollTop >= scrollHeight - 1) {
          setIsScrollEnd((prev) => !prev);
        }
      }
    }, 200),
    []
  );
  const handelEditClick = () => {
    setIsEdit(true);
  };
  const handelDeleteClick = () => {
    setIsEdit(false);
  };
  //
  return (
    <div style={{ overflowY: "auto" }}>
      <S.Contents>
        {!isEdit && (
          <Button style={{ marginBottom: "11px" }} onClick={handelEditClick}>
            편집하기
          </Button>
        )}
        {isEdit && <Button onClick={handelDeleteClick}>저장하기</Button>}
        <Test3 isEdit={isEdit} messages={messages} />
      </S.Contents>
    </div>
  );
}

export default Test;

0개의 댓글