Capacitor 앱에서 네트워크 상태를 감지하려면 설치하세요 @capacitor/network, 호출 Network.getStatus() 현재 상태를 확인하고, networkStatusChange 업데이트를 구독하세요. 그러면 기기에 연결이 있는지, Wi-Fi인지 셀룰러인지 알 수 있습니다. 인터넷이 실제로 되는지, 아니면 종량제(metered) 연결인지 확인하려면 reachability 검사를 추가하거나 @capgo/capacitor-network-diagnostics. 이 가이드에서는 Capacitor 8 code을 통해 세 가지 레이어를 모두 다룹니다.
“사용자가 온라인인가?”라는 질문 뒤에 숨은 세 가지 질문
| 질문 | API | 참고 사항 |
|---|---|---|
| 활성 네트워크 인터페이스가 있는가? | Network.getStatus().connected |
빠르고 이벤트 기반이지만, 인터넷 접속 여부는 알 수 없음 |
| 어떤 종류의 연결인가? | connectionType |
wifi, cellular, none, unknown 공식 플러그인에서 |
| 인터넷이 실제로 작동하는가? | HTTP 체크 또는 internetReachable |
요청 또는 OS 검증 필요 |
| 대용량 다운로드를 피해야 하나요? | expensive, constrained |
데이터 과금 네트워크와 iOS 저데이터 모드 |
대부분의 오프라인 버그는 첫 번째 행을 세 번째 행으로 오인할 때 발생합니다. 로그인 페이지가 있는 호텔 Wi-Fi는 connected: true 로 보고되지만 모든 API 호출은 타임아웃됩니다.
Network 플러그인 설치
bun add @capacitor/network
bunx cap sync
설정할 것이 없습니다. Android에서는 플러그인이 ACCESS_NETWORK_STATE 을 병합된 매니페스트에 추가하며, 이는 설치 시 자동으로 부여되는 일반 권한입니다. iOS에서는 아무것도 필요하지 않습니다.
현재 상태 읽기
import { Network } from '@capacitor/network';
const status = await Network.getStatus();
console.log(status.connected); // true or false
console.log(status.connectionType); // 'wifi' | 'cellular' | 'none' | 'unknown'
캐시 또는 네트워크에서 로드할지 결정하기 전에 앱 시작 시 호출하세요. OS 상태를 읽기 때문에 트래픽을 보내지 않으며 몇 밀리초 만에 반환됩니다.
네트워크 변경 감시
import { Network, type ConnectionStatus } from '@capacitor/network';
import type { PluginListenerHandle } from '@capacitor/core';
let handle: PluginListenerHandle | undefined;
export async function watchNetwork(onChange: (s: ConnectionStatus) => void) {
onChange(await Network.getStatus());
handle = await Network.addListener('networkStatusChange', onChange);
}
export async function stopWatchingNetwork() {
await handle?.remove();
}
실용적인 세부 사항 두 가지:
- 이벤트 디바운스 처리 Wi-Fi에서 셀룰러로 전환할 때, 새 링크가 연결되기 전까지 두 플랫폼 모두 잠시
none로 보고할 수 있습니다. '오프라인' 배너를 표시하기 전 1~2초 디바운스를 적용하면 깜빡임을 방지할 수 있습니다. - 리스너 제거 컴포넌트가 언마운트될 때 리스너를 제거하지 않으면, 핫 리로드 및 내비게이션 후 콜백이 여러 번 실행됩니다.
React 훅
import { useEffect, useState } from 'react';
import { Network, type ConnectionStatus } from '@capacitor/network';
export function useNetworkStatus() {
const [status, setStatus] = useState<ConnectionStatus>({ connected: true, connectionType: 'unknown' });
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
const listener = Network.addListener('networkStatusChange', (s) => {
clearTimeout(timer);
timer = setTimeout(() => setStatus(s), s.connected ? 0 : 1500);
});
Network.getStatus().then(setStatus);
return () => {
clearTimeout(timer);
listener.then((l) => l.remove());
};
}, []);
return status;
}
온라인 전환은 즉시 적용되고, 오프라인 전환은 1.5초 대기합니다. 동일한 패턴을 Vue composable 또는 Angular service로 사용할 수 있으며 BehaviorSubject.
“connected”가 온라인을 의미하지 않는 이유
connected: true OS 네트워크 스택에서 가져옵니다. 다음 경우에도 true로 유지됩니다:
- Wi-Fi 네트워크에 캡티브 포털이 있는 경우 (공항, 호텔, 기차)
- 라우터에 업스트림 연결이 없는 경우
- DNS가 손상되거나 필터링되는 경우
- 기업 방화벽 또는 VPN이 API 도메인을 차단하는 경우
- 자체 백엔드가 다운된 경우
해결 방법은 자신이 제어하는 대상에 대한 도달성 확인입니다:
export async function canReachApi(timeoutMs = 4000): Promise<boolean> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch('https://api.example.com/health', {
method: 'HEAD',
cache: 'no-store',
signal: controller.signal,
});
return res.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
앱 시작 시, networkStatusChange 보고서에 따르면 connected: true그리고 요청이 실패한 후에 실행하세요. 몇 초마다 폴링하지 마세요. 배터리와 데이터를 소모합니다. 헬스 엔드포인트가 앱의 origin을 CORS에 허용하도록 확인하세요 (capacitor://localhost iOS에서는 https://localhost Android에서는 기본적으로) 또는 CapacitorHttp 를 사용해 네이티브 code에서 전송합니다.
네트워크 진단으로 더 풍부한 상태 확인
@capgo/capacitor-network-diagnostics OS가 파악하는 정보를 더 많이 읽고, WebView CORS를 우회하는 네이티브 체크를 실행할 수 있습니다:
bun add @capgo/capacitor-network-diagnostics
bunx cap sync
import { NetworkDiagnostics } from '@capgo/capacitor-network-diagnostics';
const status = await NetworkDiagnostics.getNetworkStatus();
// {
// connected: true,
// connectionType: 'wifi' | 'cellular' | 'ethernet' | 'vpn' | 'other' | 'unknown' | 'none',
// internetReachable: true,
// expensive: false, // metered / cellular
// constrained: false, // iOS Low Data Mode
// captivePortal: false, // Android only
// details: { ... }
// }
필드가 OS에 매핑되는 방식:
| 필드 | Android | iOS |
|---|---|---|
internetReachable |
NET_CAPABILITY_VALIDATED (Android가 인터넷을 탐지함) |
의 NWPathMonitor 경로 상태를 반영합니다 |
expensive |
isActiveNetworkMetered() |
NWPath.isExpensive |
constrained |
항상 false |
NWPath.isConstrained (저용량 데이터 모드) |
captivePortal |
NET_CAPABILITY_CAPTIVE_PORTAL |
미보고 |
vpn 타입 |
지원 | 지원 |
Android에서는, internetReachable OS가 이미 연결을 테스트했기 때문에 강력한 신호입니다. iOS에서는 더 가깝습니다 connected, 따라서 중요한 플로우에는 HTTP 체크를 유지하세요.
네이티브 도달성 체크
const api = await NetworkDiagnostics.testUrl({
url: 'https://api.example.com/health',
method: 'HEAD',
timeoutMs: 4000,
});
// api.reachable, api.statusCode, api.durationMs, api.errorCode
const socket = await NetworkDiagnostics.testWebSocket({ url: 'wss://realtime.example.com' });
const port = await NetworkDiagnostics.testPort({ host: 'mqtt.example.com', port: 8883 });
네이티브 요청은 CORS에 해당하지 않으므로 서드파티 호스트도 테스트할 수 있습니다.
지원용 '네트워크 리포트' 화면
사용자가 “앱이 안 된다”고 할 때, 원탭 진단 리포트가 수 시간의 반복 커뮤니케이션을 절약해 줍니다:
const report = await NetworkDiagnostics.runDiagnostics({
urls: [{ url: 'https://api.example.com/health' }, { url: 'https://cdn.example.com/ping' }],
websockets: [{ url: 'wss://realtime.example.com' }],
download: { url: 'https://cdn.example.com/1mb.bin', maxBytes: 1_048_576 },
packetLoss: { url: 'https://api.example.com/health', count: 5 },
});
console.log(report.issues); // human-readable list of problems
console.log(report.download?.mbps);
console.log(report.packetLoss?.lossPercent);
JSON을 지원 티켓에 첨부하세요. 문제가 DNS, 차단된 WebSocket, 캡티브 포털, 느린 링크 중 무엇인지 바로 확인할 수 있습니다.
데이터 요금제 및 Low Data Mode 연결에 대응하기
모바일 데이터 요금이 비싼 시장에서 데이터 제한을 준수하는 것은 리뷰 점수 향상의 빠른 방법입니다:
export async function shouldPrefetchMedia() {
const s = await NetworkDiagnostics.getNetworkStatus();
if (!s.connected) return false;
if (s.constrained) return false; // user asked iOS to save data
if (s.expensive) return false; // cellular or metered Wi-Fi
return true;
}
비디오 자동 재생, 이미지 품질, 백그라운드 동기화, 대용량 다운로드 결정에 사용하세요. Android에서는 사용자가 Wi-Fi 네트워크를 데이터 요금제로 표시할 수 있으므로 Wi-Fi가 무료라고 가정하지 마세요.
오프라인 배너 만들기
요청 실패보다 작은 배너가 더 명확합니다. 위의 훅을 사용하여:
export function OfflineBanner() {
const { connected } = useNetworkStatus();
if (connected) return null;
return (
<div role="status" aria-live="polite" className="offline-banner">
You are offline. Changes will sync when you reconnect.
</div>
);
}
role="status" 포함 aria-live="polite" 스크린 리더가 변경 사항을 알리도록 합니다. 프레임워크별 UI에 대해서는 다음 가이드를 참고하세요: Vue, Angular, React에서 오프라인 화면 만들기.
오프라인 상태 동안 작업 대기열에 추가하기
감지는 오프라인 지원의 절반에 불과합니다. 상태가 오프라인으로 전환되면:
- 로컬 스토리지에서 읽기 요청을 처리합니다. SQLite 데이터베이스와 같은
@capgo/capacitor-fast-sql는localStorage. - 사용자 작업을 타임스탬프와 멱등성 키(idempotency key)를 포함하여 outbox 테이블에 기록하세요.
- 웹
networkStatusChange로 연결되면canReachApi()을 실행한 후 outbox를 순서대로 flush하세요. - 실패한 요청은 지수 백오프(exponential backoff)로 재시도하되, 최대 몇 분으로 제한하세요.
앱이 종료되어도 유지되어야 하는 업로드의 경우, 대신 네이티브 업로더에 위임하세요. fetch.
웹 fallback
@capacitor/network 은 브라우저에서도 실행되며, 이때는 navigator.onLine 및 online 및 offline events. 즉, code은 bun run dev 에서 변경 없이 PWA에서도 작동하지만 connectionType 웹에서는 종종 unknown 됩니다. Chrome의 navigator.connection 는 제공하며, effectiveType 및 saveData은 Safari에 없으므로 선택적 힌트로 취급하세요.
문제 해결
iOS 시뮬레이터에서는 리스너가 절대 발화되지 않습니다: 시뮬레이터는 Mac의 네트워크를 공유합니다. Mac의 Wi-Fi를 전환하거나 비행기 모드가 있는 기기에서 테스트하세요.
connectionType 이며 unknown VPN에서: 공식 플러그인은 단순히 보고만 합니다 wifi, cellular, none 및 unknown. 진단 플러그인을 사용하세요. 이 플러그인은 반환합니다 vpn 및 ethernet.
네트워크를 전환할 때 앱이 1초간 오프라인으로 표시됩니다: 위에서 보여준 대로 오프라인 전환을 디바운스하세요.
디바이스에서는 헬스 체크가 실패하지만 브라우저에서는 작동합니다: CORS입니다. 허용하세요 capacitor://localhost 및 https://localhost, 또는 NetworkDiagnostics.testUrl을(를) 사용하세요. 네이티브로 실행됩니다.
navigator.onLine 플러그인은 오프라인이라고 하지만 true입니다: 플러그인을 신뢰하세요. 일부 Android WebView는 업데이트를 navigator.onLine 신뢰적으로 수행하지 않습니다.
오프라인 수정 사항을 OTA로 배포하세요
오프라인 처리는 대부분 JavaScript입니다: 임계값, 배너, 재시도 로직, 캐시 규칙. Capgo Live Update 사용하기을(를) 사용하면 같은 날 모든 사용자에게 이를 조정할 수 있습니다. Capgo는 백그라운드에서 업데이트 번들을 다운로드하고 다운로드가 실패하면 현재 버전을 유지하므로, 불안정한 연결이 사용자에게 반쯤 설치된 업데이트를 남기지 않습니다. 네트워크 진단 문서 에 모든 옵션이 나열되어 있습니다.