Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

Google Nearby Connections API 완전 정복 가이드 (feat Claude.ai)

Billcorea 2025. 7. 23. 15:57
반응형

 

🔗 Google Nearby Connections API 완전 정복 가이드

오늘날 모바일 애플리케이션에서 기기 간 통신은 점점 중요해지고 있습니다. Google의 Nearby Connections API는 인터넷 연결 없이도 가까운 거리의 기기들 간에 안전하고 빠른 데이터 통신을 가능하게 하는 강력한 도구입니다.

 

앱 적용 예시

 

📱 Nearby Connections API란?

Nearby Connections API는 Google이 제공하는 크로스 플랫폼 API로, 다음과 같은 특징을 가집니다:

  • 오프라인 통신: 인터넷 연결 없이 기기 간 직접 통신
  • 다중 프로토콜 지원: Bluetooth, WiFi Direct, WiFi LAN 자동 선택
  • 높은 보안성: 모든 통신은 암호화되어 전송
  • 크로스 플랫폼: Android와 iOS 모두 지원
  • 쉬운 구현: 복잡한 네트워크 설정 없이 간단한 API 호출로 구현
💡 실생활 활용 사례
- 멀티플레이어 게임 (오프라인 대전)
- 파일 공유 앱
- 협업 도구 (화이트보드, 프레젠테이션)
- IoT 기기 제어
- 비상 상황 통신 시스템

🏗️ 기본 구조 및 개념

1. 주요 역할

  • Advertiser (광고자): 다른 기기가 발견할 수 있도록 자신을 광고하는 기기
  • Discoverer (발견자): 근처의 광고 중인 기기를 찾는 기기
🔄 동작 과정
1. Advertiser가 서비스를 광고 시작
2. Discoverer가 근처 기기를 탐색
3. 기기 발견 및 연결 요청
4. 연결 승인 후 데이터 송수신
5. 연결 종료

⚙️ 프로젝트 설정

build.gradle (Module: app) 설정

// Nearby Connections API 의존성 추가
implementation 'com.google.android.gms:play-services-nearby:19.0.0'

android {
    compileSdk 34
    
    defaultConfig {
        minSdk 21  // Nearby API 최소 요구사항
        targetSdk 34
    }
}

AndroidManifest.xml 권한 설정

<!-- 필수 권한들 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Android 12 이상을 위한 새로운 권한 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
⚠️ 중요한 주의사항
Android 6.0 (API 23) 이상에서는 런타임 권한 요청이 필수입니다. 특히 위치 권한은 Bluetooth와 WiFi 스캔에 필요합니다.

🚀 기본 구현 예제

1. MainActivity - 기본 설정 및 권한 처리

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "NearbyConnections";
    
    // 서비스 ID - 같은 앱끼리 통신하기 위한 고유 식별자
    private static final String SERVICE_ID = "com.yourapp.nearbyconnections";
    
    // 연결 상태를 추적하기 위한 변수들
    private String connectedEndpointId = "";
    private boolean isAdvertising = false;
    private boolean isDiscovering = false;
    
    // UI 컴포넌트들
    private Button btnStartAdvertising, btnStartDiscovery, btnSendMessage;
    private TextView tvStatus, tvMessages;
    private EditText etMessage;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // UI 컴포넌트 초기화
        initializeViews();
        
        // 권한 요청
        requestNecessaryPermissions();
        
        // 버튼 클릭 리스너 설정
        setupClickListeners();
    }
    
    /**
     * UI 컴포넌트들을 초기화하는 메소드
     */
    private void initializeViews() {
        btnStartAdvertising = findViewById(R.id.btnStartAdvertising);
        btnStartDiscovery = findViewById(R.id.btnStartDiscovery);
        btnSendMessage = findViewById(R.id.btnSendMessage);
        tvStatus = findViewById(R.id.tvStatus);
        tvMessages = findViewById(R.id.tvMessages);
        etMessage = findViewById(R.id.etMessage);
        
        // 초기 상태 설정
        updateUI();
    }
    
    /**
     * 필요한 권한들을 요청하는 메소드
     */
    private void requestNecessaryPermissions() {
        String[] permissions = {
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_FINE_LOCATION
        };
        
        // Android 12 이상에서 추가 권한 확인
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            permissions = new String[] {
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.BLUETOOTH_ADVERTISE,
                Manifest.permission.BLUETOOTH_CONNECT,
                Manifest.permission.BLUETOOTH_SCAN,
                Manifest.permission.NEARBY_WIFI_DEVICES
            };
        }
        
        // 권한이 없는 경우 요청
        ActivityCompat.requestPermissions(this, permissions, 1001);
    }

2. 광고 시작 (Advertiser 역할)

/**
 * 다른 기기들이 이 기기를 발견할 수 있도록 광고를 시작하는 메소드
 */
private void startAdvertising() {
    // 광고 옵션 설정
    AdvertisingOptions advertisingOptions = new AdvertisingOptions.Builder()
        .setStrategy(Strategy.P2P_CLUSTER) // 1:N 연결 전략
        .build();
    
    // 연결된 기기의 이름 (다른 기기에서 보여질 이름)
    String localEndpointName = Build.MODEL; // 기기 모델명 사용
    
    Nearby.getConnectionsClient(this)
        .startAdvertising(
            localEndpointName,      // 광고할 기기 이름
            SERVICE_ID,             // 서비스 ID
            connectionLifecycleCallback, // 연결 생명주기 콜백
            advertisingOptions      // 광고 옵션
        )
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void unused) {
                // 광고 시작 성공
                isAdvertising = true;
                updateStatus("광고 시작됨 - 다른 기기에서 발견 가능");
                updateUI();
                Log.d(TAG, "광고 시작 성공");
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // 광고 시작 실패
                updateStatus("광고 시작 실패: " + e.getMessage());
                Log.e(TAG, "광고 시작 실패", e);
            }
        });
}

3. 기기 탐색 시작 (Discoverer 역할)

/**
 * 근처의 광고 중인 기기들을 탐색하는 메소드
 */
private void startDiscovery() {
    // 탐색 옵션 설정
    DiscoveryOptions discoveryOptions = new DiscoveryOptions.Builder()
        .setStrategy(Strategy.P2P_CLUSTER) // 광고와 동일한 전략 사용
        .build();
    
    Nearby.getConnectionsClient(this)
        .startDiscovery(
            SERVICE_ID,              // 찾을 서비스 ID
            endpointDiscoveryCallback, // 기기 발견 콜백
            discoveryOptions         // 탐색 옵션
        )
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void unused) {
                // 탐색 시작 성공
                isDiscovering = true;
                updateStatus("탐색 시작됨 - 근처 기기 검색 중...");
                updateUI();
                Log.d(TAG, "탐색 시작 성공");
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // 탐색 시작 실패
                updateStatus("탐색 시작 실패: " + e.getMessage());
                Log.e(TAG, "탐색 시작 실패", e);
            }
        });
}

4. 기기 발견 콜백

/**
 * 기기 발견 시 호출되는 콜백
 */
private final EndpointDiscoveryCallback endpointDiscoveryCallback =
    new EndpointDiscoveryCallback() {
        @Override
        public void onEndpointFound(@NonNull String endpointId, 
                                  @NonNull DiscoveredEndpointInfo info) {
            // 새로운 기기 발견!
            Log.d(TAG, "기기 발견: " + info.getEndpointName());
            updateStatus("기기 발견: " + info.getEndpointName() + " - 연결 시도 중...");
            
            // 자동으로 연결 요청 보내기
            requestConnection(endpointId, info.getEndpointName());
        }
        
        @Override
        public void onEndpointLost(@NonNull String endpointId) {
            // 기기 연결 끊어짐
            Log.d(TAG, "기기 연결 끊어짐: " + endpointId);
            updateStatus("기기 연결이 끊어졌습니다.");
        }
    };

/**
 * 발견된 기기에 연결을 요청하는 메소드
 */
private void requestConnection(String endpointId, String endpointName) {
    Nearby.getConnectionsClient(this)
        .requestConnection(
            Build.MODEL,              // 내 기기 이름
            endpointId,              // 연결할 기기 ID
            connectionLifecycleCallback // 연결 생명주기 콜백
        )
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void unused) {
                // 연결 요청 전송 성공
                Log.d(TAG, "연결 요청 전송됨: " + endpointName);
                updateStatus("연결 요청 전송됨: " + endpointName);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // 연결 요청 실패
                Log.e(TAG, "연결 요청 실패", e);
                updateStatus("연결 요청 실패: " + e.getMessage());
            }
        });
}

5. 연결 생명주기 관리

/**
 * 연결의 생명주기를 관리하는 콜백
 */
private final ConnectionLifecycleCallback connectionLifecycleCallback =
    new ConnectionLifecycleCallback() {
        @Override
        public void onConnectionInitiated(@NonNull String endpointId, 
                                        @NonNull ConnectionInfo connectionInfo) {
            // 연결이 시작됨 - 사용자에게 승인/거절 선택권 제공
            Log.d(TAG, "연결 요청 받음: " + connectionInfo.getEndpointName());
            
            // 자동으로 연결 승인 (실제 앱에서는 사용자 확인 받는 것이 좋음)
            Nearby.getConnectionsClient(MainActivity.this)
                .acceptConnection(endpointId, payloadCallback);
            
            updateStatus("연결 승인됨: " + connectionInfo.getEndpointName());
        }
        
        @Override
        public void onConnectionResult(@NonNull String endpointId, 
                                     @NonNull ConnectionResolution result) {
            switch (result.getStatus().getStatusCode()) {
                case ConnectionsStatusCodes.STATUS_OK:
                    // 연결 성공!
                    Log.d(TAG, "연결 성공: " + endpointId);
                    connectedEndpointId = endpointId;
                    updateStatus("연결 완료! 메시지를 보낼 수 있습니다.");
                    
                    // 탐색과 광고 중지 (1:1 연결이므로)
                    stopDiscovery();
                    stopAdvertising();
                    
                    updateUI();
                    break;
                    
                case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED:
                    // 연결 거절됨
                    Log.d(TAG, "연결 거절됨: " + endpointId);
                    updateStatus("연결이 거절되었습니다.");
                    break;
                    
                default:
                    // 기타 연결 실패
                    Log.d(TAG, "연결 실패: " + endpointId);
                    updateStatus("연결에 실패했습니다.");
                    break;
            }
        }
        
        @Override
        public void onDisconnected(@NonNull String endpointId) {
            // 연결 끊어짐
            Log.d(TAG, "연결 끊어짐: " + endpointId);
            connectedEndpointId = "";
            updateStatus("연결이 끊어졌습니다.");
            updateUI();
        }
    };

6. 데이터 송수신

/**
 * 메시지를 전송하는 메소드
 */
private void sendMessage(String message) {
    if (connectedEndpointId.isEmpty()) {
        updateStatus("연결된 기기가 없습니다.");
        return;
    }
    
    // 텍스트 메시지를 바이트 배열로 변환
    Payload bytesPayload = Payload.fromBytes(message.getBytes());
    
    // 메시지 전송
    Nearby.getConnectionsClient(this)
        .sendPayload(connectedEndpointId, bytesPayload)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void unused) {
                // 전송 성공
                Log.d(TAG, "메시지 전송 성공: " + message);
                appendMessage("나: " + message);
                etMessage.setText(""); // 입력 필드 초기화
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // 전송 실패
                Log.e(TAG, "메시지 전송 실패", e);
                updateStatus("메시지 전송 실패: " + e.getMessage());
            }
        });
}

/**
 * 데이터 수신을 처리하는 콜백
 */
private final PayloadCallback payloadCallback = new PayloadCallback() {
    @Override
    public void onPayloadReceived(@NonNull String endpointId, @NonNull Payload payload) {
        // 데이터 수신됨
        if (payload.getType() == Payload.Type.BYTES) {
            // 바이트 데이터인 경우 (텍스트 메시지)
            String receivedMessage = new String(payload.asBytes());
            Log.d(TAG, "메시지 수신: " + receivedMessage);
            
            // UI 업데이트는 메인 스레드에서 실행
            runOnUiThread(() -> {
                appendMessage("상대방: " + receivedMessage);
            });
        }
    }
    
    @Override
    public void onPayloadTransferUpdate(@NonNull String endpointId, 
                                      @NonNull PayloadTransferUpdate update) {
        // 전송 상태 업데이트 (파일 전송 시 진행률 표시 등에 사용)
        if (update.getStatus() == PayloadTransferUpdate.Status.SUCCESS) {
            Log.d(TAG, "페이로드 전송 완료");
        } else if (update.getStatus() == PayloadTransferUpdate.Status.FAILURE) {
            Log.e(TAG, "페이로드 전송 실패");
        }
    }
};

7. UI 업데이트 및 정리 메소드

/**
 * UI 상태를 업데이트하는 메소드
 */
private void updateUI() {
    runOnUiThread(() -> {
        // 연결 상태에 따라 버튼 활성화/비활성화
        btnSendMessage.setEnabled(!connectedEndpointId.isEmpty());
        btnStartAdvertising.setEnabled(!isAdvertising);
        btnStartDiscovery.setEnabled(!isDiscovering);
    });
}

/**
 * 상태 메시지를 업데이트하는 메소드
 */
private void updateStatus(String message) {
    runOnUiThread(() -> {
        tvStatus.setText(message);
    });
}

/**
 * 메시지를 채팅창에 추가하는 메소드
 */
private void appendMessage(String message) {
    runOnUiThread(() -> {
        tvMessages.append(message + "\n");
    });
}

/**
 * 탐색을 중지하는 메소드
 */
private void stopDiscovery() {
    if (isDiscovering) {
        Nearby.getConnectionsClient(this).stopDiscovery();
        isDiscovering = false;
        Log.d(TAG, "탐색 중지됨");
    }
}

/**
 * 광고를 중지하는 메소드
 */
private void stopAdvertising() {
    if (isAdvertising) {
        Nearby.getConnectionsClient(this).stopAdvertising();
        isAdvertising = false;
        Log.d(TAG, "광고 중지됨");
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    
    // 앱 종료 시 모든 연결 해제 및 서비스 중지
    Nearby.getConnectionsClient(this).disconnectFromAllEndpoints();
    stopDiscovery();
    stopAdvertising();
}

📋 레이아웃 파일 (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <!-- 상태 표시 -->
    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="준비됨"
        android:textSize="16sp"
        android:padding="8dp"
        android:background="#E3F2FD"
        android:textColor="#1976D2" />

    <!-- 제어 버튼들 -->
    <Button
        android:id="@+id/btnStartAdvertising"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="광고 시작 (다른 기기에서 발견 가능)"
        android:layout_marginTop="16dp" />

    <Button
        android:id="@+id/btnStartDiscovery"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="기기 탐색 시작"
        android:layout_marginTop="8dp" />

    <!-- 메시지 입력 -->
    <EditText
        android:id="@+id/etMessage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="보낼 메시지를 입력하세요"
        android:layout_marginTop="16dp" />

    <Button
        android:id="@+id/btnSendMessage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="메시지 전송"
        android:enabled="false"
        android:layout_marginTop="8dp" />

    <!-- 메시지 표시 -->
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="16dp">

        <TextView
            android:id="@+id/tvMessages"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="메시지가 여기에 표시됩니다.\n"
            android:textSize="14sp"
            android:padding="8dp"
            android:background="#F5F5F5" />
    </ScrollView>

</LinearLayout>

🎯 고급 기능 및 팁

1. 파일 전송

/**
 * 파일을 전송하는 메소드
 */
private void sendFile(Uri fileUri) {
    try {
        // 파일에서 Payload 생성
        Payload filePayload = Payload.fromFile(fileUri);
        
        // 파일 정보를 먼저 전송 (파일명, 크기 등)
        String fileInfo = "FILE:" + getFileName(fileUri) + ":" + getFileSize(fileUri);
        Payload infoPayload = Payload.fromBytes(fileInfo.getBytes());
        
        // 정보 먼저 전송
        Nearby.getConnectionsClient(this)
            .sendPayload(connectedEndpointId, infoPayload);
        
        // 그 다음 파일 전송
        Nearby.getConnectionsClient(this)
            .sendPayload(connectedEndpointId, filePayload);
            
    } catch (Exception e) {
        Log.e(TAG, "파일 전송 실패", e);
    }
}

2. 연결 전략 선택

📊 연결 전략 비교
P2P_CLUSTER: 1:N 연결, 많은 기기와 동시 연결
P2P_STAR: 1:1 연결, 안정적이고 빠른 속도
P2P_POINT_TO_POINT: 1:1 전용, 최고 성능

3. 에러 처리 및 디버깅

/**
 * 상세한 에러 처리를 포함한 연결 메소드
 */
private void connectWithErrorHandling(String endpointId) {
    Nearby.getConnectionsClient(this)
        .requestConnection(Build.MODEL, endpointId, connectionLifecycleCallback)
        .addOnFailureListener(exception -> {
            // 구체적인 에러 타입별 처리
            if (exception instanceof ApiException) {
                ApiException apiException = (ApiException) exception;
                switch (apiException.getStatusCode()) {
                    case ConnectionsStatusCodes.STATUS_ENDPOINT_UNKNOWN:
                        updateStatus("기기를 찾을 수 없습니다.");
                        break;
                    case ConnectionsStatusCodes.STATUS_NETWORK_NOT_CONNECTED:
                        updateStatus("네트워크에 연결되지 않았습니다.");
                        break;
                    case ConnectionsStatusCodes.STATUS_BLUETOOTH_ERROR:
                        updateStatus("블루투스 오류가 발생했습니다.");
                        break;
                    default:
                        updateStatus("연결 오류: " + apiException.getStatusCode());
                        break;
                }
            }
        });
}

4. 보안 강화

/**
 * 연결 시 보안 인증을 추가하는 예제
 */
private void secureConnectionHandling(String endpointId, ConnectionInfo info) {
    // 연결 토큰 검증 (실제 앱에서는 더 복잡한 인증 로직 구현)
    String authToken = info.getAuthenticationDigits();
    
    // 사용자에게 인증 토큰 확인 요청
    new AlertDialog.Builder(this)
        .setTitle("연결 확인")
        .setMessage("다음 기기와 연결하시겠습니까?\n" + 
                   "기기명: " + info.getEndpointName() + "\n" +
                   "인증 코드: " + authToken)
        .setPositiveButton("승인", (dialog, which) -> {
            // 연결 승인
            Nearby.getConnectionsClient(this)
                .acceptConnection(endpointId, payloadCallback);
        })
        .setNegativeButton("거절", (dialog, which) -> {
            // 연결 거절
            Nearby.getConnectionsClient(this)
                .rejectConnection(endpointId);
        })
        .show();
}

🚀 성능 최적화 팁

1. 배터리 최적화

/**
 * 배터리 효율적인 설정
 */
private void optimizedAdvertising() {
    AdvertisingOptions options = new AdvertisingOptions.Builder()
        .setStrategy(Strategy.P2P_CLUSTER)
        // 저전력 모드 사용
        .setLowPower(true)
        .build();
        
    // 일정 시간 후 자동으로 광고 중지
    Handler handler = new Handler();
    handler.postDelayed(() -> {
        stopAdvertising();
        updateStatus("배터리 절약을 위해 광고가 중지되었습니다.");
    }, 60000); // 1분 후 중지
}

2. 연결 품질 모니터링

/**
 * 연결 품질을 모니터링하는 클래스
 */
public class ConnectionQualityMonitor {
    private long lastMessageTime = 0;
    private int failedMessages = 0;
    private static final long TIMEOUT_MS = 5000; // 5초 타임아웃
    
    public void onMessageSent() {
        lastMessageTime = System.currentTimeMillis();
    }
    
    public void onMessageFailed() {
        failedMessages++;
        if (failedMessages > 3) {
            // 연결 품질이 나쁘다고 판단, 재연결 시도
            reconnectWithBetterStrategy();
        }
    }
    
    private void reconnectWithBetterStrategy() {
        // 더 안정적인 전략으로 재연결
        // 예: P2P_STAR 전략 사용
    }
}

🔧 문제해결 가이드

⚠️ 자주 발생하는 문제들

1. 기기를 발견하지 못하는 경우:
- 위치 권한이 허용되었는지 확인
- 블루투스와 WiFi가 켜져있는지 확인
- 같은 SERVICE_ID를 사용하는지 확인

2. 연결이 자주 끊어지는 경우:
- 기기 간 거리가 너무 멀지 않은지 확인
- 다른 무선 신호 간섭이 없는지 확인
- 적절한 연결 전략을 선택했는지 확인

3. 메시지가 전송되지 않는 경우:
- 연결 상태를 확인
- 페이로드 크기 제한 확인 (최대 32KB)
- 네트워크 상태 확인

디버깅을 위한 로그 설정

/**
 * 상세한 로깅을 위한 헬퍼 클래스
 */
public class NearbyLogger {
    private static final String TAG = "NearbyConnections";
    
    public static void logConnectionState(String endpointId, String state) {
        Log.d(TAG, String.format("연결 상태 변경 - ID: %s, 상태: %s, 시간: %s", 
            endpointId, state, new Date()));
    }
    
    public static void logPayloadInfo(Payload payload) {
        Log.d(TAG, String.format("페이로드 정보 - ID: %d, 타입: %s, 크기: %d", 
            payload.getId(), 
            payload.getType().name(), 
            payload.asBytes() != null ? payload.asBytes().length : 0));
    }
}

🎨 UI/UX 개선 아이디어

연결 상태 시각화

/**
 * 연결 상태를 시각적으로 표시하는 메소드
 */
private void updateConnectionStatus(boolean isConnected) {
    runOnUiThread(() -> {
        if (isConnected) {
            // 연결됨 - 녹색 표시
            tvStatus.setBackgroundColor(ContextCompat.getColor(this, R.color.green_light));
            tvStatus.setTextColor(ContextCompat.getColor(this, R.color.green_dark));
            tvStatus.setText("🟢 연결됨 - 메시지 전송 가능");
        } else {
            // 연결 안됨 - 회색 표시
            tvStatus.setBackgroundColor(ContextCompat.getColor(this, R.color.grey_light));
            tvStatus.setTextColor(ContextCompat.getColor(this, R.color.grey_dark));
            tvStatus.setText("⚪ 연결 대기중");
        }
    });
}

/**
 * 메시지에 타임스탬프 추가
 */
private void appendMessageWithTimestamp(String message) {
    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
    String timestamp = sdf.format(new Date());
    String formattedMessage = String.format("[%s] %s", timestamp, message);
    
    runOnUiThread(() -> {
        tvMessages.append(formattedMessage + "\n");
        // 스크롤을 맨 아래로 이동
        scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN));
    });
}

📱 실제 앱 적용 사례

멀티플레이어 게임 구현

/**
 * 간단한 멀티플레이어 게임을 위한 데이터 구조
 */
public class GameData {
    private String playerName;
    private int playerScore;
    private String gameAction;
    
    // JSON으로 직렬화하여 전송
    public String toJson() {
        return new Gson().toJson(this);
    }
    
    public static GameData fromJson(String json) {
        return new Gson().fromJson(json, GameData.class);
    }
}

/**
 * 게임 데이터 전송
 */
private void sendGameAction(String action, int score) {
    GameData gameData = new GameData();
    gameData.playerName = Build.MODEL;
    gameData.playerScore = score;
    gameData.gameAction = action;
    
    sendMessage(gameData.toJson());
}

🎯 마무리 및 다음 단계

Nearby Connections API는 오프라인 환경에서도 강력한 기기 간 통신을 제공하는 훌륭한 도구입니다. 이 가이드를 통해 기본적인 구현부터 고급 기능까지 다뤄보았습니다.

핵심 포인트 요약:

  • ✅ 적절한 권한 설정이 성공의 열쇠
  • ✅ 연결 전략을 용도에 맞게 선택
  • ✅ 에러 처리를 통한 안정성 확보
  • ✅ 사용자 경험을 고려한 UI 설계
  • ✅ 배터리 최적화로 실용성 향상

추천 학습 경로:

  1. 기본 예제로 동작 원리 이해
  2. 파일 전송 기능 추가
  3. 보안 강화 및 인증 구현
  4. 실제 앱에 통합 및 최적화
  5. iOS와의 크로스 플랫폼 통신 구현
🚀 다음 프로젝트 아이디어
- 오프라인 채팅 앱
- 파일 공유 도구
- 멀티플레이어 보드게임
- 협업 화이트보드
- IoT 기기 제어 앱

Nearby Connections API의 무한한 가능성을 탐험하며 혁신적인 앱을 만들어보세요! 궁금한 점이나 더 자세한 구현 방법이 필요하다면 공식 문서를 참고하시기 바랍니다.

이 가이드가 도움이 되셨다면 ⭐를 눌러주세요!
더 많은 안드로이드 개발 팁은 블로그에서 만나보실 수 있습니다.

반응형