안드로이드 개발의 기본, Activity 완벽 이해하기 (초보자 가이드)
안녕하세여. 안드로이드 개발을 시작하면 반드시 알아야 하는 핵심 개념 중 하나가 바로 'Activity'입니다. 사용자와 직접 상호작용하는 화면을 담당하는 Activity는 안드로이드 앱 개발의 근간이 되는 요소입니다. 이 글에서는 초보 개발자도 쉽게 이해할 수 있도록 Activity의 기본 개념부터 활용 방법까지 상세히 알아보겠습니다.
목차
- Activity란 무엇인가?
- Activity의 생명주기(Lifecycle)
- Activity 생명주기 시각화
- Activity 구현하기
- Activity 간 전환하기
- Activity의 다양한 실행 모드
- Activity에서 권한 처리하기
- Fragment와 Activity의 관계
- Activity 상태 저장 및 복원
- 최신 안드로이드에서의 Activity
- Activity 관련 자주 발생하는 오류와 해결법
- Activity 최적화 팁
#1. Activity란 무엇인가?
Activity는 안드로이드 앱에서 사용자와 상호작용하기 위한 단일 화면을 의미합니다. 앱은 보통 여러 개의 Activity로 구성되며, 각 Activity는 서로 다른 기능과 화면을 담당합니다. 예를 들어, 메시지 앱에서는 대화 목록을 보여주는 Activity, 대화 내용을 보여주는 Activity, 새 메시지를 작성하는 Activity 등이 있을 수 있습니다.
안드로이드 공식 문서에서는 Activity를 다음과 같이 정의합니다:
"Activity는 앱과 사용자가 상호작용할 수 있는 진입점이며, 앱이 UI를 그리는 창을 제공합니다."
Activity는 android.app.Activity
클래스를 상속받아 구현하며, 최근에는 주로 androidx.appcompat.app.AppCompatActivity
를 사용하여 하위 버전 호환성을 유지합니다.
#2. Activity의 생명주기(Lifecycle)
Activity의 가장 중요한 특징 중 하나는 '생명주기(Lifecycle)'입니다. Activity는 생성되고, 사용자에게 보여지고, 다시 숨겨지며, 종료되기까지 여러 상태 변화를 겪게 됩니다. 이러한 상태 변화를 제대로 관리하는 것이 안드로이드 앱 개발의 핵심입니다.
Activity 생명주기 메서드는 다음과 같습니다:
onCreate()
Activity가 처음 생성될 때 호출됩니다. 여기서 UI를 초기화하고, 뷰를 생성하며, 필요한 데이터를 준비합니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// UI 초기화, 뷰 바인딩, 데이터 준비 등
val button = findViewById<Button>(R.id.button_start)
button.setOnClickListener {
// 버튼 클릭 처리
}
}
주요 작업:
- 레이아웃 설정 (
setContentView()
) - 뷰 찾기 및 초기화 (
findViewById()
) - 데이터 바인딩 설정
- 클릭 리스너 등록
- ViewModel 초기화
onStart()
Activity가 사용자에게 보여지기 직전에 호출됩니다. 화면에 표시되기 위한 준비를 합니다.
override fun onStart() {
super.onStart()
// 화면에 표시되기 직전 준비 작업
}
주요 작업:
- UI 업데이트 준비
- 애니메이션 시작 준비
- 센서 리스너 등록
onResume()
Activity가 사용자와 상호작용할 수 있는 상태가 되었을 때 호출됩니다. 이 상태에서 사용자 입력을 받을 수 있습니다.
override fun onResume() {
super.onResume()
// 사용자와 상호작용 준비 완료
// 실시간 업데이트, 애니메이션 시작 등
}
주요 작업:
- 실시간 데이터 업데이트 시작
- 카메라, 위치 등 리소스 집약적 기능 시작
- 애니메이션 재생 시작
onPause()
Activity가 부분적으로 보이지 않게 될 때 호출됩니다. 다른 Activity가 전면에 올라오거나, 대화상자가 표시될 때 발생합니다.
override fun onPause() {
super.onPause()
// 일시 중지 상태 처리
// 리소스 집약적 작업 일시 중지
}
주요 작업:
- 애니메이션, 비디오 일시 중지
- 지속적인 위치 업데이트 중지
- 중요한 데이터 저장
- 커밋되지 않은 변경사항 저장
onStop()
Activity가 더 이상 사용자에게 보이지 않을 때 호출됩니다. 다른 Activity가 완전히 화면을 가리거나, 앱이 백그라ун드로 이동했을 때 발생합니다.
override fun onStop() {
super.onStop()
// 화면에서 완전히 가려짐
// 무거운 리소스 해제
}
주요 작업:
- 불필요한 리소스 해제
- 데이터베이스 연결 해제
- 광고 중지
- 브로드캐스트 리시버 해제
onRestart()
Activity가 중지되었다가 다시 시작될 때 호출됩니다. onStop()
이후 다시 화면에 보이기 전에 호출됩니다.
override fun onRestart() {
super.onRestart()
// 중지 상태에서 다시 시작될 때 추가 초기화
}
주요 작업:
- 일시 중지된 작업 재개 준비
- 변경된 환경 설정 반영
onDestroy()
Activity가 완전히 종료되기 전에 호출됩니다. 시스템이 메모리 확보를 위해 Activity를 제거하거나, finish()
가 호출되었을 때 발생합니다.
override fun onDestroy() {
super.onDestroy()
// 최종 정리 작업
// 모든 리소스 해제
}
주요 작업:
- 모든 리소스 해제
- 스레드 종료
- 데이터베이스 연결 종료
- 메모리 누수 방지를 위한 정리
#3. Activity 생명주기 시각화
Activity의 생명주기를 그림으로 표현하면 다음과 같습니다:
주요 생명주기 이벤트 흐름:
- Activity 시작:
onCreate()
→onStart()
→onResume()
- Activity 일시 중지:
onPause()
- Activity 재개:
onResume()
- Activity 중지:
onPause()
→onStop()
- Activity 재시작:
onRestart()
→onStart()
→onResume()
- Activity 종료:
onPause()
→onStop()
→onDestroy()
#4. Activity 구현하기
기본 Activity 만들기
안드로이드 스튜디오에서는 새 Activity를 쉽게 생성할 수 있습니다. 수동으로 만들고 싶다면 다음과 같이 작성합니다:
// Kotlin으로 작성한 기본 Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 여기서 초기화 작업 수행
}
}
// Java로 작성한 기본 Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 여기서 초기화 작업 수행
}
}
레이아웃 연결하기
Activity에는 해당 화면의 UI를 정의하는 레이아웃 파일이 필요합니다. 레이아웃 파일은 XML로 작성되며, setContentView()
메서드를 통해 Activity와 연결됩니다.
<!-- res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, Activity!"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Next Activity"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
매니페스트 등록하기
모든 Activity는 안드로이드 시스템에 등록되어야 합니다. 이 작업은 AndroidManifest.xml
파일에서 이루어집니다:
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 메인 Activity 선언 -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 다른 Activity 선언 -->
<activity
android:name=".SecondActivity"
android:exported="false" />
</application>
</manifest>
위 매니페스트에서:
android:exported="true"
: 다른 앱에서 이 Activity를 시작할 수 있음을 의미intent-filter
의MAIN
액션과LAUNCHER
카테고리: 앱 런처에서 이 Activity를 시작 화면으로 표시
#5. Activity 간 전환하기
Intent로 새 Activity 시작하기
안드로이드에서는 Intent를 사용하여 Activity 간 전환을 수행합니다:
// Kotlin에서 새 Activity 시작하기
val button = findViewById<Button>(R.id.button_start)
button.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
// Java에서 새 Activity 시작하기
Button button = findViewById(R.id.button_start);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
});
데이터 전달하기
Intent에 추가 데이터를 담아 다른 Activity로 전달할 수 있습니다:
// Kotlin으로 데이터 전달하기
val intent = Intent(this, SecondActivity::class.java).apply {
putExtra("user_name", "John Doe")
putExtra("user_age", 25)
putExtra("is_premium", true)
}
startActivity(intent)
수신하는 Activity에서는 다음과 같이 데이터를 받습니다:
// 전달받은 데이터 읽기
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val userName = intent.getStringExtra("user_name") ?: "Unknown"
val userAge = intent.getIntExtra("user_age", 0)
val isPremium = intent.getBooleanExtra("is_premium", false)
// 데이터 사용하기
val textView = findViewById<TextView>(R.id.text_view)
textView.text = "Name: $userName, Age: $userAge, Premium: $isPremium"
}
결과 받아오기
다른 Activity를 시작하고 결과를 받아올 수도 있습니다. 최신 안드로이드에서는 ActivityResultLauncher
를 사용합니다:
// 최신 방식 (ActivityResultLauncher 사용)
private val startForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
val returnedText = data?.getStringExtra("result_text") ?: ""
Toast.makeText(this, "Returned: $returnedText", Toast.LENGTH_SHORT).show()
}
}
// 버튼 클릭 시 결과를 기대하며 Activity 시작
binding.buttonStartForResult.setOnClickListener {
val intent = Intent(this, InputActivity::class.java)
startForResult.launch(intent)
}
결과를 반환하는 Activity:
// 결과 전달하기
binding.buttonSubmit.setOnClickListener {
val resultText = binding.editTextInput.text.toString()
val resultIntent = Intent().apply {
putExtra("result_text", resultText)
}
setResult(Activity.RESULT_OK, resultIntent)
finish() // Activity 종료
}
#6. Activity의 다양한 실행 모드
Activity의 실행 방식을 제어하는 '실행 모드(Launch Mode)'에 대해 알아보겠습니다:
standard
기본 실행 모드로, Activity 시작 요청이 있을 때마다 새 인스턴스를 생성합니다.
<activity
android:name=".StandardActivity"
android:launchMode="standard" />
특징:
- 동일한 Activity가 여러 번 중첩될 수 있음
- 같은 Activity의 여러 인스턴스가 백 스택에 쌓임
singleTop
현재 화면의 최상단에 동일한 Activity가 있을 경우, 새 인스턴스를 생성하지 않고 기존 인스턴스의 onNewIntent()
메서드를 호출합니다.
<activity
android:name=".SingleTopActivity"
android:launchMode="singleTop" />
특징:
- 중복 인스턴스 생성 방지
- 알림에서 Activity를 시작할 때 유용
- 액티비티가 백 스택 최상단에 있을 때만 재사용
singleTask
해당 Activity의 인스턴스가 이미 존재하면, 해당 인스턴스를 포함하는 태스크를 전면으로 가져오고 그 위에 있던 Activity들을 모두 제거합니다.
<activity
android:name=".SingleTaskActivity"
android:launchMode="singleTask" />
특징:
- 하나의 태스크에 인스턴스 하나만 존재
- 홈 화면 등 주요 진입점 Activity에 적합
- 새 Intent는
onNewIntent()
로 전달됨
singleInstance
독자적인 태스크에서 단 하나의 인스턴스만 존재하며, 다른 Activity와 함께 실행되지 않습니다.
<activity
android:name=".SingleInstanceActivity"
android:launchMode="singleInstance" />
특징:
- 전용 태스크에서 단독으로 실행
- 다른 앱과 Activity를 공유할 때 유용
- 전화 앱 호출 화면 등에 사용
#7. Activity에서 권한 처리하기
안드로이드 6.0(API 23) 이상부터는 위험 권한을 실행 시간에 요청해야 합니다:
// 권한 확인 및 요청
private fun checkAndRequestPermission() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
// 권한이 없는 경우 요청
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_REQUEST_CODE
)
} else {
// 이미 권한이 있는 경우 카메라 시작
startCamera()
}
}
// 권한 요청 결과 처리
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
CAMERA_PERMISSION_REQUEST_CODE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 권한 획득 성공
startCamera()
} else {
// 권한 거부됨
Toast.makeText(this, "카메라 권한이 필요합니다", Toast.LENGTH_SHORT).show()
}
}
}
}
companion object {
private const val CAMERA_PERMISSION_REQUEST_CODE = 100
}
#8. Fragment와 Activity의 관계
Fragment는 Activity 내에서 동작하는 UI 모듈로, 재사용성과 화면 분할을 지원합니다:
// Activity에 Fragment 추가하기
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, HomeFragment())
.commit()
}
}
Activity와 Fragment 간 통신:
// Fragment에서 Activity 참조하기
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
listener = context
} else {
throw RuntimeException("$context must implement OnFragmentInteractionListener")
}
}
// 인터페이스를 통한 통신
interface OnFragmentInteractionListener {
fun onFragmentInteraction(data: String)
}
#9. Activity 상태 저장 및 복원
화면 회전 등으로 Activity가 재생성될 때 상태를 유지하기 위한 방법:
// 상태 저장하기
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 데이터 저장
outState.putString("user_input", binding.editTextInput.text.toString())
outState.putInt("counter", counter)
}
// 상태 복원하기
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 저장된 데이터 복원
if (savedInstanceState != null) {
val userInput = savedInstanceState.getString("user_input", "")
counter = savedInstanceState.getInt("counter", 0)
binding.editTextInput.setText(userInput)
binding.textViewCounter.text = "Counter: $counter"
}
}
#10. 최신 안드로이드에서의 Activity
ViewModel과 Activity
ViewModel을 사용하여 UI 데이터를 관리하고 화면 회전 시에도 데이터를 유지할 수 있습니다:
// ViewModel 정의
class MainViewModel : ViewModel() {
private val _counter = MutableLiveData<Int>(0)
val counter: LiveData<Int> = _counter
fun incrementCounter() {
_counter.value = (_counter.value ?: 0) + 1
}
}
// Activity에서 ViewModel 사용
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ViewModel 초기화
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// LiveData 관찰
viewModel.counter.observe(this, { count ->
binding.textViewCounter.text = "Counter: $count"
})
// 버튼 클릭 시 카운터 증가
binding.buttonIncrement.setOnClickListener {
viewModel.incrementCounter()
}
}
}
LiveData와 Activity
LiveData를 사용하여 데이터 변경을 관찰하고 UI를 자동으로 업데이트할 수 있습니다:
// LiveData 예제
class WeatherViewModel : ViewModel() {
private val _temperature = MutableLiveData<Float>()
val temperature: LiveData<Float> = _temperature
fun fetchWeatherData() {
// 날씨 데이터를 가져와서 LiveData 업데이트
viewModelScope.launch {
try {
val weatherData = weatherRepository.getWeatherData()
_temperature.value = weatherData.temperature
} catch (e: Exception) {
// 오류 처리
}
}
}
}
// Activity에서 LiveData 관찰
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
val weatherViewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)
weatherViewModel.temperature.observe(this, { temperature ->
binding.textViewTemperature.text = "현재 온도: ${temperature}°C"
})
// 데이터 로드
weatherViewModel.fetchWeatherData()
}
Navigation Component와 Activity
Navigation Component를 사용하여 Activity와 Fragment 간 탐색을 관리할 수 있습니다:
// MainActivity에서 NavHost 설정
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
// 하단 네비게이션과 연결
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNav.setupWithNavController(navController)
}
긴 글 읽어 주셔서 감사힙니다.
2편에서 이어서 연재하도록 하겠습니다.
끝.
'■Development■ > 《Android》' 카테고리의 다른 글
[Android] GC_CONCURRENT FREED 라는 에러 메시지 완벽 이해하기 (0) | 2020.04.08 |
---|---|
[Android] 안드로이드 개발의 핵심, Context 완벽 이해하기(초보자 가이드) (0) | 2020.04.08 |
[Android ] Intent FLAG 완벽 가이드 : Android 개발자를 위한 필수 지식 (0) | 2020.04.08 |
[Android] ArrayList 객체를 Intent로 전달하는 방법 (3) | 2020.04.08 |
[Android] Android 개발의 기본, Activity 완벽 이해하기 (초보자 가이드) 2편 (0) | 2020.04.08 |