Content-visibility를 활용한 가상 스크롤 적용해보기

조민성·2025년 6월 13일

HTMLCSS

목록 보기
2/2

Playus 서비스를 개발하는 중, 커뮤니티 게시글을 보여주는 과정에서 문제가 발생했다.

데이터 용량이 너무 많다

이전부터 다른 프로젝트에서도 나왔던 QA 사항이었는데, 아이템의 목록을 보여줄 때 아이템의 양이 너무 많은 경우, 혹은 용량이 너무 많은 경우에는 데이터 렌더링에 있어서 너무 오랜 시간이 소모되었다. 당시에는 백엔드에게 Pagination을 통해 자체적으로 데이터를 분할하여 로드할 수 있도록 요청했지만, 당시에는 이미 프로젝트의 진척이 많이 나가버린 상황하다 보니, 백엔드의 수정이 생각보다 복잡해지는 문제가 있었다. 문제 해결을 위해 React에서 자체적으로 Pagination을 적용해 보았지만, 여전히 데이터 로딩 속도에서는 딱히 감소폭을 보여줄 수 없었다.

문제는 이번에도 똑같은 상황이 벌어졌다는 것이었다. 당시의 이슈를 해결하지 못한 채 같은 실수를 반복해버리고 만 것이다... 채팅과 커뮤니티 구현에 있어 무한스크롤을 적용하려다 보니, 계속해서 아이템이 쌓여가며 로딩 시간이 점점 길어져 버리는 이슈가 생겼다. 특히나 우리 서비스의 경우 모바일 웹을 기반으로 서비스할 예정이었기 때문에 이 이슈를 반드시 해결해야만 사용자 경험이 크게 개선될 수 있었다.

아이템의 양이 너무 많다 보니, 페이지 로딩부터가 장시간을 소모해버리는 이슈 발생.


해결책

인터넷을 뒤지다, 가상 스크롤에 대한 글을 읽게 되었다.
가상 스크롤이란, 사용자가 눈으로 보게 되는 데이터만 웹에서 렌더링하고, 나머지 데이터의 경우 화면에 보이기 전까지는 렌더링을 하지 않음으로써 데이터 로딩 효율을 크게 증가시키는 기법이며, 예시를 들자면 인스타그램이나 X같은 대형 커뮤니티 서비스의 게시글 피드 렌더링이 있다.

이 기법을 사용하기 위해서는 React Virtualized, @tanstack/react-virtual 등의 라이브러리를 사용하는 방법도 있지만, 현재 우리 팀의 개발 진척 상황을 고려했을 때, 이미 다른 주변 기능들이 많이 구현된 시점에서 새로운 라이브러리를 도입하는 것은 다소 무리가 있다고 판단했다.

따라서 나는 css에 존재하는 content-visibility 클래스를 사용하기로 결정했다.


Content-visibility 클래스의 사용법

https://developer.mozilla.org/ko/docs/Web/CSS/content-visibility
위 링크의 문서에서 알 수 있듯, content-vibility 클래스는 언제 컨텐트를 렌더링할지를 사용자에게 명시함으로써 불필요한 컨텐트 렌더링을 줄일 수 있다는 장점을 가진다.
구현을 위해서는 단 두 줄의 css 코드를 작성해주면 된다.

content-visibility: auto;
contain-intrinsic-size: auto 200px;

여기서 content-visibility는 요소를 언제 사용자에게 렌더링할지를 명시해주며, auto로 설정해 준다면 자동으로 레이아웃 및 스타일 isolation을 실행해 준다. 하지만 content-visibility만 설정해 주는 경우, 스크롤을 내릴 때 브라우저가 요소의 크기를 몰라서 언제 렌더링해야 할 지를 제대로 이해하지 못할 수 있기 때문에, contain-instrinsic-size라는 '렌더링할 요소의 크기를 명시해주는' 클래스를 사용하여 브라우저로 하여금 렌더링 시점을 알 수 있게 해준다.

PlayUs의 적용에서는, 기존 단순 Pagination에서 무한 스크롤로의 구현 변경을 시도했고, 이 과정에서 가상 스크롤 기능을 추가하여 렌더링 시간을 줄이고자 했다.


수정 결과

Javascript 변경

<ul className={styles.postList}>
                {sortedPosts.length === 0 && !isLoading ? (
                    <li className={styles.listIsLoading}>게시글이 없습니다.</li>
                ) : (
                    sortedPosts.map((post) => (
                        <div
                            key={post.id || post.postId}
                            className={styles.autoScrollWrapper}
                        >
                            <PostListItem
                                title={post.title}
                                date={post.date}
                                writerNickname={post.writerNickname}
                                image={post.image ? `${process.env.REACT_APP_PRESIGNED_URI}/${post.image}` : null}
                                onClick={() => handlePostClick(post.team, post.id || post.postId)}
                            />
                        </div>
                    ))
                )}
                {isLoading && (
                    <>
                        {Array.from({ length: 1 }).map((_, index) => (
                            <li key={`skeleton-${index}`} className={styles.skeletonItem}>
                                <div className={styles.skeletonThumbnail}></div>
                                <div className={styles.skeletonContent}>
                                    <div className={`${styles.skeletonLine} ${styles.long}`}></div>
                                    <div className={`${styles.skeletonLine} ${styles.short}`}></div>
                                </div>
                            </li>
                        ))}
                    </>
                )}
                <div ref={bottomRef} style={{ height: '1px' }} />
            </ul>


css 변경

.autoScrollWrapper{
    content-visibility: auto;
    contain-intrinsic-size: 200px;
}


기존 수백 개씩 렌더링하던 아이템을 한 번의 스크롤당 8개씩 렌더링하도록 수정함으로써, 렌더링 효율이 크게 증가하고 사용자의 경험을 개선시킬 수 있었다..! 단, 특정 게시글 클릭 후 뒤로가기 버튼을 눌렀을 경우, 새롭게 데이터를 불러오기 때문에 기존에 브라우저에서 기억하던 게시글 페이지 위치는 불러오지 못하게 되었다. 이를 위해선 데이터 클릭 시 스크롤 위치와 페이지에 대한 자체 캐싱이 필요하다고 생각된다...

참조 게시글


가상스크롤의 원리 알고 계신가요?
https://velog.io/@k-svelte-master/virtual-scroll-principle

profile
사람도 사랑도 계획적으로

0개의 댓글