[개발] React Native

React Native 비동기 처리 핵심 노하우

브랜든정 2025. 7. 7. 16:23
반응형

React Native 비동기 처리 핵심 3가지 노하우

모던 모바일 애플리케이션 개발에서 비동기 처리는 선택이 아닌 필수입니다. 네트워크 요청, 파일 시스템 접근, 데이터베이스 작업 등 사용자의 인터랙션을 방해하지 않으면서 백그라운드에서 처리해야 할 작업들이 많기 때문입니다. 특히 사용자 경험(UX)을 중시하는 React Native 환경에서는 UI의 반응성을 유지하는 것이 핵심이며, 이를 위해 효율적인 비동기 처리 전략이 더욱 중요해집니다.

하지만 React Native 개발자들은 비동기 작업을 다루면서 다양한 문제에 직면하곤 합니다. 컴포넌트 라이프사이클과의 충돌, 상태(State) 업데이트 타이밍 문제, 복잡한 데이터 페칭 및 캐싱 관리, 로딩 및 에러 상태를 일관되게 처리하는 어려움 등이 대표적입니다. 이러한 문제들을 제대로 해결하지 못하면 애플리케이션의 성능 저하, 예측 불가능한 버그 발생, 그리고 개발 및 유지보수의 복잡성 증가로 이어질 수 있습니다.

이 글에서는 React Native 애플리케이션에서 마주치는 비동기 처리의 복잡성을 효과적으로 관리하기 위한 세 가지 핵심 노하우를 깊이 있게 다룰 것입니다. 첫째, React Hook인 useEffect를 활용하여 컴포넌트 생애주기에 안전하게 비동기 작업을 통합하는 방법, 둘째, React Query(혹은 TanStack Query)와 같은 전문 라이브러리를 사용하여 복잡한 서버 상태를 효율적으로 관리하는 전략, 셋째, 사용자에게 명확한 피드백을 제공하기 위한 로딩 상태 및 에러 처리를 구현하는 실무적인 기법들을 상세히 살펴보겠습니다.

이 글을 통해 React Native 비동기 처리의 본질을 이해하고, 실무에서 직면하는 다양한 상황에 적용할 수 있는 구체적인 해결책과 설계 패턴에 대한 통찰을 얻으시길 바랍니다.


React Native 비동기 처리의 기초 및 중요성

React Native는 JavaScript를 기반으로 하기 때문에, JavaScript의 표준 비동기 처리 메커니즘인 Promise와 async/await 문법을 기본적으로 사용합니다. 이는 개발자에게 친숙하며, 가독성 높은 비동기 코드를 작성할 수 있게 해줍니다.

Promise: 비동기 작업의 최종 성공 또는 실패 및 그 결과를 나타내는 객체입니다. then() 메서드로 성공 콜백을, catch() 메서드로 실패 콜백을 처리합니다.
async/await: Promise를 더욱 쉽게 사용할 수 있게 해주는 문법 설탕(Syntactic Sugar)입니다. async 함수 내에서 await 키워드를 사용하여 Promise가 완료될 때까지 기다렸다가 결과를 얻을 수 있습니다. 마치 동기 코드처럼 비동기 코드를 작성할 수 있어 코드의 흐름을 이해하기 쉽게 만듭니다.

// Promise 예시
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error fetching data:', error));

// async/await 예시
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}
fetchData();

React Native 환경에서 비동기 처리가 특히 중요한 이유는 다음과 같습니다.

  1. UI 반응성 유지: 네트워크 요청이나 대용량 데이터 처리와 같은 시간이 오래 걸리는 작업을 메인 스레드에서 동기적으로 수행하면 UI가 멈추거나 버벅이는 현상이 발생합니다. 비동기 처리를 통해 이러한 작업들을 백그라운드에서 실행하고, 작업이 완료되었을 때만 UI를 업데이트함으로써 부드러운 사용자 경험을 제공할 수 있습니다.
  2. 리소스 효율성: 비동기 작업은 특정 작업이 완료될 때까지 메인 스레드를 차단하지 않으므로, 해당 시간 동안 다른 작업을 수행할 수 있습니다. 이는 시스템 리소스를 더 효율적으로 사용하게 합니다.
  3. 실시간 데이터 처리: 소켓 통신, 스트리밍 등 실시간으로 발생하는 이벤트를 처리하는 데 비동기 모델이 필수적입니다.

하지만 React Native 컴포넌트 기반의 개발 패러다임은 비동기 처리에 새로운 도전 과제를 제시합니다. 컴포넌트가 마운트/언마운트되거나, 상태나 속성 변경에 따라 리렌더링될 때 비동기 작업의 생명주기와 컴포넌트의 생명주기를 동기화하는 것이 중요합니다. 완료되지 않은 비동기 작업이 언마운트된 컴포넌트의 상태를 업데이트하려고 시도하면 메모리 누수 경고가 발생하거나 예기치 않은 동작을 유발할 수 있습니다. 또한, 동일한 데이터를 여러 컴포넌트에서 필요로 하거나, 데이터 변경 시 자동으로 UI를 업데이트해야 하는 등 데이터 관리의 복잡성이 증가합니다.

이러한 문제들을 해결하기 위한 첫 번째 단계는 useEffect를 올바르게 사용하여 컴포넌트 라이프사이클 내에서 비동기 작업을 안전하게 수행하는 것입니다.

첫 번째 노하우: useEffect를 활용한 컴포넌트 생애주기 내 비동기 작업 처리

React Hook 중 useEffect는 함수 컴포넌트 내에서 부수 효과(Side Effects)를 수행하기 위해 사용됩니다. 데이터 페칭, 구독 설정, DOM 직접 조작 등 컴포넌트 렌더링 결과에 영향을 주지 않지만 컴포넌트의 기능에 필요한 작업들이 부수 효과에 해당합니다. 비동기 데이터 페칭은 useEffect의 가장 흔한 사용 사례 중 하나입니다.

useEffect 내에서 비동기 함수 호출하기

useEffect의 콜백 함수 자체는 async로 선언될 수 없습니다. useEffect의 콜백 함수는 동기적으로 동작하며, 정리(cleanup) 함수를 반환해야 하는데, async 함수는 암묵적으로 Promise를 반환하기 때문입니다. 따라서 useEffect 내에서 비동기 작업을 수행하려면 다음과 같은 패턴을 사용해야 합니다.

  1. useEffect 내부에 비동기 함수 정의 및 즉시 호출: 가장 일반적이고 권장되는 패턴입니다.
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';

function MyComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true; // 마운트 상태 추적을 위한 플래그

    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        if (isMounted) { // 컴포넌트가 마운트된 상태일 때만 상태 업데이트
          setData(result);
          setLoading(false);
        }
      } catch (e) {
        if (isMounted) { // 컴포넌트가 마운트된 상태일 때만 상태 업데이트
          setError(e);
          setLoading(false);
        }
      }
    };

    fetchData(); // 비동기 함수 호출

    return () => {
      isMounted = false; // 언마운트 시 플래그 변경
    };
  }, []); // 빈 의존성 배열: 컴포넌트 마운트 시 한 번만 실행

위 예제에서 fetchData라는 async 함수를 useEffect 콜백 내부에 정의하고 즉시 호출했습니다. 여기서 중요한 점은 isMounted 플래그를 사용하여 비동기 작업이 완료되었을 때 컴포넌트가 여전히 마운트 상태인지 확인하는 것입니다. 비동기 작업이 진행되는 동안 컴포넌트가 언마운트되면, 작업 완료 후 상태 업데이트 시도 시 "Can't perform a React state update on an unmounted component"와 같은 경고가 발생할 수 있습니다. isMounted 패턴은 이러한 경고를 방지하고 잠재적인 메모리 누수를 막는 데 도움이 됩니다.

  1. 외부에 정의된 비동기 함수 호출: 비동기 로직이 복잡하거나 재사용이 필요하면 useEffect 외부에서 비동기 함수를 정의하고 useEffect 내에서 호출할 수 있습니다.
// 외부에서 정의된 async 함수
async function fetchDataFromApi(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return await response.json();
}

function MyComponent({ apiUrl }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    setLoading(true);
    setError(null); // 새로운 페칭 시작 전 에러 초기화

    fetchDataFromApi(apiUrl)
      .then(result => {
        if (isMounted) {
          setData(result);
        }
      })
      .catch(e => {
        if (isMounted) {
          setError(e);
        }
      })
      .finally(() => {
        if (isMounted) {
          setLoading(false);
        }
      });

    return () => {
      isMounted = false;
    };
  }, [apiUrl]); // apiUrl이 변경될 때마다 다시 실행

  // ... 컴포넌트 렌더링 로직 (로딩, 에러, 데이터 표시)
}

이 패턴에서도 isMounted 플래그는 중요하며, 의존성 배열에 apiUrl을 추가하여 apiUrl이 변경될 때마다 데이터를 다시 페칭하도록 설정했습니다.

useEffect 정리(Cleanup) 함수 활용

useEffect에서 반환하는 함수는 "정리(cleanup)" 함수입니다. 이 함수는 컴포넌트가 언마운트될 때, 또는 의존성 배열의 값이 변경되어 이펙트가 다시 실행되기 직전에 호출됩니다. 비동기 작업의 경우, 이 정리 함수를 사용하여 진행 중인 비동기 작업을 취소하거나 리소스를 정리하는 데 사용할 수 있습니다.

네트워크 요청 취소는 정리 함수의 중요한 활용 사례입니다. 사용자가 페이지를 빠르게 이동하거나 컴포넌트가 빠르게 마운트/언마운트될 때, 이전 비동기 요청이 완료되기 전에 새로운 요청이 시작되거나 컴포넌트가 사라질 수 있습니다. 이 경우 이전 요청을 취소하지 않으면 불필요한 리소스가 소비되거나, 앞서 언급한 "unmounted component state update" 경고가 발생할 수 있습니다.

Modern Fetch API는 AbortController를 통해 요청 취소 기능을 제공합니다.

import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';

function MyCancellableComponent({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController(); // AbortController 생성
    const signal = controller.signal; // 신호 가져오기

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch(`https://api.example.com/users/${userId}`, { signal }); // signal 옵션 전달

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);

      } catch (e) {
        // AbortError는 무시 (요청 취소는 에러가 아님)
        if (e.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          setError(e);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 정리 함수: 컴포넌트 언마운트 또는 userId 변경 시 호출
    return () => {
      controller.abort(); // 진행 중인 요청 취소
      console.log('Cleanup: Request aborted');
    };
  }, [userId]); // userId가 변경될 때마다 실행

  // ... 컴포넌트 렌더링 (로딩, 에러, 데이터)
}

이 패턴은 isMounted 플래그를 사용하는 것보다 더 강력합니다. isMounted는 상태 업데이트 시점의 컴포넌트 마운트 상태만 확인하지만, AbortController는 실제로 진행 중인 비동기 작업(Fetch 요청) 자체를 중단시키기 때문입니다. 이는 리소스 낭비를 줄이고 레이스 컨디션(Race Condition - 여러 비동기 작업이 동시에 실행되어 예측 불가능한 결과를 초래하는 상황) 문제를 완화하는 데 도움이 됩니다. 예를 들어, userId가 빠르게 변경될 때, 이전 userId에 대한 요청이 완료되기 전에 새로운 userId에 대한 요청이 시작될 수 있습니다. 이때 이전 요청을 취소하지 않으면 나중에 완료된 요청의 데이터가 화면에 잘못 표시될 수 있습니다. AbortController를 사용하면 이러한 레이스 컨디션을 방지할 수 있습니다.

useEffect를 사용한 비동기 처리는 컴포넌트 수준의 데이터 페칭이나 간단한 부수 효과에 적합합니다. 하지만 애플리케이션 전반에 걸쳐 서버 상태를 관리해야 하거나, 복잡한 캐싱, 백그라운드 업데이트, 데이터 동기화 등의 요구사항이 있다면 useEffect만으로는 한계가 있습니다. 이럴 때 React Query와 같은 전문 라이브러리가 빛을 발합니다.

두 번째 노하우: React Query를 통한 선언적 서버 상태 관리

모던 웹/앱 개발에서 서버 상태(Server State) 관리는 클라이언트 상태(Client State, 예: UI 토글 상태, 입력 폼 값) 관리와는 다른 복잡성을 가집니다. 서버 상태는 애플리케이션 외부(API 서버)에 존재하며, 비동기적으로 페칭되고, 여러 클라이언트에서 공유될 수 있으며, 시간이 지남에 따라 변경될 수 있습니다.

useEffect를 사용하여 서버 데이터를 페칭하는 방식은 다음과 같은 문제점을 야기할 수 있습니다.

  • 수동적인 캐싱 관리: 한 번 가져온 데이터를 다른 컴포넌트에서도 사용하려면 직접 캐싱 로직을 구현해야 합니다.
  • 수동적인 백그라운드 업데이트: 데이터가 최신 상태인지 확인하고 필요시 다시 페칭하는 로직을 수동으로 작성해야 합니다 (예: 앱이 포그라운드로 돌아올 때).
  • 복잡한 동기화: 같은 데이터를 여러 곳에서 페칭할 때 발생할 수 있는 비일관성을 관리하기 어렵습니다.
  • 반복적인 로딩/에러 처리 코드: 각 데이터 페칭 로직마다 로딩, 성공, 에러 상태를 관리하는 코드를 반복적으로 작성해야 합니다.
  • 레이스 컨디션 및 요청 중복: 빠르게 발생하는 여러 요청을 효율적으로 관리하고 중복 요청을 방지하는 것이 어렵습니다.

React Query (최근에는 TanStack Query로 브랜드를 변경했습니다)는 이러한 서버 상태 관리의 어려움을 해결하기 위해 설계된 라이브러리입니다. React 애플리케이션에서 서버 상태를 "선언적(Declarative)"으로 관리할 수 있게 해줍니다. 즉, "어떤 데이터를 어디서 가져올 것인가"만 선언하면, 데이터 페칭, 캐싱, 동기화, 백그라운드 업데이트, 로딩/에러 상태 추적 등 복잡한 로직을 React Query가 대신 처리해 줍니다.

React Query의 핵심 개념 및 이점

  • 쿼리 (Queries): 서버에서 데이터를 가져오는 (GET 요청 등) 비동기 작업에 사용됩니다. useQuery 훅을 사용합니다.
  • 뮤테이션 (Mutations): 서버의 데이터를 수정하는 (POST, PUT, DELETE 요청 등) 비동기 작업에 사용됩니다. useMutation 훅을 사용합니다.
  • 캐싱 (Caching): 페칭된 데이터를 자동으로 캐싱하고 관리합니다. 이를 통해 동일한 데이터를 여러 번 요청하거나 다른 컴포넌트에서 재사용할 때 네트워크 요청을 줄이고 성능을 향상시킵니다. "Stale-while-revalidate" 전략을 기본으로 사용하여 사용자에게 즉각적으로 캐시된 데이터를 보여주면서 백그라운드에서 최신 데이터를 확인하고 필요한 경우 업데이트합니다.
  • 백그라운드 업데이트: 포커스 재획득 시, 네트워크 재연결 시 등 특정 조건에서 자동으로 데이터를 백그라운드에서 다시 페칭하여 최신 상태를 유지합니다.
  • 로딩/에러 상태 추적: 쿼리 및 뮤테이션의 현재 상태(로딩 중, 성공, 에러)를 추적하여 UI에서 쉽게 반영할 수 있도록 합니다.
  • 개발자 도구 (Devtools): 캐시 상태, 쿼리 실행 내역 등을 시각적으로 확인할 수 있는 강력한 개발자 도구를 제공하여 디버깅을 용이하게 합니다.

useQuery를 사용한 데이터 페칭 예제

React Query를 사용하려면 먼저 애플리케이션 루트 근처에 QueryClientProvider를 설정해야 합니다.

// App.js 또는 index.js
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query'; // React Native에서는 'react-query/native' 또는 'react-query' 사용

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* 애플리케이션의 나머지 컴포넌트 */}
      <MyComponent />
    </QueryClientProvider>
  );
}

export default App;

이제 컴포넌트에서 useQuery 훅을 사용하여 데이터를 페칭할 수 있습니다.

import React from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { useQuery } from 'react-query'; // React Native에서는 'react-query/native' 사용이 권장될 수 있습니다.

const fetchUserData = async (userId) => {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

function UserProfile({ userId }) {
  // useQuery 훅 사용:
  // 첫 번째 인자: 쿼리 키 (데이터를 식별하는 고유한 키 배열)
  // 두 번째 인자: 데이터를 페칭하는 비동기 함수 (쿼리 함수)
  const { data, isLoading, isError, error, refetch } = useQuery(['user', userId], () => fetchUserData(userId));

  if (isLoading) {
    return <ActivityIndicator size="large" color="#0000ff" />;
  }

  if (isError) {
    return <Text>Error fetching user data: {error.message}</Text>;
  }

  // 데이터가 성공적으로 로드되었을 때
  return (
    <View>
      <Text>User Name: {data.name}</Text>
      <Text>User Email: {data.email}</Text>
      {/* 필요에 따라 데이터 새로고침 버튼 */}
      <Button title="Refresh" onPress={() => refetch()} />
    </View>
  );
}

useQuery는 데이터(data), 로딩 상태(isLoading), 에러 상태(isError, error), 그리고 수동으로 데이터를 다시 가져올 수 있는 refetch 함수 등을 반환합니다. 개발자는 이 상태 값들을 활용하여 UI를 선언적으로 렌더링하기만 하면 됩니다. userId가 변경되면 React Query는 자동으로 해당 쿼리를 다시 실행하여 최신 데이터를 가져옵니다. 동일한 ['user', userId] 쿼리 키를 사용하는 다른 컴포넌트가 있다면, React Query는 캐시된 데이터를 제공하거나 필요시 한 번의 네트워크 요청으로 모든 곳을 업데이트합니다.

useMutation을 사용한 데이터 수정 예제

데이터를 생성, 수정, 삭제하는 작업은 useMutation 훅을 사용합니다.

import React, { useState } from 'react';
import { View, Text, TextInput, Button, ActivityIndicator, Alert } from 'react-native';
import { useMutation, useQueryClient } from 'react-query';

const updateUserData = async (userData) => {
  const response = await fetch(`https://api.example.com/users/${userData.id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });
  if (!response.ok) {
    throw new Error('Failed to update user data');
  }
  return response.json();
};

function EditUserProfile({ user }) {
  const queryClient = useQueryClient(); // 캐시를 관리하는 QueryClient 인스턴스 가져오기
  const [name, setName] = useState(user.name);
  const [email, setEmail] = useState(user.email);

  // useMutation 훅 사용
  const mutation = useMutation(updateUserData, {
    onSuccess: (updatedUser) => {
      // 뮤테이션 성공 시 캐시 업데이트 또는 무효화
      // 방법 1: 특정 쿼리 키를 무효화하여 데이터 자동 리페치 (가장 일반적)
      queryClient.invalidateQueries(['user', user.id]);
      Alert.alert('Success', 'User updated successfully!');
    // 방법 2: 캐시된 데이터를 직접 업데이트 (optimistic updates 등에 사용)
    // queryClient.setQueryData(['user', user.id], updatedUser);
    },
    onError: (error) => {
      Alert.alert('Error', `Failed to update user: ${error.message}`);
    },
  });

  const handleSave = () => {
    mutation.mutate({ id: user.id, name, email }); // 뮤테이션 실행
  };

  return (
    <View>
      <Text>Name:</Text>
      <TextInput value={name} onChangeText={setName} />
      <Text>Email:</Text>
      <TextInput value={email} onChangeText={setEmail} />

      <Button
        title={mutation.isLoading ? 'Saving...' : 'Save Profile'}
        onPress={handleSave}
        disabled={mutation.isLoading} // 저장 중에는 버튼 비활성화
      />

      {mutation.isError && <Text style={{ color: 'red' }}>Error: {mutation.error.message}</Text>}
    </View>
  );
}

useMutationmutate 함수를 제공하여 뮤테이션을 실행하고, isLoading, isError, error 상태를 추적합니다. 뮤테이션이 성공하면 onSuccess 콜백이 실행되며, 여기서 queryClient.invalidateQueries 등을 사용하여 관련 쿼리의 캐시를 무효화하여 데이터 일관성을 유지합니다. queryClient.invalidateQueries(['user', user.id]) 호출은 ['user', user.id] 키를 가진 쿼리를 "stale" 상태로 만들고, 해당 쿼리를 사용하는 컴포넌트가 화면에 마운트되어 있다면 백그라운드에서 자동으로 데이터를 다시 가져오게 합니다.

React Query의 장단점 및 적용 시점

장점:

  • 서버 상태 관리의 복잡성을 크게 줄여줍니다.
  • 데이터 페칭, 캐싱, 백그라운드 업데이트 등 일반적인 데이터 관리 패턴을 내장하고 있습니다.
  • 로딩, 에러, 성공 상태 관리를 간소화합니다.
  • 코드의 중복을 줄이고 가독성을 높입니다.
  • 성능 최적화 (캐싱, 중복 요청 방지)를 자동으로 처리합니다.
  • 개발자 도구를 통해 데이터 상태를 쉽게 파악하고 디버깅할 수 있습니다.

단점:

  • 새로운 라이브러리를 학습해야 하는 초기 비용이 발생합니다.
  • 간단한 로컬 비동기 작업에는 오버헤드가 될 수 있습니다. 서버 상태 관리 목적에 더 적합합니다.

적용 시점:

  • 애플리케이션에서 서버로부터 데이터를 자주 페칭하고 수정하는 경우.
  • 다양한 컴포넌트에서 동일한 데이터를 공유해야 하는 경우.
  • 데이터 캐싱, 백그라운드 업데이트, 오프라인 지원 등의 기능이 필요한 경우.
  • 데이터 페칭 로직과 UI 로직을 명확하게 분리하고 싶은 경우.

React Query는 복잡한 데이터 중심 애플리케이션에서 생산성과 성능을 크게 향상시킬 수 있는 강력한 도구입니다. 서버 상태 관리가 애플리케이션의 주요 복잡성 중 하나라면 React Query 도입을 적극적으로 고려해 볼 만합니다.

세 번째 노하우: 로딩 상태 및 에러 처리의 실무 기법

효과적인 비동기 처리 전략은 단순히 데이터를 가져오는 것을 넘어, 사용자에게 현재 애플리케이션 상태에 대한 명확한 피드백을 제공하고 발생 가능한 문제를 우아하게 처리하는 것을 포함합니다. 로딩 상태와 에러 처리는 사용자 경험(UX)의 핵심 요소이며, 이들을 제대로 구현하는 것은 안정적이고 신뢰할 수 있는 애플리케이션을 만드는 데 필수적입니다.

로딩 상태 처리

비동기 작업이 진행되는 동안 사용자에게 로딩 중임을 알리는 것은 매우 중요합니다. 이는 애플리케이션이 멈춘 것이 아니며 작업이 진행 중임을 시각적으로 보여줌으로써 사용자의 답답함을 줄이고 이탈을 방지합니다.

로딩 상태는 다음과 같은 방법으로 관리할 수 있습니다.

  1. 수동 상태 관리 (useState + useEffect): 비동기 함수 호출 전에 로딩 상태를 true로 설정하고, 작업 완료(then) 또는 에러 발생(catch) 후 finally 블록에서 false로 설정하는 방식입니다.
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator, Button } from 'react-native';

function ManualLoadingExample() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false); // 초기 상태는 false
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setLoading(true); // 요청 시작 전 로딩 상태 설정
    setError(null); // 이전 에러 초기화
    try {
      const response = await fetch('https://api.example.com/data');
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
    } catch (e) {
      setError(e);
    } finally {
      setLoading(false); // 요청 완료 또는 에러 발생 후 로딩 상태 해제
    }
  };

  // 컴포넌트 마운트 시 데이터 페칭
  useEffect(() => {
    fetchData();
  }, []);

  return (
    <View>
      {loading ? ( // 로딩 중일 때 ActivityIndicator 표시
        <ActivityIndicator size="large" color="#0000ff" />
      ) : error ? ( // 에러 발생 시 에러 메시지 표시
        <Text>Error: {error.message}</Text>
      ) : ( // 데이터 로드 성공 시 데이터 표시
        <Text>Data loaded: {JSON.stringify(data)}</Text>
      )}
      <Button title="Refetch" onPress={fetchData} disabled={loading} /> {/* 로딩 중에는 버튼 비활성화 */}
    </View>
  );
}

이 방식은 간단한 컴포넌트에서는 효과적이지만, 여러 비동기 작업이 있거나 애플리케이션 전반에서 로딩 상태를 관리해야 할 경우 코드가 복잡해지고 중복될 수 있습니다.

  1. 라이브러리 활용 (React Query 등): React Query의 useQueryuseMutation 훅은 isLoading, isFetching 등의 상태를 기본으로 제공합니다. 이를 활용하면 로딩 상태 관리가 매우 선언적이 되고 간편해집니다.
import React from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { useQuery } from 'react-query';

// ... fetchUserData 함수 정의 ...

function ReactQueryLoadingExample({ userId }) {
  const { data, isLoading, isError, error, isFetching } = useQuery(['user', userId], () => fetchUserData(userId));

  // isLoading: 쿼리가 처음 실행되어 데이터를 가져오는 중
  // isFetching: 쿼리가 백그라운드에서 데이터를 다시 가져오는 중 (초기 로딩 포함)

  if (isLoading) {
    // 초기 로딩 시 전체 화면 로딩 표시
    return <ActivityIndicator size="large" color="#0000ff" />;
  }

  if (isError) {
    // 에러 발생 시 에러 메시지 표시
    return <Text>Error fetching user data: {error.message}</Text>;
  }

  // 데이터 로드 성공 시 데이터 표시
  return (
    <View>
      {/* isFetching을 사용하여 백그라운드 업데이트 중임을 미세하게 표시 */}
      <Text>{isFetching ? 'Updating...' : 'User Data:'}</Text>
      <Text>Name: {data.name}</Text>
      <Text>Email: {data.email}</Text>
    </View>
  );
}

isLoading은 쿼리가 데이터를 처음 가져올 때 true이고, isFetching은 초기 로딩과 이후 백그라운드에서 데이터를 다시 가져올 때 모두 true입니다. 이 두 상태를 적절히 조합하여 다양한 로딩 UI를 구현할 수 있습니다. 예를 들어, isLoading일 때는 스켈레톤 UI나 전체 화면 로딩 스피너를 보여주고, isFetching && !isLoading일 때는 데이터는 그대로 유지한 채 작은 스피너나 애니메이션으로 백그라운드 업데이트 중임을 표시할 수 있습니다.

로딩 인디케이터 종류

  • ActivityIndicator: React Native에서 제공하는 기본 로딩 스피너입니다. 간단한 로딩 표시에 적합합니다.
  • Skeleton UI: 데이터 구조는 미리 보여주되, 내용만 비워놓고 애니메이션 효과를 주는 방식입니다. 사용자는 어떤 데이터가 로드될지 미리 파악할 수 있어 응답성이 좋아 보입니다. react-native-skeleton-placeholder 등의 라이브러리를 사용할 수 있습니다.
  • Progress Bar: 파일 다운로드나 업로드처럼 진행률을 알 수 있는 작업에 적합합니다.
  • Custom Loading Components: 브랜드 아이덴티티에 맞는 커스텀 애니메이션이나 로고를 활용한 로딩 화면을 구현할 수 있습니다.

에러 처리

비동기 작업 실패는 언제든지 발생할 수 있으며, 이를 제대로 처리하지 않으면 애플리케이션이 중단되거나 사용자에게 혼란을 줄 수 있습니다. 사용자에게 문제가 발생했음을 명확히 알리고, 가능하면 해결책(예: 다시 시도 버튼)을 제시하는 것이 좋은 에러 처리 전략입니다.

  1. try...catch 블록 사용: 가장 기본적인 JavaScript 에러 처리 방식입니다. 비동기 함수 내에서 발생한 에러를 잡아낼 수 있습니다.
async function fetchDataWithErrorHandling() {
  try {
    const response = await fetch('https://api.example.com/nonexistent');
    if (!response.ok) {
      // HTTP 에러 (404, 500 등) 처리
      const errorBody = await response.text(); // 에러 응답 본문 확인
      throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
    }
    const data = await response.json();
    console.log('Data:', data);
  } catch (e) {
    // 네트워크 에러, JSON 파싱 에러, throw된 에러 등 처리
    console.error('Fetch error:', e.message);
    // 사용자에게 에러 메시지 표시 로직 호출 (예: Alert.alert)
    Alert.alert('데이터 로드 실패', `에러: ${e.message}`);
  }
}

fetchDataWithErrorHandling();

fetch API는 네트워크 연결 문제와 같은 경우에만 catch 블록으로 에러를 던집니다. 404, 500과 같은 HTTP 에러는 response.ok 속성을 확인하여 수동으로 처리해야 합니다.

  1. 라이브러리 활용 (React Query 등): React Query의 useQueryuseMutation 훅은 isError 상태와 error 객체를 제공합니다.
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useQuery } from 'react-query';

// ... fetchUserData 함수 정의 ...

function ReactQueryErrorExample({ userId }) {
  const { data, isLoading, isError, error, refetch } = useQuery(['user', userId], () => fetchUserData(userId));

  if (isLoading) {
    return <Text>Loading...</Text>;
  }

  if (isError) {
    // 에러 발생 시 에러 메시지 및 다시 시도 버튼 표시
    return (
      <View>
        <Text style={{ color: 'red' }}>Error fetching user data: {error.message}</Text>
        <Button title="Retry" onPress={() => refetch()} /> {/* 에러 발생 시 다시 시도 버튼 제공 */}
      </View>
    );
  }

  // 데이터 로드 성공 시
  return (
    <View>
      <Text>User Data:</Text>
      <Text>Name: {data.name}</Text>
      <Text>Email: {data.email}</Text>
    </View>
  );
}

React Query는 기본적으로 실패한 쿼리를 여러 번 자동으로 재시도하는 기능(retry 옵션으로 제어)을 가지고 있어, 네트워크 불안정 등으로 인한 일시적인 에러 처리를 별도 구현 없이도 수행할 수 있습니다. useQuery에서 제공하는 refetch 함수를 에러 UI와 함께 제공하여 사용자가 수동으로 다시 시도할 수 있도록 유도하는 것도 좋은 UX 패턴입니다.

에러 표시 및 알림 방법

  • 인라인 메시지: 컴포넌트 내에서 에러 메시지를 직접 표시합니다. 간단한 폼 유효성 검사 에러나 특정 데이터 페칭 에러에 적합합니다.
  • Alert/Modal: 중요한 에러나 작업 실패를 사용자에게 강제로 인지시켜야 할 때 사용합니다. (예: 로그인 실패) React Native의 Alert API나 커스텀 모달 컴포넌트를 사용할 수 있습니다.
  • Toast/Snackbar: 화면 하단이나 상단에 짧게 나타났다 사라지는 알림입니다. 사용자 흐름을 방해하지 않으면서 에러나 작업 결과를 알리는 데 사용됩니다. @react-native-community/datetimepicker 와 같은 라이브러리가 토스트 메시지 기능을 제공하기도 하며, 커스텀 구현도 가능합니다.
  • Error Boundaries: React의 Error Boundary 컴포넌트는 자식 컴포넌트 트리에서 발생한 JavaScript 에러를 잡아내어 대체 UI를 렌더링할 수 있습니다. 이는 렌더링 과정에서 발생하는 예기치 않은 에러로 인해 전체 앱이 중단되는 것을 방지하는 데 유용합니다. 비동기 에러(Promise rejection)는 Error Boundary가 기본적으로 잡아내지 못하므로, 이를 처리하기 위해 비동기 코드 내에서 명시적으로 에러를 다시 던지거나 상태를 업데이트해야 합니다.
  • 중앙 집중식 로깅: Sentry, AppSignal 등과 같은 에러 로깅 서비스를 연동하여 프로덕션 환경에서 발생하는 에러를 수집하고 분석하는 것은 문제 해결 및 애플리케이션 안정성 확보에 필수적입니다. catch 블록이나 글로벌 에러 핸들러에서 이러한 서비스로 에러를 전송하도록 구현합니다.

종합적인 에러 처리 전략

  1. 에러 종류 분류: 네트워크 에러, 서버 에러(HTTP 상태 코드), 클라이언트 측 로직 에러, 사용자 입력 에러 등을 구분하고 각기 다른 방식으로 처리할 계획을 세웁니다.
  2. 사용자에게 유용한 메시지 제공: 기술적인 에러 메시지 대신, 사용자에게 어떤 문제가 발생했으며 어떻게 해결할 수 있는지(예: "네트워크 연결을 확인하고 다시 시도해주세요.", "잠시 후 다시 시도해주세요.")를 알려주는 메시지를 제공합니다.
  3. 가능한 경우 복구 옵션 제공: "다시 시도" 버튼이나 이전 페이지로 돌아가는 옵션을 제공합니다.
  4. 예외 상황 방지: 가능한 한 비동기 작업 전에 필수 데이터가 존재하는지, 네트워크 연결이 가능한지 등을 미리 확인하여 예상 가능한 에러를 사전에 방지합니다.
  5. 전역 에러 핸들링: 예상치 못한 에러가 발생했을 때 애플리케이션이 완전히 중단되지 않도록 최상위 레벨에서 에러를 처리하고, 사용자에게 불편을 최소화하는 fallback UI를 보여줍니다. ErrorUtils.setGlobalHandler를 사용하여 잡히지 않은 에러를 처리할 수 있습니다.
  6. 에러 로깅: 프로덕션 환경에서는 발생한 에러를 기록하고 분석하여 근본적인 문제를 해결할 수 있도록 시스템을 구축합니다.

로딩 상태와 에러 처리는 개발 과정에서 부가적인 작업처럼 느껴질 수 있지만, 사용자 경험과 애플리케이션 신뢰성에 직접적인 영향을 미치는 매우 중요한 부분입니다. 비동기 작업을 구현할 때 이 두 가지를 항상 염두에 두고 설계하고 구현해야 합니다. React Query와 같은 라이브러리는 이러한 작업을 표준화되고 효율적인 방식으로 수행하도록 돕는 강력한 도구입니다.


종합적인 고려사항 및 아키텍처 패턴

지금까지 살펴본 세 가지 노하우를 효과적으로 조합하고 적용하기 위해서는 몇 가지 종합적인 고려사항과 아키텍처 패턴을 이해하는 것이 중요합니다.

useEffect와 React Query의 선택 기준

  • useEffect: 컴포넌트의 로컬 상태나 속성에 기반한 간단한 비동기 작업, 서버 상태와 관련 없는 부수 효과(예: 애니메이션 시작, 이벤트 리스너 등록/해제, 타이머 설정/해제, 로컬 스토리지 접근)에 적합합니다. 데이터 페칭의 경우, 해당 데이터가 해당 컴포넌트 내에서만 사용되고, 캐싱이나 전역적인 상태 관리가 필요 없는 간단한 경우에 사용할 수 있습니다. 하지만 복잡한 데이터 요구사항이나 앱 전반에 걸친 데이터 공유가 필요하다면 React Query를 고려해야 합니다.
  • React Query: 서버 상태 관리(데이터 페칭, 수정, 삭제)에 특화되어 있습니다. 동일한 데이터를 여러 컴포넌트에서 사용하거나, 복잡한 캐싱, 백그라운드 업데이트, 데이터 동기화, 오프라인 지원 등이 필요한 애플리케이션에 매우 적합합니다. useEffect로 서버 데이터를 페칭하는 코드가 반복적으로 나타나거나 복잡해지기 시작한다면, React Query로 전환하는 것을 고려해야 할 시점입니다.

관심사의 분리 (Separation of Concerns)

깔끔하고 유지보수 가능한 코드를 위해 데이터 페칭 로직과 UI 렌더링 로직을 분리하는 것이 좋습니다.

  • Custom Hooks: useQueryfetch 호출과 로딩/에러/데이터 상태 관리 로직을 커스텀 훅(useUserData, useUpdateProfile 등)으로 추상화할 수 있습니다. 이렇게 하면 UI 컴포넌트는 해당 훅을 호출하고 반환된 상태 값에 따라 렌더링만 하면 되므로 코드가 훨씬 간결해지고 재사용성이 높아집니다. React Query는 기본적으로 이러한 커스텀 훅 패턴을 장려합니다.
  • Service Layer: API 호출 로직 자체를 별도의 서비스 파일(예: api.js, userService.js)에 분리하여 관리할 수 있습니다. fetch 호출, URL 구성, 헤더 설정, 응답 검증 등의 저수준 로직을 이 레이어에 두고, 커스텀 훅이나 컴포넌트에서는 이 서비스 함수의 결과를 사용합니다.
// apiService.js
const BASE_URL = 'https://api.example.com';

export const fetchData = async (endpoint, options = {}) => {
  const response = await fetch(`${BASE_URL}/${endpoint}`, options);
  if (!response.ok) {
    const error = new Error(`HTTP error! status: ${response.status}`);
    error.response = response; // 필요하다면 응답 객체를 에러에 포함
    throw error;
  }
  return response.json();
};

// useUserData.js (Custom Hook)
import { useQuery } from 'react-query';
import { fetchData } from './apiService';

export const useUserData = (userId) => {
  return useQuery(['user', userId], () => fetchData(`users/${userId}`));
};

// UserProfile.js (UI Component)
import React from 'react';
import { View, Text, ActivityIndicator, Button } from 'react-native';
import { useUserData } from './useUserData';

function UserProfile({ userId }) {
  const { data, isLoading, isError, error, refetch } = useUserData(userId);

  // ... 렌더링 로직 ...
}

이러한 구조는 코드의 응집도를 높이고 결합도를 낮추어 테스트, 수정, 확장을 용이하게 만듭니다.

비동기 코드 테스트

비동기 로직이 포함된 컴포넌트나 훅을 테스트하는 것은 동기 코드보다 복잡합니다. 비동기 작업이 완료될 때까지 기다리는 메커니즘이 필요합니다.

  • Mocking: API 호출이나 Promise를 반환하는 함수를 Jest와 같은 테스트 프레임워크를 사용하여 Mocking합니다. Mock 함수가 예상된 결과를 반환하도록 설정하여 실제 네트워크 요청 없이 테스트를 수행합니다.
  • waitFor, findBy 등의 유틸리티 활용: @testing-library/react-native와 같은 라이브러리는 비동기 작업으로 인해 UI가 업데이트될 때까지 기다리는 waitForfindBy 등의 쿼리를 제공합니다. 이를 사용하여 비동기 작업 완료 후 변경된 UI 상태를 검증할 수 있습니다.
  • React Query Testing Utilities: React Query는 자체적인 테스트 유틸리티를 제공하여 useQueryuseMutation 훅을 사용하는 컴포넌트를 쉽게 테스트할 수 있도록 지원합니다. QueryClient 인스턴스를 Mocking하거나 테스트 환경에 맞게 설정하여 사용합니다.

성능 최적화

비동기 작업은 성능에 큰 영향을 미칠 수 있습니다.

  • 불필요한 페칭 최소화: React Query의 캐싱 및 staleTime, cacheTime 옵션을 활용하여 불필요한 네트워크 요청을 줄입니다. staleTime은 캐시된 데이터가 최신으로 간주되는 시간을 설정하고, cacheTime은 캐시된 데이터가 메모리에 유지되는 시간을 설정합니다.
  • Pagination 및 Infinite Scrolling: 대량의 데이터를 한 번에 가져오기보다 페이지별로 나누어 가져오거나, 사용자가 스크롤할 때 추가 데이터를 로드하는 무한 스크롤 패턴을 적용합니다. React Query는 useInfiniteQuery 훅을 통해 이러한 기능을 지원합니다.
  • Debouncing 및 Throttling: 사용자 입력에 따라 검색 결과 등을 비동기적으로 가져와야 할 때, 사용자가 입력을 멈추거나(debounce) 일정 시간 간격으로(throttle) 요청이 발생하도록 하여 요청 빈도를 줄입니다.
  • 데이터 변환 및 필터링 (Data Transformation and Filtering): 서버에서 가져온 대량의 데이터를 UI에 표시하기 전에 필요한 형태로 변환하거나 필터링(transform or filter)하는 작업을 백그라운드 스레드(background thread, e.g., Web Workers in React Native)에서 수행하여 메인 스레드의 부하를 줄이는 방안을 고려할 수 있습니다.

오프라인 상태 처리

모바일 환경에서는 네트워크 연결이 불안정하거나 끊기는 경우가 많습니다. 비동기 작업이 오프라인 상태에서 어떻게 동작해야 하는지를 고려해야 합니다.

  • 오프라인 캐싱: React Query는 오프라인 상태에서도 캐시된 데이터를 보여줄 수 있습니다. 네트워크가 다시 연결되면 자동으로 백그라운드 업데이트를 시도합니다.
  • 오프라인 뮤테이션 (Optimistic Updates): 사용자가 데이터를 변경하는 뮤테이션을 요청했을 때, 서버 응답을 기다리지 않고 먼저 UI를 업데이트하여 즉각적인 피드백을 제공하는 기법입니다. 이후 네트워크가 연결되면 실제 서버로 요청을 보내고, 성공/실패 결과에 따라 UI를 최종 업데이트합니다. React Query는 useMutationonMutate, onError, onSettled 옵션을 활용하여 Optimistic Updates를 구현할 수 있도록 지원합니다.
  • 요청 큐잉: 오프라인 상태에서 발생한 뮤테이션 요청을 로컬에 저장해 두었다가 네트워크가 복구되었을 때 순차적으로 서버에 전송하는 기능은 react-query-offline와 같은 라이브러리를 통해 구현할 수 있습니다.

Async Storage 활용

비동기 저장소인 AsyncStorage는 로컬에 데이터를 영구적으로 저장할 때 사용됩니다. AsyncStorage 작업 역시 비동기적으로 이루어지므로, useEffect 내에서 사용하거나 별도의 유틸리티 함수로 추상화하여 사용하는 것이 일반적입니다. 사용자 설정 저장, 토큰 캐싱 등 간단한 비동기 로컬 데이터 관리에는 AsyncStorage가 유용할 수 있습니다. 하지만 복잡한 로컬 데이터베이스나 관계형 데이터가 필요하다면 Realm, SQLite 등의 대안을 고려해야 합니다.

이러한 종합적인 고려사항과 아키텍처 패턴을 염두에 두고 비동기 처리 전략을 설계하면, 더욱 견고하고 효율적이며 유지보수하기 쉬운 React Native 애플리케이션을 구축할 수 있습니다. 비동기 처리는 애플리케이션의 핵심적인 부분인 만큼, 충분한 시간을 투자하여 올바른 접근 방식을 선택하고 구현하는 것이 장기적으로 큰 이점을 가져다줍니다.


결론

React Native 개발에서 비동기 처리는 애플리케이션의 반응성, 성능, 그리고 사용자 경험에 직접적인 영향을 미치는 핵심 영역입니다. 이 글에서는 useEffect를 활용한 컴포넌트 레벨 비동기 작업 관리, React Query를 통한 효율적인 서버 상태 관리, 그리고 사용자에게 신뢰감을 주는 로딩 및 에러 처리 기법이라는 세 가지 핵심 노하우를 상세히 살펴보았습니다.

첫 번째 노하우useEffect의 올바른 사용법을 익히는 것입니다. async 함수를 콜백 내부에 정의하고 즉시 호출하는 패턴, 의존성 배열 관리, 그리고 무엇보다 AbortController와 같은 메커니즘을 활용한 정리(cleanup) 함수를 통해 컴포넌트 생애주기에 안전하게 비동기 작업을 통합하는 것이 중요합니다. 이를 통해 흔히 발생하는 "unmounted component state update" 경고를 방지하고 메모리 누수를 막을 수 있습니다.

두 번째 노하우는 복잡한 서버 상태 관리가 필요할 때 React Query와 같은 전문 라이브러리를 도입하는 것입니다. useEffect 방식의 수동적인 데이터 관리는 캐싱, 백그라운드 업데이트, 동기화 등의 요구사항 앞에서 한계를 드러냅니다. React Query는 이러한 서버 상태 관리의 어려움을 선언적으로 해결하며, 개발자는 데이터 페칭 및 관리의 복잡성에서 벗어나 UI 구현에 집중할 수 있게 됩니다. useQueryuseMutation 훅을 통해 데이터 페칭 및 수정 로직을 간결하게 작성하고, 강력한 캐싱 및 자동 업데이트 기능을 활용하여 성능과 사용자 경험을 크게 향상시킬 수 있습니다.

세 번째 노하우는 로딩 상태 및 에러 처리를 세심하게 구현하는 것입니다. 비동기 작업의 현재 상태를 사용자에게 명확하게 알리고(로딩 상태), 실패 상황을 우아하게 처리하며(에러 처리), 가능한 복구 옵션(다시 시도)을 제공하는 것은 애플리케이션의 신뢰성과 사용자 만족도를 높이는 데 필수적입니다. 수동 상태 관리든 React Query의 내장 기능을 활용하든, 항상 로딩 중, 성공, 에러 세 가지 상태를 염두에 두고 UI를 설계해야 합니다.

이 세 가지 노하우를 바탕으로, React Native 애플리케이션의 비동기 처리 로직을 더욱 견고하고 효율적으로 구축할 수 있습니다. 실무에서는 이 외에도 관심사 분리를 위한 커스텀 훅 및 서비스 레이어 패턴, 비동기 코드 테스트 전략, 성능 최적화 기법, 오프라인 상태 처리 등 다양한 고려사항이 필요합니다.

실무 적용 팁:

  • 작은 단위부터 시작하세요: 처음부터 모든 비동기 로직에 React Query를 적용하기보다, 간단한 컴포넌트의 로컬 데이터 페칭은 useEffect로 시작하고, 애플리케이션 규모가 커지거나 서버 상태 관리의 복잡성이 증가할 때 점진적으로 React Query를 도입하는 방식을 고려할 수 있습니다.
  • 로딩과 에러 처리를 최우선 순위로 두세요: 기능 구현 자체만큼 중요한 것이 사용자에게 현재 상황을 명확하게 알리는 것입니다. 비동기 작업을 구현할 때는 항상 로딩 및 에러 상태를 함께 고려하고 구현해야 합니다.
  • API 레이어를 추상화하세요: fetch 호출이나 라이브러리 사용 방식을 직접 UI 컴포넌트에 노출하기보다, 별도의 서비스 함수나 커스텀 훅으로 추상화하여 데이터 접근 방식을 중앙 집중화하고 재사용성을 높이세요.
  • 개발자 도구를 적극 활용하세요: React Native Debugger나 React Query Devtools와 같은 도구는 비동기 작업의 흐름, 상태 변화, 캐시 상태 등을 시각적으로 보여주어 문제 해결 및 성능 최적화에 큰 도움이 됩니다.

React Native 환경에서 비동기 처리는 끊임없이 발전하는 분야입니다. 새로운 Hook이나 라이브러리, 패턴들이 등장하고 있습니다. 하지만 핵심 원칙은 변하지 않습니다: UI 반응성 유지, 효율적인 리소스 사용, 그리고 견고한 상태 관리입니다. 이 글에서 다룬 노하우들이 여러분의 React Native 개발 여정에 유용한 나침반이 되기를 바랍니다. 깊이 있는 이해와 꾸준한 실무 경험을 통해 React Native 비동기 처리의 전문가로 성장하시길 응원합니다.

반응형