[만보기 어플 만들기] Kotlin으로 만보기 앱 개발하는 방법
고객이 어플을 계속해서 사용하도록 어플에 여러 기능들을 추가해 어플 사용량을 늘리고 어플 친숙도를 높여 충성 고객을 확보하도록 목적을 가진 어플이 있습니다. 바로 ‘슈퍼 앱’입니다.
슈퍼 앱은 하나의 어플의 다수의 기능을 탑재함으로써 마치 복합 쇼핑몰에 들어온 것처럼 고객을 사로잡는 ‘락인(Lock in)’ 기능을 가지고 있는데요. 이 기능을 강화하기 위해, 네이버, 카카오 등 빅 테크 기업부터 금융 IT 기업까지 ‘만보기’ 어플을 활용하고 있습니다.
어플 친숙도와 사용도를 높여 충성고객 확보를 도와주는 만보기 어플, 대한민국 No.1 IT 인재 매칭 플랫폼 이랜서에서 안드로이드 대표 언어 ‘Kotlin’으로 개발하는 방법을 알려드리겠습니다.
만보기 기능, 빅 테크 기업들은 이래서 사용합니다.
사람들은 자신이 사용하기 익숙한 어플을 찾게 됩니다. 아이폰을 사용한 사람은 계속 아이폰을 찾게 되고 갤럭시를 사용한 사람은 계속 갤럭시를 찾는 것처럼요. 그래서 모바일 시장에선 소비자가 사용하기에 친숙한 어플을 만들기 위해 여러 가지 기능들을 추가하는데요. 대표적인 것이 바로 ‘만보기 기능’입니다.
빅 테크나 금융 IT 기업들은 어플에 만보기 기능을 추가해 고객이 계속해서 사용하도록 유도하고 있습니다. 만보기 어플은 건강과 관련된 헬스케어에 대한 데이터도 수집할 수 있어, 어플 친숙도와 미래 성장 산업을 위한 데이터란 두마리 토끼를 모두 잡을 수 있게 됩니다.
고객을 사로잡는 만보기 어플 만드는 방법
이로 인해 네이버, 카카오, 토스 등 잘나가는 테크 기업과 금융 IT 기업들은 ‘만보기 어플’을 추가해 충성고객을 확보하고 있는데요. 이랜서에서 Kotlin을 활용하여 만보기 어플 만드는 방법을 알려드리겠습니다. 만보기 어플 개발 방법이 궁금한 분들은 끝까지 집중하세요.
안드로이드에서 어플 개발을 위해 윈도우 노트북에서 IDE는 안드로이드 스튜디오, 언어는 코틀린을 사용했습니다.
만보기 어플 레이아웃 만들기
만보기 어플을 만들기 위해서, 먼저 레이아웃을 만들어야 합니다.
이랜서에서는 IT 전문가의 체력 향상과 개인 성장을 돕는 달리기 커뮤니티 ‘ERC(Elancer Run Club)’를 주최해 IT 전문가의 자기 계발(커리어 개발)을 돕고 있는데요. 이랜서 런클럽 ERC 로고를 활용해 만보기 어플 레이아웃을 만들어 보겠습니다.
레이아웃은 Jetpack Compose를 사용하여 만들었습니다. Jetpack Composet는 UI 개발을 간소화하기 위해 설계된 최신 툴킷으로 동적인 UI를 구현하는데 장점이 있습니다.
override fun onCreate(savedInstanceState: Bundle?) { |
@Composable |
폰트는 ‘원스토어 mobile pop 서체’를 사용했습니다. 폰트를 사용하기 위해 customFontFamily를 지정했습니다. Jetpack Compose가 익숙하지 않은 분은 기존처럼 layout 파일을 사용하여 구현하셔도 됩니다.
만보기 어플 기능 구현하기
만보기 어플을 만들기 위해 레이아웃을 설정했으니, 이제 만보기 기능을 추가해보겠습니다. 사용자의 걷는 동작을 감지하기 위해 SensorManager 클래스를 사용했습니다.
퍼미션(Permission) 추가
SensorManager를 사용하려면 퍼미션을 추가해야 합니다. 퍼미션이란 특정 파일이나 디렉터리에 읽기, 쓰기, 삭제 등의 권한을 설정해 놓은 것으로 다중 사용자 운영체제에서 파일에 대한 접근 권한과 보호를 위해 반드시 필요합니다. 다중이 사용하는 만보기 어플을 만들기 위해선 반드시 퍼미션을 추가해야 합니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<application |
AndroidManifest 파일을 열고 ACTIVITY_RECOGNITION 퍼미션을 선언하세요.
센서 셋업
private var sensorManager: SensorManager? = null
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
sensorManager = getSystemService(SensorManager::class.java) |
override fun onResume() { super.onResume()
val sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
} |
옵션으로 넘기는 TYPE_STEP_COUNTER는 리턴 값이 무조건 1이고 앱이 종료되면 다시 0부터 시작합니다. TYPE_STEP_SCOUNTER를 지정하면 앱 종료와 관계없이 계속 기존의 값을 가지고 있다가 1씩 증가한 값을 리턴합니다.
리셋 버튼 구현
만보기의 순수 기능을 구현하기 위해 Reset 버튼을 추가했습니다. 기록을 측정하는 기록 장치를 보면 Reset 버튼이 있는데요. 걸음수를 초기화 할 수 있게 Reset 버튼을 누르면 걸음 수가 초기화되는 기능을 추가해 보겠습니다.
Box( modifier = Modifier .clickable(onClick = { _stepValue.value = "0" }) .padding(8.dp) .width(100.dp) .height(50.dp) .background(Color(0xFFFF6F00), shape = RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center ) { // Additional content goes here Text( text = "Reset", fontSize = 30.sp, fontWeight = FontWeight.Bold, fontFamily = customFontFamily, color = Color(0xFFFFFFFF), ) } |
센서 딜레이 세팅
센서 딜레이 세팅은 센서를 통해 데이터를 가져오는 속도를 조절하는 것입니다. 너무 빠른 속도로 데이터를 가져오게 될 경우 베터리나 시스템 자원이 낭비할 수 있어, 적절한 속도로 설정하는 것이 중요합니다.
만보기 어플의 경우에는 SENSOR_DELAY_UI를 사용하면 적당하게 조절할 수 있습니다.
override fun onResume() { super.onResume() isRunning = true val sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
sensor?.let { sensorManager?.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI) } } } |
센서 속도를 설정합니다. 빠른 속도 순서는 SENSOR_DELAY_FASTEST, SENSOR_DELAY_GAME, SENSOR_DELAY_UI, SENSOR_DELAY_NORMAL입니다.
메인 기능
이제 센서로 데이터를 받으면 화면에 표시하는 메인 기능을 구현하겠습니다. 이 기능은 만보기 어플의 핵심으로 센서를 통해 받은 데이터를 실시간으로 화면에 표시하는 수 역할을 합니다.
class MainActivity : ComponentActivity(), SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) { if (event != null) { val isIncrementStep = abs(event.values[0].toInt() - prevStep) prevStep = event.values?.get(0)?.toInt()!! log("abs(event.values[0].toInt() - totalStep): $isIncrementStep :: ${isIncrementStep == 1}")
if (isRunning && isIncrementStep == 1) { _stepValue.value = (++stepCount).toString() } } }
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} |
SensorEventListener를 MainActivity에 연동한 다음 onSensorChanged, onAccuracyChanged 메소드를 구현합니다.
onSensorChanged: 실제 센서의 작동과 관련된 함수입니다. 센서가 동작을 감지하면 onSensorChanged로 값이 넘어옵니다.
onAccuracyChanged: 센서의 정확도가 변경되었을 때 호출됩니다. 이 함수는 필요한 경우 구현하세요.
만보기 어플 기능을 추가한 전체 코드
만보기 어플 구현을 위해 전체 코드를 보여드리겠습니다. 아래 코드대로 입력만 해도 만보기 기능을 구현할 수 있습니다.
package com.nokhyun.sensorexam
import android.Manifest import android.content.pm.PackageManager import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role.Companion.Image import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import com.nokhyun.sensorexam.ui.theme.SensorExamTheme import kotlin.math.abs
class MainActivity : ComponentActivity(), SensorEventListener {
private var sensorManager: SensorManager? = null private var isRunning = false private var prevStep = 0 private var stepCount = 0
private var _stepValue: MutableState<String> = mutableStateOf("0") var stepValue: State<String> = _stepValue
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
if (ContextCompat.checkSelfPermission( this, Manifest.permission.ACTIVITY_RECOGNITION ) == PackageManager.PERMISSION_DENIED ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { requestPermissions(arrayOf(Manifest.permission.ACTIVITY_RECOGNITION), 1001) } }
sensorManager = getSystemService(SensorManager::class.java)
setContent { SensorExamTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { StepCounterUI() } } } }
val customFontFamily = FontFamily( Font(R.font.one_mobile_pop_otf, FontWeight.Normal), // Add other font variations if necessary )
@Composable fun StepCounterUI() { Box( modifier = Modifier .fillMaxSize() .background(Color.White), contentAlignment = Alignment.TopCenter ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { Box( modifier = Modifier .height(30.dp) .fillMaxWidth() .background(Color(0xFFFF6F00)) ) Box( modifier = Modifier .height(15.dp) .fillMaxWidth() .background(Color(0xFF595959)) )
Row( modifier = Modifier .fillMaxWidth() .padding(top = 30.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Text( text = "이랜서", fontSize = 35.sp, fontWeight = FontWeight.Bold, fontFamily = customFontFamily, color = Color.Black ) Spacer(modifier = Modifier.width(10.dp)) // Add some space between the texts Text( text = "LEARN & RUN", fontSize = 35.sp, fontWeight = FontWeight.Bold, fontFamily = customFontFamily, color = Color(0xFFFF6F00) ) }
Text( text = "'배우면서 달린다. 달리면서 배운다'", fontSize = 24.sp, fontFamily = customFontFamily, color = Color(0xFF797979) )
// Image val image: Painter = painterResource(id = R.drawable.logo) Image( painter = image, contentDescription = "Logo", contentScale = ContentScale.FillWidth, modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp) )
Row( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Text( text = stepValue.value, Modifier.width(220.dp), fontSize = 60.sp, textAlign = TextAlign.End, fontWeight = FontWeight.Bold, fontFamily = customFontFamily, color = Color(0xFFFF6F00) )
Spacer(modifier = Modifier.width(20.dp)) // Add some space between the texts Text( text = "걸음", fontSize = 40.sp, fontWeight = FontWeight.Bold, fontFamily = customFontFamily, ) Spacer(modifier = Modifier.width(10.dp))
}
Box( modifier = Modifier .height(3.dp) .padding(horizontal = 35.dp) .fillMaxSize() .background(Color(0xFF000000))
)
Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 35.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = "총 7km", fontSize = 25.sp, fontFamily = customFontFamily, color = Color.Black ) Spacer(modifier = Modifier.width(5.dp)) // Add some space between the texts Text( text = "걸음", fontSize = 25.sp, fontFamily = customFontFamily, color = Color(0xFFcacaca) ) Spacer(modifier = Modifier.weight(1f)) Box( modifier = Modifier .clickable(onClick = { _stepValue.value = "0" }) .padding(8.dp) .width(100.dp) .height(50.dp) .background(Color(0xFFFF6F00), shape = RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center ) { // Additional content goes here Text( text = "Reset", fontSize = 30.sp, fontWeight = FontWeight.Bold, fontFamily = customFontFamily, color = Color(0xFFFFFFFF), ) } }
Spacer(modifier = Modifier.height(20.dp))
// Placeholder for additional content Box( modifier = Modifier .padding(16.dp) .fillMaxWidth() .height(100.dp) .background(Color(0xFFcacaca), shape = RoundedCornerShape(8.dp))
) { // Additional content goes here }
Spacer(modifier = Modifier.weight(1f))
Box( modifier = Modifier .height(15.dp) .fillMaxWidth() .background(Color(0xFF595959)) )
Box( modifier = Modifier .height(30.dp) .fillMaxWidth() .background(Color(0xFFFF6F00)) )
} } }
override fun onResume() { super.onResume() isRunning = true val sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
sensor?.let { sensorManager?.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI) } }
override fun onPause() { super.onPause() isRunning = false }
override fun onSensorChanged(event: SensorEvent?) { if (event != null) { val isIncrementStep = abs(event.values[0].toInt() - prevStep) prevStep = event.values?.get(0)?.toInt()!! log("abs(event.values[0].toInt() - totalStep): $isIncrementStep :: ${isIncrementStep == 1}")
if (isRunning && isIncrementStep == 1) { _stepValue.value = (++stepCount).toString() } } }
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
private fun log(msg: String) { Log.e(this.javaClass.simpleName, msg) } } |
만보기 어플 개발 결과
고객을 사로잡아 충성고객을 만드는 슈퍼 앱의 필수 기능 ‘만보기 어플’을 만들어 봤습니다. 걸음 수에 따라 숫자가 올라가는 만보기 어플이 완성된 것을 볼 수 있습니다.
한 걸음 걸었을 때의 보폭 너비를 약 70cm로 잡았을 때 만보를 걸으면 7km를 걷게 되어, 총 7km 걸음이란 추가했습니다. 그리고 리셋 버튼까지 두어 매일 걸은 걸음을 기록하고 확인할 수 있습니다. 만보기 기능에 충실한 어플입니다.
어딘가 심심한 만보기 어플,
기능 및 디자인 추가해 보았습니다.
요즘 빅 테크 기업은 고객에게 혜택을 주는 다양한 기능을 추가해 만보기 어플 사용을 촉진하고 있습니다. 빅 테크 기업을 따라 기능들을 추가하여 디자인을 해 보겠습니다.
고객들이 만보기 기능을 지속적으로 사용하도록 목표 설정과 기록 보기 버튼을 추가했습니다. 목표 설정과 기록이 있을 경우 고객의 사용 빈도는 이전보다 높아지게 됩니다. 그리고 다중의 사람들과 함께 만보기 기능을 즐길 수 있도록 등급 확인 및 초대하기 및 포인트 받기 버튼을 추가했습니다.
빅 테크 기업 및 금융 IT 기업들은 만보기 어플 외에도 다양한 기능을 추가하여 어플 사용도를 높여 어플 친숙도를 높이고 있습니다. 이를 통해 충성고객을 확보하여 메인 서비스의 사용 확률도 높이고 있는데요.
잘나가는 빅 테크 기업들은 다양한 기능을 활용해
충성고객을 사로잡고 있습니다.
온라인 비즈니스를 준비하는 기업 특히 모바일 시장에 진출을 앞둔 기업이라면 만보기 어플과 같은 락인 효과를 가진 슈퍼 앱을 준비한다면, 충성고객 확보와 더불어 성공적인 비즈니스 결과를 얻을 수 있게 될 것입니다.
이랜서 추천! 충성고객 확보를 위한 필승 공략 시리즈
▶️ 랜딩 페이지, 고객을 사로잡으려면 '이렇게' 만드세요
이랜서 추천! 알아두면 업무 효율이 높아지는 업무 효율 시리즈
▶️ Chat GPT 엔지니어'가 사용하는 프롬프트 작성법
▶️ 코파일럿, 마이크로소프트 365의 AI 서비스가 일으킨 사무실 대혁명
경쟁이 치열해지는 모바일 비즈니스 시장,
이랜서와 함께라면 걱정 없습니다.
이랜서는 데이터로 검증된 IT 전문가를 매칭해주는 대한민국 최대 IT 인재 매칭 플랫폼입니다.
모바일 앱 개발을 위한 Java, Kotlin, Swift, Xamarin 전문가와 플러터(Flutter), React Native, .Net을 활용하는 크로스 플랫폼 전문가, 서버 개발과 관리를 위한 JavaScript, Typescript, Node js, Spring, .NET 프레임워크을 활용하는 백엔드 개발자와 데이터 수집 & 활용을 위한 SQL 전문가(Oracle, Mysql, MS SQL 등) 그리고 고객 여정 지도와 수준 높은 UX 디자인을 구사하여 충성고객 확보를 도울 서비스 기획자와 UX/UI 전문 디자이너까지 약 40만 명의 IT 전문 프리랜서가 파트너십으로 등록되어 있습니다.
“다른 회사에 비해서 추천 인력의 퀄리티가 높습니다.”
이랜서는 이랜서만의 체계적인 프로세스와 DB를 활용하여 IT 전문가를 매칭합니다. 이랜서를 활용하는 기업들은 다른 플랫폼에서는 찾을 수 없는 퀄리티 높은 IT 전문가를 매칭 받고 있습니다.
개발부터 디자인, 기획, 유지보수까지
맞춤형 매칭 서비스로 필요한 IT 전문가를 빠르게 매칭합니다.
이랜서는 기업들이 IT 전문가 채용 시 겪는 어려움을 해결하기 위해, 약 1.5억 개의 서비스데이터와 350만 개의 프리랜서 평가 데이터를 활용하여, 전문성부터 인성(협업 능력)까지 철저하게 확인하여 프로젝트에 가장 적합한 IT 전문가를 매칭합니다.
또한 프로젝트 등록 시 1:1로 매니저를 배정하여, 프로젝트 등록 24시간 이내 프로젝트에 가장 적합한 IT 전문가를 데이터로 검증하여 매칭합니다.
전문성과 인성이 데이터로 모두 검증된 자바 개발자를 찾으시나요?