Skip to content

feat: virtual scroll 기능 구현#93

Merged
howyoujini merged 11 commits intodevfrom
virtualScroll
Nov 20, 2024
Merged

feat: virtual scroll 기능 구현#93
howyoujini merged 11 commits intodevfrom
virtualScroll

Conversation

@howyoujini
Copy link
Contributor

@howyoujini howyoujini commented Nov 18, 2024

이슈

🔗 이슈 링크 #68

요약

  • 이미 전체 데이터를 알고 있는 상태에서, 가상스크롤을 구현하여 퍼포먼스를 최적화했습니다.

구현화면 [performance 탭]

virtual scroll 적용안했을 때, INP 첫 로딩의 경험이 굉장히 안좋음

  • INP는 사용자가 페이지를 방문하는 전체 기간에 발생하는 모든 클릭, 탭, 키보드 상호작용의 지연 시간을 관찰하여 사용자 상호작용에 대한 페이지의 전반적인 응답성을 평가하는 측정항목을 말한다. [참고링크]
  • virtual scroll 적용안했을 때

virtual scroll 적용시, CLS 가 조금 안좋지만 이는 개선의 여지가 있음 (-> 추후 skeleton UI 만들기 등으로 해결가능)

  • CLS 란 예상치 못한 레이아웃 전환은 텍스트가 갑자기 이동하여 읽는 도중에 위치를 잃게 되거나 잘못된 링크나 버튼을 클릭하게 되는 등 다양한 방식으로 사용자 환경을 방해하는 것이 있나, 체크하는 측정단위 [참고링크]
virtual scroll 적용시

작업내용

  • useWindowSize 훅을 구현했습니다.
  • useWindowScroll 훅을 구현했습니다.
  • VirtualScroll 공용 컴포넌트를 구현했습니다.

참고사항

  • virtualScroll 브랜치로 이동하여, 잘 동작하는지 확인부탁드립니다 !

PR 체크 사항

주의 사항

  • PR 크기는 300~500줄로 하되 최대 1000줄 미만으로 합니다.
  • conflict를 모두 해결하고 PR을 올려주세요.

PR 전 체크리스트

  • 가장 최신 브랜치를 pull 하였습니다.
  • virtualScroll 브랜치명을 확인하였습니다.
  • 코드 컨벤션을 모두 확인하였습니다.
  • 브랜치명을 확인하였습니다.

리뷰 반영사항

  • [ ]

@howyoujini howyoujini added the feat New feature or request label Nov 18, 2024
@howyoujini howyoujini self-assigned this Nov 18, 2024
index.html Outdated
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="public/favicon.ico" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정을 깜빡하신 것 같아요!

@howyoujini howyoujini closed this Nov 19, 2024
@howyoujini howyoujini deleted the virtualScroll branch November 19, 2024 10:22
@howyoujini howyoujini restored the virtualScroll branch November 19, 2024 10:47
@howyoujini howyoujini reopened this Nov 19, 2024
(cherry picked from commit 52c4012)
(cherry picked from commit 21ebba8)
(cherry picked from commit c87b2c2)
@howyoujini howyoujini linked an issue Nov 19, 2024 that may be closed by this pull request
2 tasks

const useShowCommitDetails = () => {
const commitList = useCommitStore((state) => state.commitInfo.commitList);
const needsVirtualScroll = commitList.length > MAX_LENGTH_RENDERING_BOX;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보여주어야 할 결과가 300개 이상일 때 virtual scroll이 적용되도록 해주셨는데 300개로 정해주신 이유가 궁금합니다~ 여러 레포지토리로 테스트한 결과로 나온 평균치일까요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다!
우선은 지금 일일히 확인하기 어렵기 때문에, 여러 레포지토리로 테스트한 결과로 나온 평균치로 300으로 정의하였습니다

commit={commit}
/>
))}
</VirtualScroll>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트는 self closing tag가 가능하니 <VirtualScroll /> 로 변경하는 것이 더 좋을 것 같습니다!

Copy link
Contributor Author

@howyoujini howyoujini Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VirtualScroll 은 self closing tag 로 사용이 불가능합니다!

이유는, 스크롤의 전체 높이를 감싸야지만, children 의 length 를 계산해주고 있기 때문에, 감싸야지 가상 스크롤이 적용됩니다:)

children 을 사용하면, [참고링크]

<ParentComponent>
	<ChildComponent/>
</ParantComponent>
const ParantComponent =(props) =>{
	return <>
    	{props.children}
    </>
}

이렇게 children 을 활용할 수 있습니다! 이렇게 활용된 children 을 아래 VirtualScroll.jsx에서, children.length로 활용하고 있습니다!

const containerHeight = (itemHeight + columnGap) * children.length;

  const startIndex = Math.max(
    Math.floor(relativeY / (itemHeight + columnGap)) - renderAhead,
    0
  );

  const endIndex = Math.min(
    Math.ceil(height / (itemHeight + columnGap) + startIndex) + renderAhead,
    children.length
  );

  const visibleItem = children.slice(
    Math.max(startIndex, 0),
    Math.min(endIndex + 1, children.length)
  );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Childern prop인 경우엔 직접 컴포넌트를 감싸주는군요..! 설명 감사합니다~

children,
itemHeight,
columnGap = 0,
renderAhead = 5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동작과정에서 renderAhead의 의미가 무엇인가요?!
변수명으로 파악되는 부분은 이미 렌더된 값이라고 되어있는데 정확히 어떤 부분인지 궁금합니다!

viewport에 보여지는 부분만 알기 위해 커밋내용의 개수를 미리 파악하여 높이를 계산 후
이 높이만큼 transform을 통해 공간을 확보하게되는 동작 과정으로 봤을 땐 renderAhead이 어떤 역할인지 잘 떠오르지 않아 리뷰남깁니다~!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 스크롤이 아래로 움직일 때, dom 이 스크롤 위치에 따라 붙게 됩니다.

하지만 lazyloading 이라던가, dom 이 늦게 따라서 html 에 붙게 되면 사용자는 스크롤을 내리다가 흰 배경 혹은 Skeleton UI 가 먼저 뜨고 0.몇초후에 dom 이 뜨는 것을 보게 됩니다! 이런 사용자 경험을 개선하기 위해서 미리 먼저 로드를 해주는 갯수를 추가하여, 미리 dom 에 몇개 더 붙도록 해주는 역할을 합니다.

즉, 사용자가 현재 스크롤 위치 위 아래로 추가적인 DOM 요소를 미리 렌더링하여 스크롤 경험을 더 부드럽게 만드는 데 활용된다고 보면 될 것 같습니다!

5개로 잡은 이유는, renderAhead 값이 너무 크면 실제 필요한 데이터보다 많은 DOM 요소를 유지해야 하므로 메모리 사용량이 많아지게 되기 때문에 일반적으로 5-10 개를 적용한다고 합니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일반적으로 5~10개를 잡는지 처음 알았네요! 자세한 설명 감사합니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가적인 돔 요소를 뜻하는 것이었군요..! 감사합니다~

@howyoujini howyoujini requested a review from shkimjune November 20, 2024 07:04
Comment on lines +4 to +16
const [scrollPosition, setScrollPosition] = useState(() => ({
x: window.scrollX,
y: window.scrollY,
}));

useEffect(() => {
const handleScroll = () => {
setScrollPosition({
x: window.scrollX,
y: window.scrollY,
});
};

Copy link
Contributor

@GreenteaHT GreenteaHT Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x를 사용하지 않기 때문에 사용하지 않는 state에 대한 불필요한 리소스 소모가 있을 것 같아 조금 조사를 해보았습니다.

  1. x와 y를 따로 구독해서 이용하기 때문에 x만 변경사항이 있더라도 y에 대한 구독된 요소는 렌더링 되지 않습니다.
  2. 다만 setter를 이용하기 때문에 비교작업이 일어나 아주 약간의 리소스 소모가 발생합니다.
  3. 물론 사용하지 않는 x state를 제거하는 것이 이상적이지만, 모듈로 이용하기 위해 놔두는 것이 좋겠습니다.

어떻게 생각하시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네! 좋은 의견 감사합니다. 저도 고민해보지 못한 부분이라 더 생각해볼만한 주제인 것 같네요 :)

우선 y 값만 사용하고 있지만, 훅을 x, y 로 나누기 보다 useWindowScroll 에서 각 값을 메모이제이션을 활용하여 최적화를 해볼 수 있을 것 같습니다!

useMemo는 특정 값이 변경되지 않으면 이전에 계산된 값을 재사용하기 때문에, x 값이 바뀔때만 다시 계산하도록 최적화해보겠습니다 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네! 유진님 처럼 저도 조금 더 해결방법을 찾아보겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 우선 useMemo 로 수정하여 커밋을 올려두었었는데, 당장 useMemo 를 쓴다고 y 에서만 렌더링 트리거를 할 수 있는 부분이 아니라고 생각하여 해당 커밋 revert 해두었습니다 :)

이후, 같이 논의해보아요~!

children,
itemHeight,
columnGap = 0,
renderAhead = 5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가적인 돔 요소를 뜻하는 것이었군요..! 감사합니다~

commit={commit}
/>
))}
</VirtualScroll>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Childern prop인 경우엔 직접 컴포넌트를 감싸주는군요..! 설명 감사합니다~

@howyoujini howyoujini merged commit 19a3b2f into dev Nov 20, 2024
@howyoujini howyoujini deleted the virtualScroll branch November 20, 2024 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

디테일 페이지에 Virtual Scroll 적용

3 participants