React Native 에서 네트워크가 불안정할 때의 대응하기

eeennsu·2026년 7월 7일

React Native

목록 보기
63/92

개요 - 온라인은 예외 상태다.

모바일에서 네트워크는 항상 불안정하다고 전제하고 설계한다. 안정적으로 고정된 위치에서 연결이 이루어지는 PC와 달리, 스마트폰은 지하철·엘리베이터·터널·해외 로밍·약한 와이파이와 연결된 케이스는 흔히 일어나는 일상이다. 때문에 앱개발자는 개발을 진행하면서 안정적인 신호 속에 개발을 하겠지만, 실제 앱이 사용되어지는 사용자의 관점에서 바라보아야 하는것도 중요하다.

  1. 연결 안 됨 — 비행기 모드, 신호 없음이다.
  2. 연결됐지만 인터넷 없음 — 와이파이는 잡혔으나 게이트웨이가 막힘(카페 로그인 페이지 등)이다. isConnected는 true인데 실제 요청은 실패하는 가장 까다로운 상태다.
  3. 연결됐지만 매우 느림 / 패킷 손실 — 요청이 30초씩 걸리거나 중간에 죽는다.
  4. 정상 — 빠르고 안정적이다.

핵심 원칙 세 가지로 압축된다.

  • 읽기는 캐시 우선(Cache-first) — 네트워크를 기다리지 않고 일단 보여준다.
  • 쓰기는 큐잉(Queue & Retry) — 실패해도 버리지 않고 모아뒀다가 재전송한다.
  • 상태는 정직하게(Honest UX) — 지금 온라인인지, 동기화 중인지, 실패했는지 사용자에게 명확히 보여준다.



1. 네트워크 상태 감지

모든 대응의 출발점은 현재 연결 상태를 아는 것이다. @react-native/netinfo 라이브러리를 사용한다.

import NetInfo from '@react-native-community/netinfo';

// 구독: 상태 변화 감지
const unsubscribe = NetInfo.addEventListener(state => {
  console.log('연결됨:', state.isConnected);
  console.log('인터넷 도달 가능:', state.isInternetReachable);
  console.log('타입:', state.type); // wifi / cellular / none
});

// 단발성 조회
const state = await NetInfo.fetch();

1) isConnectedisInternetReachable를 구분한다

  • isConnected: 네트워크 인터페이스가 붙어 있는지(와이파이/셀룰러 연결 여부)다.
  • isInternetReachable: 실제로 인터넷에 도달 가능한지다. 연결됐지만 인터넷 없음을 잡으려면 이 값을 봐야 한다.

isInternetReachable는 NetInfo가 주기적으로 reachability를 확인해 채워준다. 더 정확히 하려면 reachabilityUrl을 자사 헬스체크 엔드포인트로 설정한다.

NetInfo.configure({
  reachabilityUrl: 'https://api.myapp.com/health',
  reachabilityTest: async (response) => response.status === 204,
  reachabilityLongTimeout: 60 * 1000,
  reachabilityShortTimeout: 5 * 1000,
});

2) 전역 상태로 끌어올린다

연결 상태는 앱 전역에서 필요하므로 Context나 전역 스토어(Zustand 등)에 올려, 어느 화면에서든 구독하게 한다.



2. 읽기 전략 - 캐시 우선

1) 메모리 캐시만으로는 부족하다

TanStack Query의 기본 캐시는 메모리(JS 힙 스레드)상에 객체 형태로 저장되어 있어, 앱이 종료되면 사라진다. 콜드 스타트(오프라인 상태로 앱 재실행)에서도 데이터를 보여주려면 디스크에 영속화해야 한다.

2) React Query + 영속 캐시

TanStack Query의 persistQueryClient로 캐시를 디스크에 직렬화해 둔다. 저장소는 동기·고속인 MMKV를 권장한다.

import { QueryClient } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { MMKV } from 'react-native-mmkv';

const storage = new MMKV();

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 오프라인이어도 캐시를 stale로 두고 일단 보여줌
      staleTime: 1000 * 60,           // 1분간 fresh로 간주
      gcTime: 1000 * 60 * 60 * 24,    // 24시간 캐시 보관
      retry: 3,
      networkMode: 'offlineFirst',    // 캐시 먼저, 네트워크는 뒤에
    },
  },
});

const persister = {
  persistClient: async (client) => storage.set('rq-cache', JSON.stringify(client)),
  restoreClient: async () => {
    const cache = storage.getString('rq-cache');
    return cache ? JSON.parse(cache) : undefined;
  },
  removeClient: async () => storage.delete('rq-cache'),
};

persistQueryClient({ queryClient, persister });

3) networkMode: 'offlineFirst'

TanStack Query는 networkMode를 지원한다. offlineFirst로 두면 캐시를 즉시 반환하고, 네트워크가 가능할 때 백그라운드에서 갱신한다(Stale-While-Revalidate). 오프라인일 때 쿼리가 무한 pending에 빠지지 않게 한다.

4) 정리

읽기 흐름은 캐시 즉시 렌더 → 온라인이면 백그라운드 갱신 → 갱신분으로 화면 교체다. 사용자는 오프라인에서도 마지막으로 본 데이터를 그대로 본다.



3. 쓰기 전략 : 뮤테이션 큐잉 (Outbox 패턴)

읽기보다 어려운 게 쓰기다. 오프라인에서 "좋아요", "글 작성", "장바구니 담기"를 했을 때 그 동작을 잃으면 안 된다.

1) 옵티미스틱 업데이트 + 큐 적재

  1. 사용자 액션 발생 시 즉시 로컬 UI에 반영한다(옵티미스틱).
  2. 동시에 그 동작을 영속 큐(outbox) 에 적재한다.
  3. 온라인이 되면 큐를 순서대로 서버에 전송한다.
  4. 성공하면 큐에서 제거, 실패하면 재시도 정책에 따라 다시 시도한다.

2) TanStack Query의 뮤테이션 일시정지/재개

TanStack Query는 오프라인 시 뮤테이션을 paused 상태로 두고, 온라인 복귀 시 재개하는 기능을 내장한다. 뮤테이션도 영속화하면 앱 재시작 후에도 이어서 보낸다.

import { onlineManager } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';

// NetInfo를 React Query의 온라인 판단에 연결
onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected);
  });
});

// 앱 시작 시 일시정지됐던 뮤테이션 재개
queryClient.resumePausedMutations();

3) 멱등성(Idempotency)이 핵심이다

재시도 큐는 같은 요청을 두 번 보낼 수 있다. 네트워크가 죽어 응답을 못 받았지만 서버는 이미 처리한 경우, 재시도하면 중복 생성된다. 이를 막으려면:

  • 클라이언트가 각 뮤테이션에 고유 ID(클라이언트 생성 UUID, idempotency key)를 붙인다.
  • 서버는 같은 키의 요청을 한 번만 처리하고, 재요청에는 기존 결과를 반환한다.

멱등성 없이 재시도 큐를 돌리면 "결제 두 번", "댓글 중복" 같은 사고가 난다. 쓰기 큐를 도입할 때 반드시 서버와 함께 설계한다.

4) 큐 순서와 의존성

"글 작성 → 그 글에 댓글"처럼 순서 의존이 있는 동작은 큐를 FIFO로 처리하고, 앞 동작이 실패하면 뒤 동작을 막거나 보류한다. 의존 없는 동작은 병렬 처리해도 된다.



4. 재시도 전략

무작정 즉시 재시도하면 서버를 때리거나 배터리를 태운다. 정석은 지수 백오프 + 지터다.

1) 지수 백오프 + 지터(Jitter)

재시도 간격을 1초 → 2초 → 4초 → 8초처럼 늘리되, 랜덤 지터를 더해 여러 클라이언트가 동시에 재시도하는 thundering herd를 막는다.

// React Query의 retryDelay 예시
retryDelay: (attempt) =>
  Math.min(1000 * 2 ** attempt, 30000) + Math.random() * 1000,

2) 재시도해도 되는 것만 재시도한다

  • 재시도 O: 네트워크 오류, 타임아웃, 5xx 서버 오류, 429(레이트리밋, Retry-After 존중).
  • 재시도 X: 4xx 대부분(400, 401, 403, 404). 같은 요청을 다시 보내도 똑같이 실패하므로 재시도는 무의미하고, 사용자에게 에러를 알려야 한다.

3f) 타임아웃을 반드시 건다

느린 네트워크에서 요청이 영원히 매달리지 않게 AbortController로 상한을 둔다.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10초

try {
  const res = await fetch(url, { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

4) 재시도 횟수 상한 + 폴백

무한 재시도는 금지다. 최대 횟수를 정하고, 초과하면 사용자에게 실패를 알리거나 "나중에 다시 시도" 큐로 옮긴다.



5. 끊김 상태 UX

기술적으로 잘 처리해도, 사용자에게 지금 상태를 알려주지 않으면 "앱이 고장났다"고 느낀다. 상태를 정직하게 드러낸다.

1) 오프라인 배너

연결이 끊기면 화면 상단/하단에 얇은 배너를 띄운다. 복구되면 잠깐 "다시 연결됨"을 보여주고 사라지게 한다. 전체 화면을 막는 모달은 지양한다 — 오프라인에서도 캐시로 앱을 쓸 수 있어야 하기 때문이다.

2) 동기화 상태 표시

큐에 대기 중인 동작이 있으면 "전송 대기 중", "동기화 중"을 보여준다. 채팅·메모 앱의 "전송 중 → 전송됨" 체크 표시가 대표적 패턴이다. 사용자는 자기 동작이 사라지지 않았음을 확인하고 안심한다.

3) 액션별 낙관적 피드백

오프라인에서 누른 "좋아요"는 즉시 채워지되, 아직 미전송임을 은근히 표시(흐린 상태, 시계 아이콘 등)할 수 있다. 전송 성공 시 정상 상태로 전환한다.

4) 비활성화할 것과 허용할 것을 구분한다

  • 허용: 캐시 기반 읽기, 로컬 작성(큐잉됨)이다.
  • 비활성화 또는 명확한 안내: 실시간 결제, 본인인증처럼 온라인이 필수인 동작이다. 버튼을 누르게 한 뒤 실패시키기보다, 미리 비활성화하고 "오프라인에서는 사용할 수 없다"고 알린다.

5) 에러 메시지는 행동 가능하게

"오류가 발생했습니다" 대신 "인터넷 연결을 확인해 주세요"처럼 원인과 다음 행동을 알린다. 재시도 버튼을 함께 둔다.



6. 동기화 & 충돌 해결

오프라인에서 수정한 데이터가 온라인 복귀 시 서버 데이터와 충돌할 수 있다. 도메인에 맞는 정책을 정한다.

  • Last-Write-Wins(LWW) — 마지막 쓰기가 이긴다. 단순하지만 데이터 유실 가능성이 있다. 단순 설정값 등에 적합하다.
  • 서버 우선 / 클라이언트 우선 — 한쪽을 신뢰 기준으로 둔다.
  • 머지(Merge) — 필드 단위로 합친다. 구현이 복잡하지만 협업 데이터에 필요하다.
  • 버전/타임스탬프 기반 충돌 감지 — 각 레코드에 버전을 두고, 서버가 충돌을 감지해 클라이언트에 알린다.

본격 오프라인 우선 앱(노트, 협업 도구)이라면 WatermelonDB나 동기화 엔진(Replicache, PowerSync, Legend-State 등) 같은 전용 솔루션을 검토한다. 직접 outbox·동기화를 구현하는 비용을 크게 줄여준다.



7. 테스트

오프라인 동작은 실제로 끊어 보지 않으면 검증되지 않는다.

  • 비행기 모드 / 네트워크 컨디셔너로 끊김·느림을 재현한다. iOS는 Network Link Conditioner, Android는 에뮬레이터 네트워크 속도 설정을 쓴다.
  • 요청 중간에 끊기 시나리오를 본다(응답 직전 끊김 → 멱등성 검증).
  • 앱 강제 종료 후 재시작 시 큐가 살아남아 재전송되는지 본다.
  • 온라인 복귀 순간 큐가 순서대로 비워지는지, 중복 전송이 없는지 본다.


9. 체크리스트

  1. NetInfo로 isConnected + isInternetReachable를 전역 구독한다.
  2. 읽기는 영속 캐시(MMKV) + offlineFirst로 캐시 우선 렌더한다.
  3. 쓰기는 옵티미스틱 + 영속 큐(outbox) + 온라인 복귀 시 재개로 처리한다.
  4. 모든 뮤테이션에 멱등성 키를 붙여 중복 전송을 막는다(서버와 함께 설계).
  5. 재시도는 지수 백오프 + 지터, 재시도 가능한 오류만, 횟수 상한 + 타임아웃을 둔다.
  6. 오프라인 배너·동기화 상태·행동 가능한 에러로 상태를 정직하게 보여준다.
  7. 충돌 해결 정책(LWW/머지/버전)을 도메인에 맞게 정한다.
  8. 비행기 모드·중간 끊김·앱 재시작 시나리오로 실제 검증한다.

핵심은 하나다. 읽기는 캐시로 항상 보여주고, 쓰기는 절대 잃지 않으며, 상태는 숨기지 않는다. 이 셋을 지키면 네트워크가 불안정해도 앱은 안정적으로 느껴진다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글