모바일에서 네트워크는 항상 불안정하다고 전제하고 설계한다. 안정적으로 고정된 위치에서 연결이 이루어지는 PC와 달리, 스마트폰은 지하철·엘리베이터·터널·해외 로밍·약한 와이파이와 연결된 케이스는 흔히 일어나는 일상이다. 때문에 앱개발자는 개발을 진행하면서 안정적인 신호 속에 개발을 하겠지만, 실제 앱이 사용되어지는 사용자의 관점에서 바라보아야 하는것도 중요하다.
isConnected는 true인데 실제 요청은 실패하는 가장 까다로운 상태다.핵심 원칙 세 가지로 압축된다.
모든 대응의 출발점은 현재 연결 상태를 아는 것이다. @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();
isConnected와 isInternetReachable를 구분한다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,
});
연결 상태는 앱 전역에서 필요하므로 Context나 전역 스토어(Zustand 등)에 올려, 어느 화면에서든 구독하게 한다.
TanStack Query의 기본 캐시는 메모리(JS 힙 스레드)상에 객체 형태로 저장되어 있어, 앱이 종료되면 사라진다. 콜드 스타트(오프라인 상태로 앱 재실행)에서도 데이터를 보여주려면 디스크에 영속화해야 한다.
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 });
networkMode: 'offlineFirst'TanStack Query는 networkMode를 지원한다. offlineFirst로 두면 캐시를 즉시 반환하고, 네트워크가 가능할 때 백그라운드에서 갱신한다(Stale-While-Revalidate). 오프라인일 때 쿼리가 무한 pending에 빠지지 않게 한다.
읽기 흐름은 캐시 즉시 렌더 → 온라인이면 백그라운드 갱신 → 갱신분으로 화면 교체다. 사용자는 오프라인에서도 마지막으로 본 데이터를 그대로 본다.
읽기보다 어려운 게 쓰기다. 오프라인에서 "좋아요", "글 작성", "장바구니 담기"를 했을 때 그 동작을 잃으면 안 된다.
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();
재시도 큐는 같은 요청을 두 번 보낼 수 있다. 네트워크가 죽어 응답을 못 받았지만 서버는 이미 처리한 경우, 재시도하면 중복 생성된다. 이를 막으려면:
멱등성 없이 재시도 큐를 돌리면 "결제 두 번", "댓글 중복" 같은 사고가 난다. 쓰기 큐를 도입할 때 반드시 서버와 함께 설계한다.
"글 작성 → 그 글에 댓글"처럼 순서 의존이 있는 동작은 큐를 FIFO로 처리하고, 앞 동작이 실패하면 뒤 동작을 막거나 보류한다. 의존 없는 동작은 병렬 처리해도 된다.
무작정 즉시 재시도하면 서버를 때리거나 배터리를 태운다. 정석은 지수 백오프 + 지터다.
재시도 간격을 1초 → 2초 → 4초 → 8초처럼 늘리되, 랜덤 지터를 더해 여러 클라이언트가 동시에 재시도하는 thundering herd를 막는다.
// React Query의 retryDelay 예시
retryDelay: (attempt) =>
Math.min(1000 * 2 ** attempt, 30000) + Math.random() * 1000,
Retry-After 존중).느린 네트워크에서 요청이 영원히 매달리지 않게 AbortController로 상한을 둔다.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10초
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
무한 재시도는 금지다. 최대 횟수를 정하고, 초과하면 사용자에게 실패를 알리거나 "나중에 다시 시도" 큐로 옮긴다.
기술적으로 잘 처리해도, 사용자에게 지금 상태를 알려주지 않으면 "앱이 고장났다"고 느낀다. 상태를 정직하게 드러낸다.
연결이 끊기면 화면 상단/하단에 얇은 배너를 띄운다. 복구되면 잠깐 "다시 연결됨"을 보여주고 사라지게 한다. 전체 화면을 막는 모달은 지양한다 — 오프라인에서도 캐시로 앱을 쓸 수 있어야 하기 때문이다.
큐에 대기 중인 동작이 있으면 "전송 대기 중", "동기화 중"을 보여준다. 채팅·메모 앱의 "전송 중 → 전송됨" 체크 표시가 대표적 패턴이다. 사용자는 자기 동작이 사라지지 않았음을 확인하고 안심한다.
오프라인에서 누른 "좋아요"는 즉시 채워지되, 아직 미전송임을 은근히 표시(흐린 상태, 시계 아이콘 등)할 수 있다. 전송 성공 시 정상 상태로 전환한다.
"오류가 발생했습니다" 대신 "인터넷 연결을 확인해 주세요"처럼 원인과 다음 행동을 알린다. 재시도 버튼을 함께 둔다.
오프라인에서 수정한 데이터가 온라인 복귀 시 서버 데이터와 충돌할 수 있다. 도메인에 맞는 정책을 정한다.
본격 오프라인 우선 앱(노트, 협업 도구)이라면 WatermelonDB나 동기화 엔진(Replicache, PowerSync, Legend-State 등) 같은 전용 솔루션을 검토한다. 직접 outbox·동기화를 구현하는 비용을 크게 줄여준다.
오프라인 동작은 실제로 끊어 보지 않으면 검증되지 않는다.
isConnected + isInternetReachable를 전역 구독한다.offlineFirst로 캐시 우선 렌더한다.핵심은 하나다. 읽기는 캐시로 항상 보여주고, 쓰기는 절대 잃지 않으며, 상태는 숨기지 않는다. 이 셋을 지키면 네트워크가 불안정해도 앱은 안정적으로 느껴진다.