안드로이드 개발을 하다 보면 View와 ViewGroup이라는 용어를 자주 접하게 된다. 처음에는 이 둘의 차이가 명확하지 않아 헷갈릴 수 있는데, 오늘은 이 두 개념의 차이점과 함께 UI 성능 최적화에 유용한 ViewStub까지 정리해보려고 한다.
View는 안드로이드 UI의 가장 기본이 되는 구성 요소이다. 화면에 표시되는 모든 UI 요소의 최상위 클래스라고 볼 수 있다. Button, TextView, ImageView 같은 위젯들이 모두 View를 상속받아 만들어진다.
ViewGroup은 View를 상속받은 특수한 클래스로, 다른 View들을 담을 수 있는 컨테이너 역할을 한다. LinearLayout, ConstraintLayout, FrameLayout 등이 대표적인 예이다.
가장 핵심적인 차이는 자식 요소를 가질 수 있느냐 없느냐이다.
두 클래스는 UI에서 담당하는 역할이 다르다.
안드로이드의 렌더링 과정에서도 차이가 있다.
onMeasure()에서 자기 자신의 크기만 측정하면 된다.onLayout()에서 각 자식을 어디에 배치할지 결정해야 한다.화면에 그려지는 방식도 다르다.
onDraw()에서 자기 자신만 그린다.dispatchDraw()를 통해 모든 자식들을 순서대로 그린다.ViewGroup은 View를 상속받기 때문에 추가적인 메서드들을 제공한다.
onDraw(), onMeasure() 등 기본적인 메서드만 가진다.addView(), removeView(), getChildAt() 등 자식 View를 관리하는 메서드들이 추가로 제공된다.실제 UI를 구성할 때는 다음과 같은 트리 구조로 만들어진다.
ConstraintLayout (ViewGroup)
├── TextView (View)
├── Button (View)
└── LinearLayout (ViewGroup)
├── ImageView (View)
└── EditText (View)
View와 ViewGroup의 기본 개념을 이해했다면, 이제 성능 최적화를 위한 특별한 View인 ViewStub에 대해 알아보자.
안드로이드 개발을 하다 보면 때때로 조건부로 보여줘야 하는 UI가 있다. 예를 들어 에러 메시지, 로딩 화면, 또는 특정 상황에서만 나타나는 배너 같은 것들이다. 이럴 때 유용하게 사용할 수 있는 것이 바로 ViewStub이다.
ViewStub은 지연 로딩(Lazy Loading)을 위한 가벼운 View이다. 초기에는 실제 View를 생성하지 않고 자리만 차지하고 있다가, 필요한 시점에 inflate되어 실제 레이아웃으로 대체된다.
일반적인 View는 레이아웃이 inflate될 때 함께 생성되지만, ViewStub은 명시적으로 inflate를 호출하기 전까지는 메모리에 로드되지 않는다. 크기도 0이고 화면에 그려지지도 않기 때문에 거의 비용이 들지 않는다.
setVisibility(View.VISIBLE)을 호출하면 자동으로 inflate된다.XML에서 정의:
<ViewStub
android:id="@+id/stub_error_layout"
android:layout="@layout/error_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
코드에서 inflate:
// 방법 1: inflate() 메서드 사용
val errorView = findViewById<ViewStub>(R.id.stub_error_layout).inflate()
// 방법 2: visibility 변경
findViewById<ViewStub>(R.id.stub_error_layout).visibility = View.VISIBLE
// inflate 후에는 ViewStub이 제거되므로 실제 View로 접근
val errorLayout = findViewById<View>(R.id.error_layout)
ViewStub을 사용하면 다음과 같은 방식으로 성능을 최적화할 수 있다.
자주 사용되지 않는 복잡한 레이아웃을 ViewStub으로 대체하면, 앱 초기 실행 시 화면이 더 빠르게 로드된다.
// 에러 상황에서만 에러 화면을 inflate
if (isError) {
val errorView = errorStub.inflate()
// 에러 정보 표시
}
조건부로 보여지는 UI가 많을수록 효과가 크다. 예를 들어 프리미엄 사용자에게만 보이는 배너, 특정 상황에서만 나타나는 안내 메시지 등을 ViewStub으로 처리하면 대부분의 사용자에게는 해당 View가 메모리에 로드되지 않는다.
상세 정보 화면처럼 스크롤해야 보이는 하단 영역이 복잡한 경우, ViewStub으로 만들어두고 사용자가 스크롤할 때 inflate하면 초기 로딩이 빨라진다.
scrollView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
if (scrollY > threshold && !isDetailInflated) {
detailStub.inflate()
isDetailInflated = true
}
}
1. 빈 화면(Empty State)
<ViewStub
android:id="@+id/stub_empty_view"
android:layout="@layout/empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent" />
2. 로딩 인디케이터
<ViewStub
android:id="@+id/stub_loading"
android:layout="@layout/loading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
3. 네트워크 에러 화면
<ViewStub
android:id="@+id/stub_network_error"
android:layout="@layout/network_error_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
visibility를 사용하는 일반 View가 낫다.ViewStub은 다음과 같은 상황에서 효과적이다.
반대로 자주 보였다 숨겼다 해야 하는 UI라면 ViewStub보다는 일반 View의 visibility를 조절하는 것이 더 적합하다.
View와 ViewGroup의 차이를 이해하면 안드로이드 UI가 어떻게 구성되고 동작하는지 더 명확하게 파악할 수 있다. 특히 커스텀 View나 ViewGroup을 만들 때 이런 차이를 알고 있으면 큰 도움이 된다.
여기에 ViewStub까지 적절히 활용하면 초기 로딩 속도를 개선하고 메모리 사용량을 줄일 수 있다. ViewStub은 작은 최적화처럼 보이지만, 사용자 경험을 개선하는 데 확실히 도움이 된다. 특히 앱 초기 실행 속도가 중요한 경우라면 적극적으로 활용해볼 만하다.
안드로이드 UI 개발은 이런 기본 개념들을 탄탄히 이해하는 것에서 시작된다. 오늘 정리한 내용이 더 나은 UI를 만드는 데 도움이 되길 바란다.