Android를 실무 흐름으로 이해하기
처음 Android Studio를 여는 순간부터 Play 스토어에 앱을 올리는 순간까지, Kotlin과 Jetpack Compose로 안드로이드 앱을 만드는 전 과정을 하나하나 친절하게 설명합니다. 왜 이렇게 하는지 이유까지 함께 이해할 수 있도록 구성했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
처음 Android Studio를 여는 순간부터 Play 스토어에 앱을 올리는 순간까지, Kotlin과 Jetpack Compose로 안드로이드 앱을 만드는 전 과정을 하나하나 친절하게 설명합니다. 왜 이렇게 하는지 이유까지 함께 이해할 수 있도록 구성했습니다.
처음 Android Studio를 여는 순간부터 Play 스토어에 앱을 올리는 순간까지, Kotlin과 Jetpack Compose로 안드로이드 앱을 만드는 전 과정을 하나하나 친절하게 설명합니다. 왜 이렇게 하는지 이유까지 함께 이해할 수 있도록 구성했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
화면을 그리는 법에서 멈추지 않고, 상태, 데이터 요청, 라우팅, 접근성, 배포 단위까지 함께 봅니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 Android를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
Android를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
| 비교 대상 | 예전 방식 | 이 가이드에서 다루는 방식 |
|---|---|---|
| UI 작성 | XML 레이아웃 파일 | Kotlin @Composable 함수 |
| 언어 | Java | Kotlin (더 간결하고 Null 안전) |
| 상태 반영 | findViewById로 수동 갱신 | 상태가 바뀌면 자동으로 재구성(recomposition) |
| 공식 권장 여부 | 유지보수 대상 | Google 공식 최신 표준 (2024~) |
여기서는 개발 환경 구축을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# Android Studio 설치 후 최초 실행 시 "Setup Wizard"가 뜹니다.
# Standard 옵션을 선택하면 Android SDK, 빌드 도구, 에뮬레이터 이미지가
# 자동으로 다운로드됩니다. (인터넷 속도에 따라 10~20분 정도 소요)
# 설치가 끝났는지 확인하려면 상단 메뉴에서:
# Android Studio > Settings > Languages & Frameworks > Android SDK
# 여기서 최신 SDK Platform (API 34 이상)이 체크되어 있는지 확인하세요.| 운영체제 | 설치 방법 | 참고사항 |
|---|---|---|
| Windows | 공식 사이트에서 .exe 인스톨러 다운로드 후 실행 | 설치 중 "Android Virtual Device" 항목 체크 유지 |
| macOS | 공식 사이트에서 .dmg 다운로드 (Apple Silicon/Intel 버전 확인) | M1/M2/M3 칩이면 반드시 Apple Silicon용 다운로드 |
| Linux | 공식 사이트에서 .tar.gz 압축 해제 후 studio.sh 실행 | 64비트 배포판 권장, KVM 가상화 활성화 필요 |
여기서는 첫 프로젝트 만들기을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# 프로젝트 생성이 끝나면 상단 툴바의 초록색 ▶ (Run) 버튼을 누르세요.
# 연결된 실제 기기가 없다면 Android Studio가 에뮬레이터를 자동으로
# 실행하고, 잠시 후 화면에 "Hello Android!" 문구가 뜨는 첫 앱을 볼 수 있습니다.
# 만약 에뮬레이터 목록이 비어있다면:
# 우측 Device Manager 아이콘 클릭 → Create Device → Pixel 8 선택
# → 시스템 이미지(API 34 등) 다운로드 → Finish| 설정 항목 | 의미 | 초보자 추천값 |
|---|---|---|
| Name | 앱 이름 (사람이 보는 이름) | 자유롭게 (예: MyFirstApp) |
| Package name | 앱의 전 세계 고유 식별자 | com.example.myfirstapp 같은 역도메인 형식 |
| Save location | 프로젝트 파일이 저장될 폴더 | 기본값 그대로 사용 |
| Language | 사용할 프로그래밍 언어 | Kotlin (기본값) |
| Minimum SDK | 앱이 지원할 가장 오래된 안드로이드 버전 | API 24 (Android 7.0) — 실무에서 널리 쓰이는 기준선 |
프로젝트 구조 이해은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 경로 | 이게 뭔가요? | 언제 여나요? |
|---|---|---|
| app/src/main/java/.../MainActivity.kt | 앱이 처음 실행될 때 호출되는 진입점 코드 | 거의 항상 — 여기서 Composable을 연결합니다 |
| app/src/main/res/ | 이미지, 아이콘, 문자열, 색상 같은 리소스 모음 | 이미지나 앱 이름을 바꿀 때 |
| app/src/main/AndroidManifest.xml | 앱의 이름, 권한, 화면 목록을 선언하는 설정 파일 | 카메라 등 권한이 필요할 때 |
| app/build.gradle.kts | 이 앱 모듈에서 쓸 라이브러리(의존성)와 빌드 설정 | 새 라이브러리를 추가할 때 — 가장 자주 여는 파일 |
| build.gradle.kts (프로젝트 최상단) | 프로젝트 전체에 적용되는 공통 빌드 설정 | 거의 열 일 없음 |
여기서는 Compose 첫걸음: Composable 함수을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@Composable 어노테이션이 붙은 일반 Kotlin 함수로 만들어집니다. 특별한 문법이 아니라 그냥 함수이기 때문에, 다른 Composable을 호출해서 조합하고, 매개변수를 받아 재사용할 수 있습니다.Column은 자식들을 세로로 쌓는 레이아웃, Text는 글자를 표시, Button은 누를 수 있는 버튼입니다. MainActivity의 setContent { } 블록 안에서 우리가 만든 Composable을 호출하면 화면에 그려집니다.import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
// @Composable: "이 함수는 화면을 그리는 함수입니다" 라는 표시
@Composable
fun Greeting(name: String) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "안녕하세요, $name!")
Button(onClick = { /* 버튼을 누르면 실행할 코드 */ }) {
Text("눌러보세요")
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContent 안에서 Composable을 호출하면 화면에 그려집니다
setContent {
Greeting(name = "AI DevOps")
}
}
}여기서는 레이아웃 & Modifier을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@Composable
fun ProfileCard() {
Row(
modifier = Modifier
.fillMaxWidth() // 가로 폭을 화면 끝까지 채움
.padding(16.dp), // 바깥 여백 16dp
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text("홍길동")
Text("Android 개발자")
}
Spacer(modifier = Modifier.weight(1f)) // 남은 공간을 모두 차지 (오른쪽 정렬 효과)
Text("팔로우")
}
}여기서는 상태 관리: remember와 mutableStateOf을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
remember는 Composable 함수가 다시 실행되어도(recomposition) 값을 잃어버리지 않게 "기억"해주는 역할이고, mutableStateOf는 그 값이 바뀔 때 Compose에게 "이 값을 읽는 UI들을 다시 그려줘"라고 알려주는 관찰 가능한 상태 홀더입니다. 이 둘을 함께 쓰지 않으면(예를 들어 그냥 var count = 0으로 선언하면) 값은 바뀌어도 화면은 갱신되지 않습니다 — 처음 배우는 분들이 가장 자주 겪는 실수입니다.@Composable
fun Counter() {
// remember: recomposition 간에 값을 유지
// mutableStateOf: 값이 바뀌면 이 값을 읽는 UI를 자동으로 다시 그림
// by 키워드를 쓰면 count.value 대신 count로 바로 읽고 쓸 수 있습니다
var count by remember { mutableStateOf(0) }
Column {
Text(text = "카운트: ${count}")
Button(onClick = { count++ }) { Text("증가") }
}
}여기서는 리스트 UI: LazyColumn을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
LazyColumn은 이름 그대로 "게으르게(lazy)" 동작해서, 실제로 화면에 보이는 항목만 그리고 스크롤할 때마다 필요한 만큼만 추가로 그립니다. 웹 개발의 가상 스크롤(virtual scrolling)과 같은 개념입니다.data class Todo(val id: Int, val title: String)
@Composable
fun TodoList(todos: List<Todo>) {
LazyColumn {
items(todos, key = { it.id }) { todo ->
Text(
text = todo.title,
modifier = Modifier.padding(12.dp)
)
}
}
}여기서는 ViewModel과 생명주기을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
class CounterViewModel : ViewModel() {
// private set: 외부에서는 읽기만 가능, 수정은 이 클래스 안에서만
var count by mutableStateOf(0)
private set
fun increment() { count++ }
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
Column {
Text("카운트: ${viewModel.count}")
Button(onClick = { viewModel.increment() }) { Text("증가") }
}
}여기서는 네트워크 통신 (Retrofit + 코루틴)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
suspend 키워드를 붙이면 "이 함수는 시간이 걸릴 수 있으니 기다리는 동안 다른 작업(예: 화면 그리기)을 막지 않겠다"는 뜻이 됩니다 — 콜백 지옥 없이 마치 동기 코드처럼 순서대로 작성할 수 있습니다.data class User(val id: Int, val name: String)
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
val api: ApiService = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
class UserViewModel : ViewModel() {
var users by mutableStateOf<List<User>>(emptyList())
private set
fun loadUsers() {
// viewModelScope: ViewModel이 사라질 때 자동으로 코루틴도 함께 취소됨
viewModelScope.launch {
try {
users = api.getUsers()
} catch (e: Exception) {
// 네트워크 오류, 서버 오류 등을 여기서 처리
}
}
}
}여기서는 권한 처리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
AndroidManifest.xml에 어떤 권한이 필요한지 선언합니다. 둘째, 앱 실행 중에 사용자에게 실제로 권한을 요청하고 승인을 받아야 합니다 (Android 6.0 이상은 설치 시점이 아니라 실제 사용 시점에 물어보는 "런타임 권한" 방식입니다).<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />@Composable
fun CameraScreen() {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
// 권한이 승인된 경우의 처리
} else {
// 거부된 경우 — 이유를 설명하는 안내 UI를 보여주는 것이 좋습니다
}
}
Button(onClick = {
launcher.launch(Manifest.permission.CAMERA)
}) {
Text("카메라 권한 요청")
}
}여기서는 로컬 데이터 저장 (DataStore)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
val Context.dataStore by preferencesDataStore(name = "settings")
val DARK_MODE_KEY = booleanPreferencesKey("dark_mode")
// 저장하기
suspend fun setDarkMode(context: Context, enabled: Boolean) {
context.dataStore.edit { prefs -> prefs[DARK_MODE_KEY] = enabled }
}
// Flow로 값 관찰하기 — 값이 바뀔 때마다 자동으로 새 값이 흘러옴
fun darkModeFlow(context: Context): Flow<Boolean> =
context.dataStore.data.map { prefs -> prefs[DARK_MODE_KEY] ?: false }여기서는 테스트 작성하기을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
createComposeRule()로 테스트용 화면을 띄우고, 텍스트나 버튼을 찾아 클릭한 뒤 결과를 확인하는 식입니다. ViewModel처럼 UI와 분리된 로직은 순수 Kotlin 단위 테스트로 훨씬 빠르게 검증할 수 있습니다.class CounterTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun clickingButton_incrementsCounter() {
composeTestRule.setContent { Counter() }
// 초기 상태 확인
composeTestRule.onNodeWithText("카운트: 0").assertExists()
// 버튼 클릭
composeTestRule.onNodeWithText("증가").performClick()
// 클릭 후 상태 확인
composeTestRule.onNodeWithText("카운트: 1").assertExists()
}
}여기서는 빌드 및 배포을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# 개발 중 테스트용 디버그 APK 빌드
./gradlew assembleDebug
# Play 스토어 제출용 릴리스 App Bundle 빌드
./gradlew bundleRelease| 단계 | 해야 할 일 |
|---|---|
| 1. 서명 키 생성 | Android Studio의 Build → Generate Signed Bundle/APK 마법사로 keystore 파일을 만들고 안전하게 보관 (분실하면 같은 앱을 업데이트할 수 없습니다) |
| 2. 릴리스 빌드 | 위 keystore로 서명된 AAB 파일을 빌드 |
| 3. Play Console 등록 | Google Play Console에 개발자로 등록 (1회 등록비 필요) 후 AAB 업로드 |
| 4. 단계적 출시 | 내부 테스트 → 비공개 테스트 → 전체 공개 순서로 단계적으로 배포하는 것이 안전합니다 |
이 섹션은 다음 단계을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
Android 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | Android 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 Android 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
Android 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |