[번역] React Native Masters-4: FlatList 성능 최적화

오성준·2024년 5월 26일
4

React Native

목록 보기
7/9

원문: https://medium.com/stackademic/react-native-masters-4-optimizing-flatlist-performance-5204ab6751cf

FlatList는 React Native 프로젝트에서 효율적이고 스크롤 가능한 방식으로 대규모 데이터 세트를 렌더링하는 데 사용하는 고성능 인터페이스입니다.

긴 이야기가 될테니 ☺️, 커피 한 잔 하세요 ☕️

용어

VirtualizedList: FlatList(Virtual List 개념을 구현한 React Native의 컴포넌트)의 기반이 되는 컴포넌트

메모리 사용량: 앱 충돌로 이어질 수도 있는 List에 대한 정보가 메모리에 저장되는 양

응답성: 애플리케이션이 interaction에 반응하는 능력. 예를 들어 낮은 응답성은 컴포넌트를 터치했을 때 예상대로 즉시 응답하지 않고, 조금 기다렸다가 응답하는 것을 말합니다.

빈 영역: VirtualizedList가 항목을 충분히 빠르게 렌더링할 수 없는 경우, 목록의 일부가 렌더링되지 않은 컴포넌트인 빈 영역으로 표시될 수 있습니다.

Viewport: 픽셀로 렌더링되는 콘텐츠의 표시 영역입니다.

Window: 항목이 마운트되어야 하는 영역으로, 일반적으로 Viewport보다 훨씬 큽니다.


1. keyExtractor prop을 사용하여 key 부여하기

지정된 인덱스에서 특정 항목의 고유 key를 추출하는 데 사용됩니다. key는 캐싱에 사용되며 아이템 재배치를 추적하는 react key로 사용됩니다. 기본 extractor는 item.key를 확인한 다음 item.id를 확인한 다음 React처럼 인덱스를 사용하는 것으로 돌아갑니다. keyExtractor는 list에 기본 key 속성 대신 React key의 id를 사용하도록 지시합니다.

const renderItem = useCallback(({ item }) => <Item title={item.title} />, []);

<FlatList
  data={listArray}
  keyExtractor={(item, index) => item.id}
  renderItem={renderItem}
/>;

2. inline 익명 함수 사용하지 않기

서로 다른 두 함수 인스턴스는 결코 동일하지 않으므로 inline 함수는 얕은 동일성 비교가 실패하여 항상 리렌더링을 트리거합니다.

나쁜 예시:

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const InlineFunctionFlatList = () => {
  const data = [
    { id: '1', text: 'Item 1' },
    { id: '2', text: 'Item 2' },
    { id: '3', text: 'Item 3' },
    // ... more data
  ];

  return (
    <View>
      <FlatList
        data={data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => alert(`Clicked ${item.text}`)}>
            <Text>{item.text}</Text>
          </TouchableOpacity>
        )}
      />
    </View>
  );
};

export default InlineFunctionFlatList;

좋은 예시:

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = (item, onPress) => (
  <TouchableOpacity onPress={() => onPress(item.text)}>
    <Text>{item.text}</Text>
  </TouchableOpacity>
);

const NoInlineFunctionFlatList = () => {
  const data = [
    { id: '1', text: 'Item 1' },
    { id: '2', text: 'Item 2' },
    { id: '3', text: 'Item 3' },
    // ... more data
  ];

  const handleItemClick = (text) => {
    alert(`Clicked ${text}`);
  };

  return (
    <View>
      <FlatList
        data={data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => renderItem(item, handleItemClick)}
      />
    </View>
  );
};

export default NoInlineFunctionFlatList;

두 번째 예시에서는 FlatList 컴포넌트 외부에 renderItem 함수를 정의했습니다. handleItemClick 함수 역시 FlatList 외부에 정의되어 있습니다. 이는 renderItem 프로퍼티에 inline 함수를 작성하는 것보다 더 나은 방법입니다.

참고: 클래스 컴포넌트(이전 React 및 RN 버전)를 사용하는 경우 renderItem을 render 함수 외부로 이동시키세요.

팁: 함수 컴포넌트를 사용하는 경우(적극 권장) renderItem 함수에 useCallback을 사용하세요.

3. 너무 큰 이미지 사용하지 않기

때때로(특히 저사양 디바이스의 경우) Android는 ListView(항목의 list를 렌더링하기 위해 Android에서 제공하는 기본 솔루션)에서 고해상도 이미지를 처리할 수 없습니다. 해상도를 즉시 줄일 수 있다고 해도 성능 문제가 발생할 수 있습니다. 따라서 sm/md 해상도 이미지를 사용하세요. 최대 720p를 사용할 수 있지만 충분히 작게 유지해야 합니다. 그렇지 않으면 이미지 품질에 영향을 미칩니다. 또한 이미지 fadeDuration을 줄여 빠르게 스크롤하는 동안 이미지 fade 문제를 방지할 수 있습니다.

4. initialNumToRender prop 사용하기

초기 배치에서 렌더링할 항목 수를 결정하려면 initialNumToRender prop을 사용합니다. 화면을 채우기에 충분한 만큼의 수를 설정합니다. scroll-to-top 동작의 성능을 향상시키기 위해 이러한 항목은 window 렌더링의 일부로 마운트 해제되지 않습니다.

5. getItemLayout prop 사용하기

(data, index) => {length: number, offset: number, index:number}

getItemLayout은 항목의 크기(높이 또는 너비)를 미리 알고 있으면 동적 내용 측정을 건너뛸 수 있는 선택적 최적화입니다. 다음과 같이 고정된 크기의 항목이 있는 경우 getItemLayout이 효율적입니다.

import React from "react";
import { View, FlatList, Text, TouchableOpacity } from "react-native";

const renderItem = (item, onPress) => (
  <TouchableOpacity onPress={() => onPress(item.text)}>
    <Text>{item.text}</Text>
  </TouchableOpacity>
);

const List = () => {
  const data = [
    { id: "1", text: "Item 1" },
    { id: "2", text: "Item 2" },
    { id: "3", text: "Item 3" },
    // ... more data
  ];

  const handleItemClick = (text) => {
    alert(`Clicked ${text}`);
  };

  const getItemLayout = (data, index) => ({
    length: 50, // Assuming each item has a height of 50
    offset: 50 * index,
    index,
  });

  return (
    <View>
      <FlatList
        data={data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => renderItem(item, handleItemClick)}
        initialNumToRender={10}
        getItemLayout={getItemLayout}
      />
    </View>
  );
};

export default List;

6. removeClippedSubviews

removeClippedSubviews은 큰 list의 스크롤 성능을 향상시킬 수 있습니다. Android에서는 기본값이 true입니다. 이 프로퍼티는 off-screen view를 제거하여 리소스를 확보합니다. 큰 list에서는 매력적으로 작동할 수 있지만 로드되지 않은 view로 다시 스크롤할 때 지연이 발생할 수 있습니다.

참고: 상황에 따라 버그(콘텐츠 누락)가 있을 수 있습니다. 사용에 따른 책임은 사용자에게 있습니다.

7. maxToRenderPerBatch

각 배치에서 렌더링할 최대 항목 수를 제어하기 위해 maxToRenderPerBatch로 큰 list를 최적화합니다. 이 프로퍼티의 역할은 배치당 렌더링되는 항목 수를 제한하는 것입니다. 특히 큰 list를 처리할 때 유용합니다.

장점: 숫자를 크게 설정하면 스크롤할 때 시각적으로 빈 공간이 줄어듭니다(채우기 비율이 증가합니다).

단점: 배치당 항목이 많으면 자바스크립트 실행 시간이 길어져 프레스와 같은 다른 이벤트 처리가 차단되어 응답성이 저하될 수 있습니다.

내가 제공해야 하는 값을 어떻게 알 수 있나요?

모든 프로젝트와 상황에 보편적이고 적합한 방법은 아니지만, 저는 이 방법을 프로젝트에 적용하고 있습니다. list 항목이 렌더링될 때 viewport 바깥쪽 하단에 2~3개의 항목이 더 있기를 원합니다. 따라서 뷰포트에 10개 항목이 있는 경우 보통 maxToRenderPerBatch={13}을 만들지만, 4개 항목만 있는 경우 maxToRenderPerBatch={6}을 만듭니다. 물론 list에 있는 항목의 성격과 필요에 따라 결정해야 합니다.

✨ 이제 List 컴포넌트가 한 번 더 최적화되었습니다.

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = (item, onPress) => (<TouchableOpacity onPress={() => onPress(item.text)}>
    <Text>{item.text}</Text>
  </TouchableOpacity>
);

const List = () => {
  const data = [
    { id: '1', text: 'Item 1' },
    { id: '2', text: 'Item 2' },
    { id: '3', text: 'Item 3' },
    // ... more data
  ];

  const handleItemClick = (text) => {
    alert(`Clicked ${text}`);
  };
  
    const getItemLayout = (data, index) => ({
    length: 50, // Assuming each item has a height of 50
    offset: 50 * index,
    index,
  });

  return (
    <View>
      <FlatList
        data={data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => renderItem(item, handleItemClick)}
        initialNumToRender={10}
        getItemLayout={getItemLayout}
        removeClippedSubviews={true}
        maxToRenderPerBatch={13}
      />
    </View>
  );
};

export default List;

8. 부드럽고 효율적인 windowSize

여기에 전달된 숫자는 측정 단위로, 1은 viewport 높이에 해당합니다. 기본값은 21(위쪽 viewport 10개, 아래쪽 viewport 10개, 중간 viewport 1개)입니다.

장점: windowSize가 클수록 스크롤하는 동안 빈 공간이 표시될 가능성이 줄어듭니다. 반면 windowSize가 작으면 동시에 마운트되는 항목 수가 줄어들어 메모리를 절약할 수 있습니다.

단점: windowSize가 클수록 메모리를 더 많이 사용합니다. windowSize 작을수록 빈 공간이 표시될 확률이 높아집니다.

9. memo() 사용하기

React.memo()는 컴포넌트에 전달된 prop이 변경될 때만 다시 렌더링되는 메모화된 컴포넌트를 생성합니다. 이 함수를 사용해 FlatList의 컴포넌트를 최적화할 수 있습니다.

import React, { memo, useCallback } from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = memo(({ item, onPress }) => (
  <TouchableOpacity onPress={() => onPress(item.text)}>
    <Text>{item.text}</Text>
  </TouchableOpacity>
));

const List = () => {
  const data = [
    { id: '1', text: 'Item 1' },
    { id: '2', text: 'Item 2' },
    { id: '3', text: 'Item 3' },
    // ... more data
  ];

  const handleItemClick = useCallback((text) => {
    alert(`Clicked ${text}`);
  }, []); // Empty dependency array because handleItemClick doesn't depend on any external variables

  const getItemLayout = (data, index) => ({
    length: 50, // Assuming each item has a height of 50
    offset: 50 * index,
    index,
  });

  return (
    <View>
      <FlatList
        data={data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => renderItem({ item, onPress: handleItemClick })}
        initialNumToRender={10}
        getItemLayout={getItemLayout}
        removeClippedSubviews={true}
        maxToRenderPerBatch={13}
        windowSize={5}
      />
    </View>
  );
};

export default List;

빈 종속성 배열의 useCallback으로 handleItemClick 함수를 래핑했습니다. 이렇게 하면 컴포넌트가 다시 마운트되지 않는 한 해당 참조가 일정하게 유지됩니다. 또한 renderItemmemo로 래핑하면 컴포넌트는 prop(itemonPress)이 변경될 때만 다시 렌더링됩니다. 이 리팩터링은 list 항목의 불필요한 리렌더링을 방지하여 성능을 향상시킵니다.

10. 캐시된 최적화된 이미지 사용

커뮤니티 패키지(예: @DylanVannreact-native-fast-image)를 사용하면 성능이 더 뛰어난 이미지를 얻을 수 있습니다. list의 모든 이미지는 new Image() 인스턴스입니다. loaded 훅에 더 빨리 도달할수록 JavaScript 스레드가 다시 해제되는 속도도 빨라집니다.

profile
React Native 개발자

0개의 댓글

관련 채용 정보