안드로이드 앱에서 사용자와 상호작용할 수 있는 화면(window)을 의미한다.
앱의 한 화면을 나타내는 기본 구성 요소이다.
그렇다면 네이버처럼 화면이 많은 앱은 액티비티가 몇백개일까?
결론은 개발 방식에 따라 달라진다.
예전 개발방식으로는, 각 화면 = 1개의 액티비티 였다.
화면을 전환하려면 반드시 새로운 액티비티를 생성해야 했었다.
예: 로그인 화면 → 메인 화면 → 프로필 화면
// 로그인 액티비티
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
}
// 메인 액티비티
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
이제 화면 != 액티비티인 시대가 되었다.
단일 액티비티 내에서 여러 화면(컴포저블) 구현이 가능해진 것이다.
Jetpack Compose를 사용함으로써 내비게이션을 사용하여서 여러 화면(컴포저블)을 구현하면 된다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 하나의 액티비티 안에 여러 화면(컴포저블) 존재
NavHost {
composable("login") { LoginScreen() }
composable("main") { MainScreen() }
composable("profile") { ProfileScreen() }
}
}
}
}
그럼 jetpack compose 등장 이후, Activity의 객체 자체의 구조가 완전히 수정된걸까?
아니면 구조 자체는 동일한데 사용 트렌드가 바뀐걸까? 두 문장은 큰 차이가 있다!
Activity 객체 자체의 본질적인 구조는 동일하다.
다. 그러나 사용하는 방식과 앱 개발의 트렌드는 크게 변화했다.
Compose 이전 (다중 액티비티 방식)
각 화면마다 Activity를 생성하여 UI를 구성하는 방식이 일반적이었다.
액티비티는 화면 전환을 책임지며, 각각의 생명주기와 관련된 작업을 처리해야 했다.
Compose 이후 (단일 액티비티 방식)
Activity는 애플리케이션의 시작점이자 화면의 컨테이너 역할로 축소되었다.
실제 화면은 Compose의 Composable 함수와 상태 관리로 구성되며, 하나의 Activity에서 화면을 동적으로 전환하는 방식이 보편화되었다.
1) 단일 Activity의 유지보수성과 유연성 증가
Compose는 화면 전환 및 상태 관리가 강력하고 간소화된 방식으로 가능하므로, 굳이 여러 액티비티를 사용하지 않아도 되었다.
2) Composable 함수의 UI 구성 철학
Compose는 화면을 함수형으로 설계한다. 즉, 화면의 UI를 Composable 함수 단위로 나누고 조합할 수 있으므로, 화면 간 전환과 상태 변화도 함수 호출만으로 해결된다.
3) 다중 액티비티의 단점
물론 Compose 시대에도 다중 액티비티 방식이 완전히 사라진 것은 아닙니다.
다른 앱에서 호출될 가능성이 있는 화면
특정 액티비티를 외부에서 바로 호출해야 하는 경우(예: 파일 선택 화면).
명확한 컨텍스트 분리 필요
앱의 주요 기능이 완전히 다른 컨텍스트를 가지며 서로 독립적일 경우.
대규모 팀 작업
화면 단위로 개발을 분리하여 독립적으로 작업해야 하는 경우.
그렇다면 생명주기가 개발에 있어서 왜 중요한지 유튜브 앱을 기반으로 알아보자.
백그라운드 재생: 사용자가 유튜브 앱에서 동영상을 시청하는 도중, 다른 앱으로 전환하거나 화면을 회전하면, 생명주기 메서드가 호출되어 현재 상태를 저장하거나 재생 상태를 관리해야 한다.
예를 들어, onPause()에서 비디오 재생을 일시 정지하고, onResume()에서 사용자가 다시 돌아왔을 때 비디오를 재생하는 로직을 개발자가 작성해야 한다.
메모리 관리: 동영상 스트리밍과 같은 리소스가 많은 기능을 구현할 때, 메모리 누수를 방지하는 것이 중요하다. onStop()이나 onDestroy()에서 리소스를 해제하는 코드를 작성하여 메모리 효율성을 높일 수 있다.
사용자 상태 저장: 사용자가 동영상을 시청 중에 앱이 종료되거나 백그라운드로 이동할 경우, onSaveInstanceState() 메서드를 통해 현재 시청 중인 동영상의 상태를 저장하고, 나중에 복원할 수 있어야 한다. 유튜브에서는 사용자가 어디까지 봤는지 기억하는 기능이 사용자 경험을 크게 향상시킨다.
옛날 방식으로 생명주기를 관리할 때는 ViewModel을 사용하지 않고,
주로 액티비티의 생명주기 메서드를 직접 오버라이드하여 상태를 관리했다.
class MainActivity : AppCompatActivity() {
private var currentPlaybackPosition: Long = 0
private var isPlaying: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// UI 초기화 코드
}
override fun onStart() {
super.onStart()
// 액티비티가 시작될 때 필요한 작업 수행
}
override fun onResume() {
super.onResume()
// 저장된 재생 위치 복원
// 예: mediaPlayer.seekTo(currentPlaybackPosition)
if (isPlaying) {
// 비디오 재생 시작
// mediaPlayer.start()
}
}
override fun onPause() {
super.onPause()
// 현재 재생 위치 저장
// 예: currentPlaybackPosition = mediaPlayer.currentPosition
// 비디오 일시 정지
// mediaPlayer.pause()
}
override fun onStop() {
super.onStop()
// 액티비티가 더 이상 보이지 않을 때 필요한 작업 수행
}
override fun onDestroy() {
super.onDestroy()
// 리소스 해제
// 예: mediaPlayer.release()
}
}
상태 변수: ViewModel을 사용하지 않고, currentPlaybackPosition
과 isPlaying
을 MainActivity
클래스의 프로퍼티로 직접 정의했다.
UI 초기화: Compose를 사용하지 않고 XML 레이아웃을 사용하는 경우, setContentView(R.layout.activity_main)
을 통해 레이아웃을 설정한다.
리소스 관리: onDestroy()
에서 메모리 누수를 방지하기 위해 비디오 플레이어와 같은 리소스를 해제하는 코드가 필요하다.
전통적인 방식으로 생명주기를 관리하는 것은 상태를 관리하고 UI를 업데이트하는 데 더 많은 코드를 필요로 하며, 각 생명주기 메서드에서 직접 상태를 관리해야 하기 때문에 코드의 가독성이 떨어질 수 있다.
반면, ViewModel을 사용하는 현대적인 접근 방식은 상태 관리를 쉽게 해주며, 생명주기와 UI 업데이트를 더 깔끔하게 분리할 수 있다.
요즘에는 최신 라이브러리들을 활용하면 생명주기 관리가 훨씬 간편해졌다.
ViewModel은 UI 관련 데이터를 저장하고 관리하는 클래스다. 액티비티가 재생성되더라도 ViewModel은 생명주기와 관계없이 유지되므로, 생명주기 관리의 복잡성을 줄일 수 있다.
예를 들어, 동영상의 현재 재생 위치나 재생 상태를 ViewModel에 저장하면, 액티비티가 재생성되더라도 쉽게 복원할 수 있다.
class VideoViewModel : ViewModel() {
var currentPlaybackPosition: Long = 0
var isPlaying: Boolean = false
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: VideoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Composable UI
}
}
// 생명주기 메서드에서 ViewModel 사용
override fun onPause() {
super.onPause()
viewModel.currentPlaybackPosition = // 현재 재생 위치 저장
}
override fun onResume() {
super.onResume()
// 저장된 재생 위치 복원
// viewModel.currentPlaybackPosition 사용
}
}
LifecycleObserver를 활용하면 액티비티의 생명주기 이벤트를 좀 더 세부적으로 관리할 수 있다. 특정 작업이 발생하는 생명주기 이벤트를 감지할 수 있다.
class VideoPlaybackObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
// 동영상 재생 시작
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
// 동영상 일시 정지
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val playbackObserver = VideoPlaybackObserver()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(playbackObserver)
setContent {
// Composable UI
}
}
}
그러면 유튜브 앱 개발자는 생명주기를 어떻게 활용하고 관리할까?
백그라운드 재생: 사용자가 다른 앱으로 전환하거나 화면을 잠글 때, onPause()
에서 비디오를 일시 정지하고, 백그라운드에서 오디오를 재생할 수 있도록 설정한다.
데이터 저장 및 복원: 사용자가 다시 돌아왔을 때 현재 재생 중인 동영상의 상태를 복원한다. ViewModel 또는 데이터베이스를 사용하여 사용자가 마지막으로 본 위치를 저장하고, onResume()
에서 복원한다.
사용자 경험 최적화: 사용자가 앱을 다시 열었을 때 마지막 시청 위치에서 비디오가 자동으로 재생되도록 하여 연속성을 유지한다. 이를 위해 생명주기 메서드를 통해 상태를 관리하고, 적절한 UI 업데이트를 수행한다.
코드 간결성: 최신 Android 라이브러리와 Compose의 도입으로 인해, 액티비티 생명주기 메서드를 일일이 오버라이드하는 대신, 필요할 때만 메서드를 구현하고, 나머지는 ViewModel이나 LifecycleObserver를 통해 처리하여 코드의 간결성을 높일 수 있다.
액티비티는 전체(entire), 가시적(visible), 포그라운드(foreground)의 세 가지 생애를 오간다.
전체 생애(Entire Lifetime) - 액티비티가 생성될 때 최초 호출되는 onCreate( ) 메서드 호출과 종결되기 전에 호출되는 onDestroy( ) 호출 사이에 액티비티에서 발생하는 모든 것을 나타내는 데 '전체 생애'라는 용어가 사용된다.
가시적 생애(Visible Lifetime) - onStart( )와 onStop( ) 호출 사이의 액티비티 실행 시기다. 이 시기 동안 액티비티는 자신을 사용자에게 화면으로 보여줄 수 있다.
포그라운드 생애(Foreground Lifetime) - onResume( ) 메서드 호출과 onPause( ) 호출 사이의 액티비티 실행 시기를 의미한다. 액티비티는 자신의 전체 생애 동안 포그라운드와 가시적 생애를 여러 번 거칠 수 있다. 아래의 그림은 액티비티의 생애와 생명주기 메서드들의 개념을 보여준다.
onSaveInstanceState()
메서드를 사용해 사용자가 보고 있던 화면 상태를 저장하고, 복원할 때는 onRestoreInstanceState()
또는 onCreate()
에서 복원해야 한다.Intent
로 데이터를 전달하거나 onActivityResult()
를 사용하여 데이터를 주고받을 수 있다. 최근에는 ActivityResultLauncher
로 더 간단하게 관리할 수 있다.setContent
를 통해 Composable을 Activity에 연결할 수 있다.ViewModel
이나 Compose의 상태 관리를 통해 이러한 변화를 처리할 수 있다.