[나만의 무기] 스크롤 삽질 기록

yeopto·2022년 8월 9일
1

SW사관학교 정글

목록 보기
14/14
post-thumbnail

Intro

간만에 포스팅이다. 프로젝트가 끝났다. 첫 프로젝트라 그런지 쉽지 않은 여정이었다.. 프로젝트 중에 포스팅을 하기엔 개발 할게 너무 많았다.. 끝나고 해결과정을 써 보려고 사진과 동영상을 남겨 놓았는데 지금 써 본다.. 이번 프로젝트에서 스크롤에 대해 삽질을 많이 했다.. 본론으로 고고씽

1. 로딩 속도 실화?

첫번째 문제 자료

화면이 띄어지는데 시간이 너무 오래 걸리는 것이다. 팀에서 프론트를 담당하게 됐는데 이렇게 느린 로딩을 어떤 유저가 좋아하겠는가? 가만히 내버려둘 수 없었다. 처음엔 스크롤 할 때마다 요청하는 것이 비용이 크고 서버에 부하를 주지 않을까? 라는 생각을 했다. 그래서 데이터를 한번에 받아도 프론트에서 해결할 수 있는 방법이 뭐가 있을지 생각하다가 데이터를 한번에 받되 네 개씩 뿌려주면 어떨까?라는 생각에 로직을 변경해보았다.

개선 코드 1

// src/pages/WorkerNearWorkPage.jsx

const [itemIndex, setItemIndex] = useState(0); // 인덱스 상태관리(4개)
const [items, setItems] = useState(4); // 인덱스 상태관리(4개)
const [result, setResult] = useState([]); // 뿌려줄 데이터들

const getData = async () => {
    await axios
    .post("http://localhost:4000/worker/show/hourly_orders", {
      worker_id: sessionStorage.getItem("worker_id"),
    })
    .then((res) => {
      console.log(">>>>>>>>>>>>", res.data);
      setStores(res.data); // 모든 데이터 저장
      setResult(res.data.slice(itemIndex, items)); // 그 중 네개만 일단 Result에 저장
    });
  };
  
  useEffect(() => {
    getData();
    .
		.
		.
    });
  }, []);

const _infiniteScroll = useCallback(() => {
    let scrollHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
    let scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
    let clientHeight = document.documentElement.clientHeight;

    if(scrollTop + clientHeight === scrollHeight) { // 바닥에 닿아질 때
      setItemIndex(itemIndex + 4); // 인덱스를 다시 재설정 
      setResult(result.concat(stores.slice(itemIndex + 4, itemIndex + 8))); // 기존 4개를 합쳐줌 
    }
  }, [itemIndex, result]);

  useEffect(() => {
    window.addEventListener('scroll', _infiniteScroll, true);
    return () => window.removeEventListener('scroll', _infiniteScroll, true);
  }, [_infiniteScroll]);

개선 결과 1

이 때 느꼈다. 데이터를 받고 렌더링하는데엔 시간이 줄어들었지만, 이건 데이터 받아오는데 시간이 너~무 오래 걸리는 것이 문제라는걸.. 이전과 비교해봤을 때 데이터를 받고나서 렌더링하는 시간은 눈에 띄게 빨라졌다.

두번째 문제 자료

데이터 받는 데 시간이 오래걸린다는 것이 확실한 문제라는 것을 알게 되었고, 주변일감 페이지를 함께 했던 왕형과 이야기한 끝에 프론트에선 무한스크롤을 도입하고 서버에선 cursor방식의 페이지네이션 api를 구축해서 데이터를 주기로 결론이 났다. cursor 라는 상태를 하나 선언하고, 맨 처음 페이지 렌더링 시 요청할 때 커서 값 “null” 을 담아 줘서 응답받은 데이터 배열 마지막 원소의 id를 cursor 상태값으로 변경해주고 스크롤이 바닥에 닿을 시 상태가 바뀐 cursor 와 함께 데이터 요청을 해서 받은 데이터를 렌더링 시키는 방식으로 구현하였다.

개선코드 2

// src/pages/WorkerNearWorkPage.jsx

const [cursor, setCursor] = useState(0);

const getData = async () => {
    await axios
      .post(`${process.env.REACT_APP_ROUTE_PATH}/worker/show/hourly_orders`, {
        worker_id: sessionStorage.getItem("worker_id"),
        cursor: "null", // 커서 추가
      })
      .then((res) => {
        if (res.data === "notFound") {
          setIsNotFound(true);
        } else {
          // 데이터 파싱
          setCursor(res.data[res.data.length - 1].store_id);
          setStores(res.data);
        }
      });
  };

useEffect(() => {
    getData();
    .
		.
		.
    });
  }, []);

const _infiniteScroll = useCallback(() => {
    let scrollHeight = Math.max(
      document.documentElement.scrollHeight,
      document.body.scrollHeight
    );
    let scrollTop = Math.max(
      document.documentElement.scrollTop,
      document.body.scrollTop
    );
    let clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight === scrollHeight) {
      // console.log(cursor)
      axios
        .post(`${process.env.REACT_APP_ROUTE_PATH}/worker/show/hourly_orders`, {
          worker_id: sessionStorage.getItem("worker_id"),
          cursor: cursor,
        })
        .then((res) => {
          if (res.data === "notFound" || res.data.length === 0) {
            return;
          }
          // 데이터 파싱
          setCursor(res.data[res.data.length - 1].store_id);
          setStores((list) => [...list, ...res.data]);
        });
    }
  }, [stores]);

  useEffect(() => {
    window.addEventListener("scroll", _infiniteScroll, true);
    return () => window.removeEventListener("scroll", _infiniteScroll, true);
  }, [_infiniteScroll]);

개선결과 2

잘 구현된 것을 볼 수 있다! 처음엔 서버에 부하를 많이 준다고 생각했지만, 유저가 필요할 때까지만 데이터를 본다면 데이터가 많은 경우엔 데이터를 나눠서 요청하는 것이 효율적이라는 것을 알게되었다!

2. 반응형 문제

문제자료

프로젝트 막바지 쯤 배포를하고 왕형과 혜원이 폰에서 테스트를 해봤는데 스크롤이 바닥에 닿았는데도 데이터가 나오지 않았다.. 분명 크롬 개발자도구에서 모바일 테스트를 다해보고 배포를 했는데 무엇이 문제일지 감이 안잡혔다. 일단 데이터 요청 조건이 scrollTop + clientHeight === scrollHeight 이렇게 되니까 콘솔을 한번 찍어보았다.

콘솔창을 봐보면 스크롤 시 scrollheightclientheight 은 고정인 것을 확인 할 수 있다. 스크롤을 움직일 때 마다 scrollTop의 값이 변화한다는 걸 알 수 있었다. 콘솔 마지막이 스크롤이 바닥을 닿았을 때 찍힌 로그다. 788 + 653이 1516이 안되는 것이었다.. 기기마다 스크롤 수치가 달랐고 그래서 특정 핸드폰에서는 되지 않는다는 걸 깨달았다! 조건을 도달하기위해선 75정도 더 필요했는데 모든 모바일 환경을 생각해서 조금 더 널널한 수치를 주어 조건을 변경하였다.

개선코드

// src/pages/WorkerNearWorkPage.jsx

const _infiniteScroll = useCallback(() => {
    let scrollHeight = Math.max(
      document.documentElement.scrollHeight,
      document.body.scrollHeight
    );
    let scrollTop = Math.max(
      document.documentElement.scrollTop,
      document.body.scrollTop
    );
    let clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight + 130 >= scrollHeight) { // 조건 변경, 130인 이유는 배포 후 시연 폰에서 테스트 시 제일 안정적인 수치
      // console.log(cursor)
      axios
        .post(`${process.env.REACT_APP_ROUTE_PATH}/worker/show/hourly_orders`, {
          worker_id: sessionStorage.getItem("worker_id"),
          cursor: cursor,
        })
        .then((res) => {
          if (res.data === "notFound" || res.data.length === 0) {
            return;
          }
          // 데이터 파싱
          setCursor(res.data[res.data.length - 1].store_id);
          setStores((list) => [...list, ...res.data]);
        });
    }
  }, [stores]);

  useEffect(() => {
    window.addEventListener("scroll", _infiniteScroll, true);
    return () => window.removeEventListener("scroll", _infiniteScroll, true);
  }, [_infiniteScroll]);

최종결과

3. 채팅방에서도 스크롤 시 이슈가..

문제자료

상황

채팅방 페이지에선 무한 스크롤을 onScroll 방식이 아닌 react-intersection-observer 라이브러리를 사용하여 구현하였다. useInView 를 사용하면 ref 로 설정된 채팅 메시지가 보이게되면 InViewtrue가 된다. 로직은 useEffect 를 사용하여 InView 값이 변경될 때 데이터를 요청하고 받은 데이터를 기존 데이터에 합쳐서 기존 데이터가 변경 될 때 re-rendering 되도록 설계하였다.

그로인한 문제

문제는 스크롤 시 데이터를 요청하여 데이터가 합쳐질 때 re-rendering이 되기 때문에 첫 번째 데이터부터 보여진다는 것이 문제였다. 유저 입장에선 과거 메세지를 불러오면 원래 보던 메세지로 돌아가기위해 스크롤을 내려야하고 그로 인해 굉장히 헷갈리게 되는 상황인데 이러한 UX를 개선하고 싶었다.

해결방법은? 그리고 또 다시 생긴 문제

InView 값이 변할 때(데이터 요청하는 시점) scrollHeightprevScrollHeight 라는 상태로 저장해주고, 기존 데이터가 변경되어 re-rendering 할 때 현재 scrollHeight 에서 prevScrollHeight 뺀 위치로 스크롤을 이동시켜주면 되겠다는 생각을 했다. 하지만 문제가 있었다. UI 설계를 div태그 안에 스크롤 설정을 해놔서 그 상태에서 scrollHeight 은 고정 값이 되어 re-rendering 이전의 scrollHeight 을 잡을 수 있는 방법이 없었다.

해결

scrollHeight을 인식 할 수 있도록 채팅 UI를 공사했다. div내의 스크롤 설정을 제외시키고 수정해서 prevScrollheight 을 설정할 수 있게 만들었다. 그 후 위에 설명한 해결방법으로 해결할 수 있었다.

개선코드

// src/pages/ChatRoomPage.jsx

const [ref, inView] = useInView();

useEffect(() => {
    if (inView) {
      setPrevScrollHeight(document.documentElement.scrollHeight);
      axios
        .get(`${process.env.REACT_APP_ROUTE_PATH}/chatting/message/loading`, {
          params: {
            room_id: roomId,
            cursor: chatId,
            user_id: userId,
            user_type: userType,
          },
        })
        .then((res) => {
          setChatId(res.data[res.data.length - 1].chatting_id);
          const arr = res.data.sort((a, b) => {
            return a.chatting_id - b.chatting_id;
          });
          return arr;
        })
        .then((arr) => {
          setMessageList((list) => [...arr, ...list]);
        })
        .catch((err) => {
          console.log(err);
        });
    }
  }, [inView]);

// 원래 로직에서 추가된 코드  
useEffect(() => {
    if (prevScrollHeight) {
      document.documentElement.scrollTo(
        0,
        document.documentElement.scrollHeight - prevScrollHeight
      );
      return setPrevScrollHeight(null);
    }

    document.documentElement.scrollTo(
      0,
      document.documentElement.scrollHeight -
        document.documentElement.clientHeight
    );
  }, [messageList]);

개선결과

4. 결론

정리하면서 다시 돌아보면 스크롤 삽질을 정말 많이했다.. 안될때마다 괴로웠던 순간이 아직도 생생하다. 특히 채팅 저 이슈는..(채팅 자체에서 스크롤 위를 인지하는게 어려웠음) 진짜 오래걸렸고 힘들었다 정신적으로. 하지만 이렇게 지나고보니 결국 다 만들어내긴 했다.. 좀 뿌듯하다. 별거 아닐 수 있을 문제해결들이지만 첫 개발 프로젝트하는 나에겐 하나하나가 성장하는데 도움이 되었던 것 같다.

profile
https://yeopto.github.io로 이동했습니다.

0개의 댓글