상세 컨텐츠

본문 제목

React Native 앱 성능 최적화

React Native

by jini_ 2024. 10. 4. 18:53

본문

React Native 앱 성능 최적화

 

앱 개발 과정에서 적용했던 다양한 성능 최적화 방법에 대해 정리해 보겠습니다. 이는 앱의 성능을 극대화하고, 사용자 경험을 향상시키기 위해 필요한 요소들입니다.

 

1. useQuery, useInfiniteQuery 사용

React Query의 useQuery와 useInfiniteQuery를 사용해 비동기 데이터 관리를 최적화했습니다. 이 두 훅을 통해 API 호출 시 발생할 수 있는 중복을 줄이고, 데이터를 효율적으로 캐싱하여 상태 관리를 개선할 수 있었습니다.

특히 queryKey를 사용하여 각 쿼리를 고유하게 식별함으로써 캐싱 효율성을 높였습니다. 이를 통해 이미 요청된 데이터는 빠르게 로드되어 사용자 대기 시간을 최소화할 수 있었습니다. 무한 스크롤을 지원하는 useInfiniteQuery는 페이지네이션이 필요한 데이터에 대해 자동으로 다음 데이터를 로드하는 기능을 제공하여 사용자 경험을 향상시킵니다.

결과적으로, 이 최적화 덕분에 복잡한 상태 관리를 간소화하고 서버 상태와 클라이언트 상태를 일관되게 유지할 수 있었으며, 앱의 전반적인 성능과 사용자 경험이 크게 향상되었습니다.

//Sample code
import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
  const fetchProjects = async ({ pageParam }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  return status === 'pending' ? (
    <p>Loading...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.data.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
              ? 'Load More'
              : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )
}

2. 디바운스 기법을 활용한 사용자 입력 최적화

디바운스란 사용자의 입력이 멈춘 후 일정 시간(delay) 동안 기다렸다가 마지막 입력값만을 처리하는 기법입니다. 사용자 입력이 빈번하게 발생하는 상황에서 불필요한 서버 요청을 방지하기 위해 이 기법을 사용했습니다.

//Sample code
import { useState, useEffect } from 'react';
import { TextInput, View, Text } from 'react-native';

/**
 * 디바운스 훅
 * @param value 디바운스할 값
 * @param delay 디바운스 지연 시간 (밀리초)
 * @returns 디바운스된 값
 */
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      // 입력값이 변경된 후 디바운스 값 설정
      setDebouncedValue(value);
    }, delay);

    // value가 변경되면 기존 타이머를 클리어하고 새로 설정
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// 사용 예시
const SearchComponent = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500); // 500ms 지연

  useEffect(() => {
    if (debouncedSearchTerm) {
      // 디바운스된 검색어를 사용하여 API 호출 등 수행
    }
  }, [debouncedSearchTerm]);

  return (
    <View style={{ padding: 16 }}>
      <TextInput
        style={{
          height: 40,
          borderColor: 'gray',
          borderWidth: 1,
          paddingHorizontal: 8,
        }}
        value={searchTerm}
        onChangeText={setSearchTerm}
        placeholder="검색어를 입력하세요..."
      />
      <Text style={{ marginTop: 16 }}>
        디바운스된 검색어: {debouncedSearchTerm}
      </Text>
    </View>
  );
};

export default SearchComponent;
 

3. 메모이제이션을 통한 상태 관리 최적화: createSelector 사용

createSelector는 Reselect 라이브러리에서 제공하는 함수로, 복잡한 상태를 효율적으로 관리하고 최적화하는 데 도움을 줍니다. 이 함수는 특정 상태를 선택하는 기본 셀렉터를 기반으로 메모이제이션(memoization)된 셀렉터를 생성합니다. 메모이제이션이란, 동일한 입력에 대해 계산된 결과를 캐싱하여 재사용함으로써 불필요한 계산을 방지하는 기법입니다.

상태 관리에서 createSelector를 사용하면 리덕스 스토어의 상태를 기반으로 하는 파생 상태를 효율적으로 생성할 수 있습니다. 예를 들어, 최근 검색어 리스트를 관리할 때, 기본 셀렉터인 selectSearchHistory를 사용하여 검색어 목록을 선택하고, 이 목록을 메모이제이션된 셀렉터인 selectSearchData로 반환합니다. 이 방법을 통해 상태가 변경되지 않는 한 불필요한 리렌더링을 피할 수 있습니다.

//Sample code
import { createSelector } from 'reselect';
import { RootState } from '../store';

// 기본 selector
const selectSearchHistory = (state: RootState) => state.search.searchHistory;

// 메모이제이션된 selector 정의
export const selectSearchData = createSelector(
  [selectSearchHistory],
  (searchHistory) => ({
    searchHistory,
  })
);

4. AsyncStorage 사용

AsyncStorage를 사용하여 사용자의 최근 검색어와 같은 데이터를 로컬에 저장했습니다. 이는 간단한 key-value 저장소로, 앱을 다시 실행할 때 빠르게 데이터를 불러오는 데 도움을 줍니다. 예를 들어, 사용자가 마지막으로 검색한 내용을 기억하여 다음에 앱을 실행했을 때 해당 검색어를 자동으로 입력할 수 있도록 구현할 수 있습니다. 이렇게 함으로써 사용자 경험을 향상시키고, 불필요한 API 호출을 줄여 네트워크 비용을 절감할 수 있습니다.

//Sample code
import AsyncStorage from '@react-native-async-storage/async-storage';

// 저장 함수
const saveSearchTerm = async (term) => {
  try {
    const existingTerms = await AsyncStorage.getItem('recentSearches');
    const updatedTerms = existingTerms ? [...JSON.parse(existingTerms), term] : [term];
    await AsyncStorage.setItem('recentSearches', JSON.stringify(updatedTerms));
  } catch (error) {
    console.error('Error saving search term:', error);
  }
};

// 불러오는 함수
const loadRecentSearches = async () => {
  try {
    const existingTerms = await AsyncStorage.getItem('recentSearches');
    return existingTerms ? JSON.parse(existingTerms) : [];
  } catch (error) {
    console.error('Error loading recent searches:', error);
    return [];
  }
};

5. FlashList 사용

긴 리스트의 성능을 개선하기 위해 FlashList를 사용했습니다. FlashList는 많은 데이터를 효율적으로 렌더링하며, 기본 FlatList에 비해 성능이 뛰어납니다.

//Sample code
import { FlashList } from '@shopify/flash-list';

function MyFlashListComponent({ data }) {
  return (
    <FlashList
      data={data}
      renderItem={({ item }) => <Text>{item.name}</Text>} // 각 아이템을 렌더링하는 부분
      estimatedItemSize={100} // 각 아이템의 예상 크기
      keyExtractor={(item) => item.id.toString()} // 고유 키 추출
    />
  );
}

 

🔎  발생했던 이슈

https://shopify.github.io/flash-list/docs/known-issues/

FlashList는 recyclerlistview라는 라이브러리의 재활용 기능을 활용하여 성능을 최적화합니다. 이때 recyclerlistview의 기본 레이아웃 알고리즘은 유효한 사이즈 없이는 제대로 작동할 수 없습니다. FlashList는 먼저 자신의 크기를 측정한 후, 어느 정도의 아이템을 그릴지와 재사용할지를 결정하는데, 이를 위해 부모 컴포넌트가 적어도 2px 이상의 유효한 크기를 가지고 있어야 합니다.

 

🗝️  해결 방법:

FlashList는 부모의 크기에 맞춰 자동으로 크기가 설정되므로, 부모 컴포넌트가 올바르게 렌더링되어 유효한 크기를 가져야만 제대로 작동합니다. FlashList를 View로 감싸서 적절한 크기를 설정해주었습니다.


6. react-native-fast-image 사용

이미지 로딩 속도를 개선하고 캐싱을 활성화하기 위해 Fast Image를 사용했습니다. Fast Image는 특히 고해상도 이미지를 처리할 때 성능을 향상시킵니다.

//Sample code
import FastImage from 'react-native-fast-image';

const MyImage = () => (
  <FastImage
    style={{ width: 200, height: 200 }}
    source={{
      uri: 'https://example.com/my-image.jpg',
      priority: FastImage.priority.high,
    }}
    resizeMode={FastImage.resizeMode.cover}
  />
);

 

이상으로 앱 성능 최적화에 대해 정리해 보았습니다.

 

참고

Infinite queries | Tanstack Query Guide

FlashList GitHub Repository

react-native-fast-image Repository

'React Native' 카테고리의 다른 글

Expo 주요 개념 정리  (0) 2024.08.09
[React Native] React Native에 대해 알아보기  (0) 2024.06.17

관련글 더보기