본문 바로가기
Project/관리하당

무한스크롤을 통해서 UX 개선하기 / React-Native

by 이의찬 2023. 11. 30.

0.TL;DR

  • 같은 데이터를 상단에는 그래프로, 하단에는 날짜별 카드 형식으로 보여줘야하는 화면이 있음
  • 모든 정보 한번에 로딩 / 페이지네이션 / 무한 스크롤 중 무한 스크롤 방식이 가장 적절해 선택
  • React-Native의 ScrollView를 사용해 무한 스크롤 카드 리스트 구현
  • ScrollView가 하위 요소를 한번에 랜더링하기에 성능 이슈가 발생할 수 있다고 생각, FlatList를 사용하도록 수정

1. Why?

관리하당의 혈당 정보 페이지와 식사 정보 페이지 기획 과정은 다음과 같았다.

 

1. 재사용성을 높이기 위해 같은 카드 형식 styled component를 사용해서 날짜별로 보여주자.

2. 혈당 정보와 식사 정보를 그래프로 시각적으로 알 수 있으면 좋겠다.

 

결론적으로 상단에는 그래프, 하단에는 카드의 형태로 보여주는 것이 되었다.


2. 고민

카드의 역할은 무엇일까?

- (우리 프로덕트에서는) 날짜별 정보를 보여준다.

 

그렇다면 그래프의 역할은?

- 가져온 정보를 시각적으로 가시성 높게 한눈에 보여줌으로서 사용자가 쉽게 정보를 인지하는데 도움을 준다.

 

그렇다. 카드는 날짜별 정보를 하나씩 하나씩 보여주고, 그래프는 정보를 한눈에 보여준다는 점에서 상호 보완관계다.

그런데 만약에 누가 최근 5년간의 혈당 변동폭을 보고 싶다면?

 

  1. 카드로 정보를 싹 가져오자!
    • -> 사용자가 한 5년치 정보 선택해버리면? 스크롤 압박도 어마어마하고 보여주는데도 한참 걸리겠는데?
  2. 그럼 페이지네이션으로 나눠서 보여주면 어떨까?
    • -> 혈당 측정을 식전/식후에만 한다고 해도, 6번이니까 6 * 365 * 5.... 한 페이지에 20개면 500개 정도 있어야할듯?
  3. 그럼 무한 스크롤을 도입해서 그때 그때 필요한 적은 정보만 들고오는건 어때?
    • -> 1년 골랐는데 1달 정보만 그래프로 보여주게 되네?
  4. 일단 정보를 싹 가져와서 React Query를 사용해서 캐싱해두고 스크롤 내리면 보여주는 방식은 어떨까?
    • -> 그래도 최초로 정보 가져오는 시간은 동일할텐데 그게 의미가 있을까?

내가 떠올린 아이디어는 위의 4가지였는데, 모두 다 각자의 단점이 있었다. 일단 1, 4번은 그래프를 한번에 보여줄 수 있다는 장점이 있지만 정보를 가져오는 과정에서, 대단히 불쾌한 경험을 할 수 있을것이기에 배제했다.

 

남은 선택지는 2, 3번인데 어차피 적은 정보량만 보여줄 거라면 페이지네이션보다는 무한 스크롤 방식이 사용자 경험 측면에서 좀 더 나을것이라고 생각했다. 


3. How?

그럼 이제 무한 스크롤을 코드로 구현해보자.

import React, { useState } from 'react';
import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native';

export default function useScroll() {
  const [isTop, setIsTop] = useState(true);

  const handleScroll = (
    event: NativeSyntheticEvent<NativeScrollEvent>,
    isEnd: boolean,
    setPage: React.Dispatch<React.SetStateAction<number>>
  ) => {
    const { nativeEvent } = event;
    const { contentOffset, layoutMeasurement, contentSize } = nativeEvent;
    const isScrolledToTop = contentOffset.y === 0;
    const isScrolledToBottom =
      Math.round(contentOffset.y + layoutMeasurement.height) >=
      Math.round(contentSize.height);
    setIsTop(isScrolledToTop);
    if (isScrolledToBottom && !isEnd) {
      setPage((prevPage) => prevPage + 1);
    }
  };

  return { isTop, handleScroll };
}

구현 방식은 위와 같은데 간단하게 설명하자면

  1. 스크롤이 이동하면 handleScroll 함수가 실행된다.
  2. isScrolledToBottom에서 최하단 여부를 판단한다.
    • 스크롤의 현재 위치(contentOffset.y)와 화면의 높이(layoutMeasurement.height)의 합이 전체의 높이(contentSize.height)와 같거나 크면 최하단이다.
  3. 최하단으로 판단되었다면, page값에 1을 더해서 다음 페이지로 넘겨준다.
  4. Screen에서는 page가 변경되었으니 데이터를 가져오는 get함수를 실행시키고, 기존 값 뒤에 덧붙여준다. 
  const getBloodSugarData = useCallback(async () => {
    const data = await getPeriodBloodSugar({
      nickname,
      startDate,
      endDate,
      page,
    });
    if (data.response.length === 0) {
      setIsEnd(true);
    } else {
      setBloodSugarData((prevData) => [...prevData, ...data.response]);
    }
  }, [nickname, startDate, endDate, page]);

 

결과물(ScrollView)

ScrollView를 이용해서 구현한 이후, ScrollView가 한번에 모든 하위요소를 랜더링 시킨다는 것을 알게되었다. 이로 인한 성능 문제가 발생할 수 있기에 FlatList를 사용하도록 개선하였다.

      <ScrollView onScroll={handleScroll}>
        <DatePickerControllerWrapper>
          <DatePickerController
            startDate={startDate}
            setStartDate={setStartDateSafe}
            endDate={endDate}
            setEndDate={setEndDateSafe}
          />
        </DatePickerControllerWrapper>
        {bloodSugarData.map(
          //생략

기존코드

      <FlatList
        onScroll={(event) => handleScroll(event, isEnd, setPage)}
        data={bloodSugarData}
        ListHeaderComponent={
          <DatePickerControllerWrapper>
            <DatePickerController
              startDate={startDate}
              setStartDate={setStartDateSafe}
              endDate={endDate}
              setEndDate={setEndDateSafe}
            />
          </DatePickerControllerWrapper>
        }
        keyExtractor={(item) => item.time}
        renderItem={({

개선 후 코드

  • onScroll: ScrollView와 동일하게 스크롤 시 호출된다.
  • data: List에 랜더링할 내용물
  • renderItem: data로 넘겨받은 값의 item들을 각각 랜더링 시키는 콜백함수

4. 래퍼런스

https://reactnative.dev/docs/scrollview

 

ScrollView · React Native

Component that wraps platform ScrollView while providing integration with touch locking "responder" system.

reactnative.dev

https://james-sleep.tistory.com/6

 

React Native 무한스크롤

React Native에서 를 이용해 많은 양의 데이터를 표현할때 랜더링 속도가 현저히 느려집니다. 이 문제 해결을 위해 Instagram의 Feed처럼 무한 스크롤을 구현합니다. Example https://github.com/JamesSleep/InfiniteS

james-sleep.tistory.com