티스토리 뷰

Scrap 페이지에서 Chat 페이지로 이동 후 스크롤이 해당 메세지에 위치하고 질문을 할 때에는 스크롤이 이동하지 않는다

스크롤 제일 아래로 보내는 이벤트와 스크랩 페이지에서 선택했던 메세지가 있는 곳으로 이동시키는 스크롤 이벤트 두 가지가 있다.

1. 컴포넌트 초기 렌더링 시 URI에 hash가 있으면 hash로 스크롤 위치시키고 없으면 맨 아래로 내려야 함. scrollRef.current.scrollTop에 할당. useEffect(( ) => { // hash 엘리먼트로 scroll 이동시키는 로직 }, [ ])

2. useSelector가 렌더링 이후에 실행되기 때문에 업데이트된 messages 상태를 가지고 scroll을 내리기 위해
useEffect(( ) => { // messages 맨 아래로 scroll 이동시키는 로직 }, [messages])

3. 이렇게 되면 1번은 항상 덮어씌어진다. 1번이 실행되고 나서 2번이 실행된다.

4. messages를 가지고 오고 나서 hash가 있으면 딱 한 번을 실행해야 하는데, 빈 [ ] deps를 줘서 마운트될 때 실행시키면 2번이 덮어쓰니.
messages를 가지고 오고 나서 딱 한번 실행되게 만드는 방법이 없을까.

 

실패

React는 값을 유지시키려면 상태를 사용해야 하고 상태는 값이 바뀌면 리렌더링 된다.
값을 유지시키면서 리렌더링을 하고 싶지 않을 때에는 useRef를 사용할 수 있다.

useRef를 사용하여 messages가 업데이트되고나서 딱 한 번 실행할 custom Hooks를 만들어준다.

// useEffectAfterGetData.tsx
import { useEffect, useRef } from "react";
import { IMessage } from "./interfaces";

const useEffectAfterGetData = (
  data: IMessage[],
  func: React.EffectCallback,
  deps?: React.DependencyList | undefined
): void => {
  const isExecuted = useRef(false); // func의 실행 유무

  useEffect(() => {
    if (data.length !== 0 && !isExecuted.current) { // data가 있고 func가 실행된 적이 없으면 실행
      func();
      isExecuted.current = true;
    }

    return () => {
      isExecuted.current = false; // useRef값은 계속 유지되기 때문에 컴포넌트 언마운트시 실행유무 초기화
    };
  }, deps);
};

export default useEffectAfterGetData;
// Messages.tsx
export default function Messages({ chatId, isFetching }: IMessagesProps) {
  const dispatch = useAppDispatch();
  const scrollRef = useRef<HTMLUListElement>(null);

  const {
    requestGetMessages: { data: messages },
  } = useAppSelector((state) => state);

  const orderedMessages = useMemo(
    () => JSON.parse(JSON.stringify(messages)).sort((a: IMessage, b: IMessage) => a.seq - b.seq),
    [messages]
  );

  useEffect(() => {
    dispatch(requestGetMessages(chatId));
  }, []);

  useEffect(() => {
    scrollRef.current &&
      (scrollRef.current.scrollTop = scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
  }, [orderedMessages, isFetching]);

  useEffectAfterGetData(
    messages,
    () => {
      const { hash } = window.location;
      const scrollToHashElement = () => {
        const elementToScroll = document.getElementById(hash?.replace("#", ""));
        if (!elementToScroll) return;
        scrollRef.current && (scrollRef.current.scrollTop = elementToScroll.offsetTop - 30);
      };

      if (hash) {
        scrollToHashElement();
      }
    },
    [orderedMessages]
  );

  return (
    <MessagesWrapper>
      <MessagesContainer ref={scrollRef}>
      ...
  )
}

그런데 의미가 없다. customHooks의 전달인자 deps가 customHooks의 useEffect의 deps로 그대로 전달되기 때문에 cleanup 함수도 deps 요소가 업데이트될때마다 실행되어 매번 false로 초기화되어 딱 한번 실행되는게 아니라 계속 실행된다.

또한, Messages 컴포넌트가 처음 렌더링되면 data(messages)가 길이가 0이라서 의도한대로 작동하는데, 언마운트되고 다시 컴포넌트를 렌더링하면 useSelector가 계속 subscribe 하고있어서 초기 렌더링부터 data(messages)가 이전 상태값으로 유지되고 있다. 그래서 customHooks의 useEffect 콜백함수가 실행조건이 충족되어 버린다. 새로운 messages 를 가져오고 나서 실행되어야 하는데 이전 messages를 가지고 있는 상태여서 이미 실행이 되고, useRef로 인해 새로운 messages로 업데이트되고 나서는 실행이 안될 것이다. (지금은 이전 단락에서 이야기한대로 deps 업데이트로 인해 계속 실행이 되지만 deps 문제를 해결해도 messages를 가져오고 '나서' 한번 실행시켜야 하므로 이전 messages 상태가 유지되는 한 data(messages).length !== 0 조건은 알맞지 않다)

 

성공(?)

비동기 함수도 아니고 동기적으로 실행될 것이고, useRef를 사용해서 실행여부를 가릴 것이라면 굳이 같은 useEffect를 두 번 사용하고 customHooks까지 만들 필요가 없어보인다.
useEffectAfterGetData.tsx를 없애고 Messages.tsx를 다음과 같이 수정했다.

useEffect를 사용하는 이상 초기 렌더링 이후 실행되는 것을 막을 방법이 없어서 실행 횟수로 조건을 준다. hash가 있고 excutedTimes < 2 가 true이면 hash값을 id로 가지는 엘리먼트로 scroll을 위치시키는 함수를 호출한다.

false이면 메시지 목록의 최 하단으로 스크롤을 위치시켜 최신 메시지를 보여준다.

// Messages.tsx
function Messages({ chatId, isFetching }: IMessagesProps) {
  const dispatch = useAppDispatch();
  const scrollRef = useRef<HTMLUListElement>(null);
  const hashRef = useRef({ excutedTimes: 0 });

  const {
    requestGetMessages: { data: messages },
  } = useAppSelector((state) => state);

  const orderedMessages = useMemo(
    () => JSON.parse(JSON.stringify(messages)).sort((a: IMessage, b: IMessage) => a.seq - b.seq),
    [messages]
  );

  useEffect(() => {
    dispatch(requestGetMessages(chatId));

    return () => {
      hashRef.current.excutedTimes = 0;
    };
  }, []);

  useEffect(() => {
    scrollRef.current &&
      (scrollRef.current.scrollTop = scrollRef.current.scrollHeight - scrollRef.current.clientHeight);

    const { hash } = window.location;
    const scrollToHashElement = () => {
      const elementToScroll = document.getElementById(hash?.replace("#", ""));
      console.log("el", elementToScroll);
      if (!elementToScroll) return;
      scrollRef.current && (scrollRef.current.scrollTop = elementToScroll.offsetTop - 30);
    };

    if (hash && hashRef.current.excutedTimes < 2) {
      scrollToHashElement();
      hashRef.current.excutedTimes++;
    }
  }, [orderedMessages, isFetching]);

  return (
    <MessagesWrapper>
      <MessagesContainer ref={scrollRef}>
      ...
  )
}


flag처럼 호출 횟수로 구분하는 것은 정확하지 않을 것 같다 .. 그런데 다른 방법은 떠오르지 않는다 🥲

Scrap 페이지에서 Chat 페이지로 이동한 후에 해당 메세지로 스크롤이 위치하고 질문을 하면 스크롤이 맨 아래로 내려온다.

 

반응형
댓글