Jetpack Compose로 Google Map과 ARCore 연동하기: 카메라 방향 화살표 UI 만들기 🗺️ AR
미해결
최근 Jetpack Compose를 사용하여 안드로이드 앱을 개발하던 중 흥미로운 아이디어가 떠올랐습니다. 바로 Google Map 위에 AR(증강현실)을 오버레이하여, 내가 바라보는 방향을 화살표로 알려주는 기능이었죠. 이 포스트는 그 아이디어를 현실로 만들어가는 과정을 기록한 것입니다.
1단계: 목표 설정 및 현재 진행 상황
나의 목표: ARCore 예제를 참고해서 Google Map을 Compose로 보여주고, 그 위에 ARCore가 제시하는 정보를 활용해서 내가 바라보는 방향을 향해 화살표를 렌더링하는 UI를 구성하고 싶어. 현재는 Google Map에 나의 마지막 위치를 얻어서 표시하는 기능까지는 구현했어. 이번에 하고 싶은 것은 ARCore의 Session 정보를 활용해서 View를 하나 띄우고 카메라가 바라보는 방향으로 화살표가 가도록 만들어 보고 싶어.
목표는 명확했습니다. 이미 구현된 지도 위에 AR 뷰를 띄우고, ARCore 세션에서 얻은 카메라의 방향 값으로 3D 화살표를 실시간으로 움직이는 것이죠.
2단계: ARCore와 Jetpack Compose 통합하기
가장 먼저 부딪힌 문제는 '어떻게 Jetpack Compose 환경에서 AR 뷰를 자연스럽게 통합할 것인가?' 였습니다. 검색 결과, sceneview-android 라이브러리가 사실상의 표준이라는 것을 알게 되었습니다. 이 라이브러리는 ARCore와 3D 렌더링 엔진인 Filament를 Compose에서 쉽게 사용할 수 있도록 도와줍니다.
@Composable fun ArMapScreen() { val userLocation = LatLng(36.3504, 127.3845) // 대전시청 예시 val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(userLocation, 15f) }
Box(modifier = Modifier.fillMaxSize()) { // 1. Google Map (배경) GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Marker(state = rememberMarkerState(position = userLocation), title = "My Location") }
// 2. AR Scene (오버레이) ArDirectionOverlay() } }
@Composable fun ArDirectionOverlay() { val engine = rememberEngine() val modelLoader = rememberModelLoader(engine) val cameraNode = rememberARCameraNode(engine)
ARScene( modifier = Modifier.fillMaxSize(), nodes = listOf(cameraNode, arrowNode), planeRenderer = false, // 바닥 감지 평면 끄기 onFrame = { // 카메라 앞 1미터에 화살표 위치시키기 arrowNode.position = cameraNode.pose.transformPoint(floatArrayOf(0f, 0f, -1f)) // 화살표 방향을 카메라 방향과 일치시키기 arrowNode.rotation = cameraNode.rotation } ) }
3단계: 의존성 문제 해결하기
코드를 작성하고 의존성을 추가하는 과정에서 문제가 발생했습니다. 처음에는 com.google.ar:core만 추가하면 되는 줄 알았지만, sceneview-android 라이브러리가 필수적이라는 것을 깨달았습니다.
질문: 나는 이미 com.google.ar:core를 추가했는데, 따로 추가할 필요는 없는 건가?
이에 대한 답변은 명확했습니다. com.google.ar:core는 AR 기능의 핵심 '엔진'이고, sceneview-android는 이 엔진을 장착하여 Jetpack Compose에서 바로 운전할 수 있게 만든 '완성된 자동차'와 같다는 것입니다. sceneview-android를 사용하면 복잡한 렌더링과 UI 통합을 매우 쉽게 처리할 수 있습니다.
하지만 sceneview-android를 추가하자마자 더 큰 문제에 부딪혔습니다.
빌드 오류: Duplicate class ... found in modules ...
빌드 시 수많은 'Duplicate class' 오류가 발생했습니다. 오류 로그는 com.google.ar.sceneform.rendering.Color 같은 클래스가 서로 다른 두 라이브러리에서 중복으로 발견되었다고 알려주고 있었습니다.
Duplicate class com.google.ar.sceneform.rendering.Color found in modules rendering-1.17.1.aar (com.google.ar.sceneform:rendering:1.17.1) and sceneview-2.3.0.aar (io.github.sceneview:sceneview:2.3.0) ... (수많은 중복 클래스 오류)
원인 및 해결
원인은 명확했습니다. 제 프로젝트에 구글의 **오래된 Sceneform 라이브러리**와 새로운 SceneView 라이브러리가 동시에 포함되어 있었기 때문입니다. SceneView는 Sceneform의 업그레이드 버전이므로, 두 라이브러리 안에는 이름이 똑같은 클래스들이 가득했습니다.
해결 방법은 간단했습니다. build.gradle 파일에서 오래된 Sceneform 관련 의존성을 모두 제거하는 것이었습니다.
// build.gradle (Groovy)
dependencies { // ...
// 아래와 같은 오래된 Sceneform 의존성들을 모두 삭제! // implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1' // implementation 'com.google.ar.sceneform:rendering:1.17.1'
// 최신 SceneView 라이브러리만 남겨둡니다. implementation("io.github.sceneview:arsceneview:2.3.0")
// ... }
오래된 의존성을 제거하고 Gradle을 다시 동기화하자, 빌드 오류는 마법처럼 사라졌습니다.
결론
Jetpack Compose 환경에서 ARCore를 연동하는 것은 sceneview-android 라이브러리 덕분에 생각보다 어렵지 않았습니다. 특히 의존성 충돌 문제는 초기에 겪기 쉬운 함정이지만, 라이브러리 간의 관계만 잘 이해하면 쉽게 해결할 수 있었습니다. 이제 지도 위에 내가 바라보는 방향을 알려주는 AR 화살표가 성공적으로 나타납니다!
#billcorea #운동동아리관리앱
🏸 श्नीडल, बैडमिंटन क्लबों के लिए एक आवश्यक ऐप!
👉 मैच खेलें - स्कोर रिकॉर्ड करें और विरोधियों को खोजें 🎉
कहीं भी, अकेले, दोस्तों के साथ, या क्लब में बिल्कुल सही! 🤝
अगर आपको बैडमिंटन पसंद है तो इसे जरूर ट्राई करें
ऐप पर जाएं 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay
// 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);
/** * 메시지를 전송하는 메소드 */ 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); }); }
<!-- 메시지 입력 --> <EditText android:id="@+id/etMessage" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="보낼 메시지를 입력하세요" android:layout_marginTop="16dp" />
// 파일 정보를 먼저 전송 (파일명, 크기 등) 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);
📊 연결 전략 비교 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())); }
/** * 연결 상태를 시각적으로 표시하는 메소드 */ 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("⚪ 연결 대기중"); } }); }
Firebase Storage로 이미지 업로드하기 — 갤러리/카메라 선택 + 업로드 진행률 표시
적용해본 예시 이미지
이 글은 Jetpack Compose로 만든 안드로이드 앱에서 Firebase Storage에 이미지를 업로드하는 전 과정을 다룹니다. 사용자는 갤러리에서 이미지 선택 혹은 카메라로 촬영한 사진을 업로드할 수 있고, 업로드 진행률과 다운로드 URL을 바로 확인할 수 있습니다. 초보자분들도 그대로 따라 하면 동작하도록 전체 코드와 함께 단계별로 설명했습니다.
dependencies { // Firebase BOM: 버전 하나로 일괄 관리 implementation(platform("com.google.firebase:firebase-bom:33.1.2")) // KTX 사용 시 -ktx 종속성 권장 implementation("com.google.firebase:firebase-storage-ktx")
// Compose 필수 의존성 (버전은 프로젝트에 맞게 조정 가능) implementation("androidx.activity:activity-compose:1.9.0") implementation("androidx.compose.ui:ui:1.6.8") implementation("androidx.compose.material3:material3:1.2.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
// 이미지 미리보기(선택) implementation("io.coil-kt:coil-compose:2.6.0") }
Tip: Firebase BOM은 수시로 업데이트됩니다. 새 프로젝트에선 최신 BOM을 사용하세요.
@Composable fun ImagePickerWithCamera(onImageSelected: (Uri) -> Unit) { val context = LocalContext.current
// 카메라 촬영을 위한 임시 파일 & Uri val imageFile = remember { File(context.cacheDir, "temp_${System.currentTimeMillis()}.jpg") } val imageUri = FileProvider.getUriForFile( context, "${context.packageName}.provider", imageFile )
val cameraLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.TakePicture() ) { success -> if (success) onImageSelected(imageUri) }
val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri -> uri?.let(onImageSelected) }
fun uploadImageToFirebaseWithProgress( uri: Uri, onProgress: (Int) -> Unit, onResult: (String?) -> Unit ) { val storageRef = Firebase.storage.reference val fileRef = storageRef.child("uploads/${System.currentTimeMillis()}.jpg")
val uploadTask = fileRef.putFile(uri) uploadTask .addOnProgressListener { snap -> val p = (100.0 * snap.bytesTransferred / snap.totalByteCount).toInt() onProgress(p) } .addOnSuccessListener { fileRef.downloadUrl.addOnSuccessListener { url -> onResult(url.toString()) } } .addOnFailureListener { e -> e.printStackTrace() onResult(null) } }
초보자 포인트:
uploads/ 폴더 아래에 타임스탬프 기반 파일명을 사용해 중복을 피합니다.
성공 시 URL은 공유 가능한 다운로드 링크입니다. (규칙에 따라 접근 제한 가능)
6) 완성 화면(Compose) 구성
버튼 한 번으로 선택/촬영 후 업로드까지 이어지고, 이미지 미리보기와 진행률, 최종 URL을 보여주는 간단한 화면입니다.
@Composable fun UploadImageScreenWithCamera() { var imageUri by remember { mutableStateOf<Uri?>(null) } var uploadedUrl by remember { mutableStateOf<String?>(null) } var progress by remember { mutableStateOf(0) }
fun uploadImageToFirebaseWithProgress( uri: Uri, onProgress: (Int) -> Unit, onResult: (String?) -> Unit ) { val storageRef = Firebase.storage.reference val fileRef = storageRef.child("uploads/${System.currentTimeMillis()}.jpg")
@Composable fun UploadImageScreenWithCamera() { var imageUri by remember { mutableStateOf<Uri?>(null) } var uploadedUrl by remember { mutableStateOf<String?>(null) } var progress by remember { mutableStateOf(0) }
<주변검색 주변찾기 앱, 동작감지기 앱 : 이하 사용자앱으로 표시 >:는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.
○ 이 개인정보처리방침은 2022년 4월 1부터 적용됩니다.
제1조(개인정보의 처리 목적)
<사용자 앱>은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.
○ 제공되는 앱의 사용자 확인을 위해서 만 사용 됩니다.
제2조(개인정보의 처리 및 보유 기간)
① <사용자 앱>은(는) 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.
② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.
1.<앱 사용자 회원가입 및 관리>
<앱 사용자 회원가입 및 관리>와 관련한 개인정보는 수집.이용에 관한 동의일로부터<이 앱의 사용기간 동안>까지 위 이용목적을 위하여 보유.이용됩니다.
보유근거 : 이 앱의 사용자 확인을 위해서 사용 됩니다.
2 <앱 사용자 위치정보 : 백그라운드 위치 포함>
백그라운드 위치정보 : 이 앱의 기본 기능인 도착 알림 발송등을 위해서 백그라운드 에서 위치 정보를 활용 합니다. 사용자의 위치 정보는 앱의 기능 활용을 위해서만 사용 되며, 앱 외부로 전송 하지 않습니다.
3 <Health Data 의 사용>
이 앱은 사용자의 폰이나, 워치를 통해 사용자의 Health Data 가 측정 됩니다. 측정된 Health Data 는 이 앱에서 사용자의 신체상태을 표시 하는 데만 사용 됩니다.
측정된 Health Data 을 앱 이외의 공간(온라인전송 포함)으로 복사 되거나, 제공 되지 않습니다.
제3조(개인정보의 제3자 제공)
① <사용자 앱>은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.
② < billcorea >은(는) 다음과 같이 개인정보를 제3자에게 제공하지 않습니다.
제4조(개인정보처리 위탁)
① <사용자 앱>은(는) 원활한 개인정보 업무처리를 위하여 다음과 같이 개인정보 처리업무를 위탁하지 않습니다.
② 위탁업무의 내용이나 수탁자가 변경될 경우에는 지체없이 본 개인정보 처리방침을 통하여 공개하도록 하겠습니다.
제5조(정보주체와 법정대리인의 권리·의무 및 그 행사방법)
① 정보주체는 우리집에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.
② 제1항에 따른 권리 행사는우리집에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 우리집은(는) 이에 대해 지체 없이 조치하겠습니다.
③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 “개인정보 처리 방법에 관한 고시(제2020-7호)” 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.
④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.
⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.
⑥ billcorea 은(는) 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.
제6조(처리하는 개인정보의 항목 작성)
①<사용자 앱>은(는) 다음의 개인정보 항목을 처리하고 있습니다.
1<앱 사용자 회원가입 및 관리 >
필수항목 : 식별기호(uuid token), 이메일주소,
소셜 로그인 : 이메일주소, 별명(별칭), 프로필 이미지 링크
2<앱 사용자 위치 정보>
앱 사용중 위치 정보 및 백그라운드 위치 정보 포함.
3<Health Data>
앱 사용중 사용자의 심박수, 걸음수의 정보.
제7조(개인정보의 파기)
① <사용자 앱> 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.
② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.
1. 법령 근거 : 관련법규 적용 사항 없음
2. 보존하는 개인정보 항목 : 없음
③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.
1. 파기절차 <사용자 앱>은(는) 파기 사유가 발생한 개인정보를 선정하고, < billcorea > 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.
2. 앱의 사용자 설정에서 로그인 관련 정보 삭제 버튼을 이용하여 삭제 할 수 있습니다.
제8조(개인정보의 안전성 확보 조치)
<사용자 앱>은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.
1. 내부관리계획의 수립 및 시행
개인정보의 안전한 처리를 위하여 내부관리계획을 수립하고 시행하고 있습니다.
2. 개인정보에 대한 접근 제한
개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.
3. 비인가자에 대한 출입 통제
개인정보를 보관하고 있는 물리적 보관 장소를 별도로 두고 이에 대해 출입통제 절차를 수립, 운영하고 있습니다.
제9조(개인정보 자동 수집 장치의 설치•운영 및 거부에 관한 사항)
<사용자 앱> 은(는) 정보주체의 이용정보를 저장하고 수시로 불러오는 ‘쿠키(cookie)’를 사용하지 않습니다.
제10조 (개인정보 보호책임자)
① <사용자 앱> 은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.
▶ 개인정보 보호책임자
성명 :강동엽
직책 : manager
직급 : manager
연락처 : 0504-0662-8122, help@billcorea.com
※ 개인정보 보호 담당부서로 연결됩니다.
▶ 개인정보 보호 담당부서
부서명 : manager
담당자 : 강동엽
연락처 : 0504-0662-8122, help@billcorea.com
② 정보주체께서는 우리집 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. 우리집 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.
제11조(개인정보 열람청구) 정보주체는 「개인정보 보호법」 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
<사용자 앱>은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.
▶ 개인정보 열람청구 접수·처리 부서
부서명 : manager
담당자 : 강동엽
연락처 : 0504-0662-8122, help@billcorea.com
제12조(권익침해 구제방법)
정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.
「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정·삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.
※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(www.simpan.go.kr) 홈페이지를 참고하시기 바랍니다.
이번 YouTube 영상 "AI Breakfast 개발자 특별편 | Episode 3 - Android 앱 개발의 미래"는 구글 I/O 2023에서 공개된 AI 기술이 안드로이드 앱 개발의 **기획, 개발, 배포, 운영 등 모든 단계를 어떻게 변화시키고 있는지**에 대해 심도 깊은 논의를 진행합니다 [1]. AI가 단순한 도구를 넘어 개발자들의 워크플로우 전반에 큰 변화를 가져오고 있으며, 개발자 역할의 재정의 가능성도 언급되었습니다 [2].
**AI 기술이 안드로이드 앱 개발 각 단계에 미치는 영향:**
* **기획 및 UI/UX 디자인 단계**:
* **Stitch(스티치)**: 텍스트나 이미지 입력만으로 AI가 UI 디자인과 코드를 자동으로 생성해주는 도구로 소개되었습니다 [2]. 이 도구는 개발자가 디자인을 할 수 있게 돕고, 기획자나 디자이너의 반복적인 레이아웃 작업 부담을 줄여주며, 개발자와 디자이너/기획자 간의 **효율적인 협업을 가능하게 합니다** [3]. 데모 버전 제작 시에도 디자인 퀄리티를 크게 높여주는 장점이 있습니다 [3].
* **코딩 단계**:
* **AI 코딩 어시스턴트**: 안드로이드 앱 개발은 웹 개발에 비해 AI 도구 적용이 다소 늦었지만, 이제 구글의 대표적인 안드로이드 개발 툴인 **안드로이드 스튜디오에 Gemini(제미나이)가 공식적으로 통합**되면서 개발자들이 AI의 도움을 적극적으로 받을 수 있게 되었습니다 [3].
* **높은 AI 코드 기여도**: 현재 현업에서는 코딩 시 AI의 기여도가 **벌써 50%에 달하는 사례**가 언급되었으며, 이는 거의 동료와 같은 협업 수준으로 평가됩니다 [4]. AI는 변수명 짓기 등 단순 반복 작업을 대신하여 개발자가 **더 큰 그림과 창의적인 부분에 집중**할 수 있도록 돕습니다 [4].
* **안드로이드 개발의 숙제 해결**: AI는 특히 안드로이드 개발자들이 어려움을 겪는 **다양한 화면 크기 및 OS 버전 대응, 구글 플레이 정책 준수**와 같은 '영원한 숙제'들을 해결하는 데 큰 도움을 줄 것으로 기대됩니다 [4, 5].
* **AI 모델 및 온디바이스 AI**:
* **Gemini(제미나이) vs. Gemma(잼마)**:
* **Gemini**: 구글의 대표적인 거대 AI 모델이며, API를 통해 활용할 수 있는 PaaS(Platform as a Service) 형태로 제공됩니다 [6].
* **Gemma**: Gemini의 내용을 경량화한 작은 모델로, 사용자가 직접 가져다가 자신에게 맞게 변경하고 학습시킬 수 있습니다 [6]. 디바이스에서도 원활하게 동작하며, 특히 **온디바이스 AI에 최적화된 폼팩터**로 주목받고 있습니다 [6].
* **온디바이스 AI의 장점**:
* **개인 정보 보호**: 민감한 사용자 정보가 디바이스 내에서만 머물러 클라우드로 나가지 않아 보안에 강합니다 [7].
* **오프라인 사용 가능**: 인터넷 연결 없이도 기능 구현이 가능합니다 [7].
* **빠른 응답 속도**: 클라우드 통신 지연 없이 빠른 응답이 가능합니다 [7].
* **비용 절감**: 추가 비용 없이 디바이스 내에서 처리되어 전기 요금만으로 해결됩니다 [7].
* **활용 사례**: 카카오T 데모에서 Gemini Nano 모델을 활용하여 사용자의 자연어 주문을 분석하고 주소 등 정보를 추출하는 기능이 소개되었으며 [7], 개인 상담사처럼 **사용자의 민감한 정보를 보호하며 파인 튜닝이 가능한 온디바이스 모델**(Gemma 1B) 활용 사례도 제시되었습니다 [7].
* **API 활용**:
* **Gen AI SDK**: Gemini API 사용 시 **Gen AI SDK를 반드시 사용할 것을 권장**합니다 [8]. 이는 개발자용/운영용 API 구분을 없애고 플래그 변경만으로 보안 강화를 할 수 있게 하여 코드 변경 없이 개발부터 운영까지 자연스럽게 보안을 적용할 수 있도록 합니다 [8].
* **Vertex AI**: API 키 방식의 한계(관리 어려움, 유출 위험)를 넘어, **강화된 보안과 특정 클라이언트/IP 제약 등 기업 환경에서의 요구사항을 충족시키기 위해 Vertex AI를 함께 활용**할 수 있습니다 [8].
* **데이터 보안**: API 호출 시 개인 데이터가 서버로 그대로 넘어가는 것이 아니라, 머신러닝의 첫 레이어를 거쳐 **웨이트 값만 전달되므로 개인 정보 유출에 대한 우려를 덜 수 있습니다** [9].
* **배포 및 운영 단계**:
* AI는 앱 배포 시 **구글 플레이 정책 준수**에 대한 가이드를 제공하고 [10], 배포 후 발생하는 오류 보고서를 분석하여 **자동으로 코드를 수정해주는 가능성**을 제시합니다 [10]. 이는 개발자의 운영 부담을 크게 줄여줄 것으로 기대됩니다 [10].
**안드로이드 개발의 미래 및 개발자의 역할**:
* **새로운 기회와 생산성 향상**: AI는 개발자에게 **새로운 장난감**과 같으며, 생산성 향상을 바탕으로 **더욱 높은 품질의, 더 멋진 사용자 경험을 제공하는 앱**을 만들 수 있게 합니다 [10]. XR(확장 현실)과 같은 새로운 분야에서도 개발자들이 할 일이 훨씬 늘어날 것으로 예상됩니다 [10].
* **AI 에이전트 앱**: AI를 활용하여 앱 내에서 사용자에게 화면 설명, 쇼핑 추천 등 **다양한 기능을 제공하는 AI 에이전트 앱** 개발이 활성화될 것으로 보이며 [11], 구글에서는 에이전트 개발을 용이하게 하는 **Agent Development Kit(ADK)**를 제공합니다 [11]. 안드로이드는 다양한 앱 간 상호작용을 염두에 둔 플랫폼 설계(인텐트 개념 등)로 **에이전트 시대에 이미 준비된 플랫폼**으로 평가됩니다 [12].
* **생성형 AI의 확장**: 동영상 생성, 이미지 생성 등 **생성형 AI API를 활용하여 기존에 상상하기 어려웠던 새로운 사용자 경험**을 제공하는 앱이 등장할 수 있습니다 [12]. 안드로이드 피규어에 옷을 입혀주는 '안드로이드파이'와 같은 쇼케이스 앱이 그 예시입니다 [12].
* **개발자 역량 강화**:
* **주니어 개발자**: AI 시대에는 학생들이나 주니어 개발자들이 **최고 레벨 회사들의 기술을 바로 접하고 활용할 수 있는 기회**가 열렸습니다 [13]. 새로운 툴 학습에 민첩하고 호기심 많은 주니어들이 유리할 수 있습니다 [13].
* **시니어 개발자**: AI가 90%의 코드를 생성하더라도, **10%의 결함을 찾아내고 노하우를 발휘하는 것은 시니어의 역할**이므로, 주니어와 시니어의 협업이 가장 아름다운 결과를 만들 것이라 예상됩니다 [13].
* **무한한 가능성**: 안드로이드는 스마트폰, 태블릿, 시계뿐 아니라 키오스크 등 생활 속 곳곳에 숨어있는 **다양한 폼팩터에서 AI의 힘을 빌려 새로운 경험을 펼칠 수 있는 무한한 가능성**을 가지고 있습니다 [13]. AI는 개발자들의 '꿈을 꾸지 못하던 저 너머의 세계'를 현실로 만들 수 있게 합니다 [10, 13].
@Destination @Composable fun NewPostScreen(navigator: DestinationsNavigator) { var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = title, onValueChange = { title = it }, label = { Text("제목") }, colors = TextFieldDefaults.outlinedTextFieldColors( textColor = Color.Black ) ) Spacer(Modifier.height(8.dp)) OutlinedTextField( value = content, onValueChange = { content = it }, label = { Text("내용") } ) Spacer(Modifier.height(16.dp)) Button(onClick = { // Firebase에 저장 navigator.popBackStack() }) { Text("등록") } } }
4. 게시글 수정
수정 시, postId를 인자로 전달해 해당 글을 로딩하고 수정합니다.
@Destination @Composable fun EditPostScreen(postId: String, navigator: DestinationsNavigator) { val viewModel: PostViewModel = hiltViewModel() val post = viewModel.getPost(postId)
// 수정 UI 구성 }
5. Compose Destinations 구성
@Composable fun MainNavHost() { val navController = rememberNavController() DestinationsNavHost( navController = navController, navGraph = NavGraphs.root ) }
#스하리1000명프로젝트,
Parfois, il est difficile de parler avec des travailleurs étrangers, n'est-ce pas ?
J'ai créé une application simple qui aide ! Vous écrivez dans votre langue et les autres le voient dans la leur.
Il se traduit automatiquement en fonction des paramètres.
Super pratique pour des discussions faciles. Jetez-y un oeil quand vous en aurez l'occasion !
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416