Wednesday, September 13, 2023

안드로이드 앱 바의 정석: Toolbar와 AppBarLayout 심층 분석

스마트폰 애플리케이션의 첫인상은 대부분 화면 상단에 위치한 '앱 바(App Bar)'에 의해 결정됩니다. 앱 바는 단순히 앱의 제목을 표시하는 공간을 넘어, 사용자가 앱의 핵심 기능에 접근하고 현재 위치를 파악하는 데 중요한 역할을 하는 핵심적인 UI 컴포넌트입니다. 사용자는 앱 바를 통해 검색을 수행하고, 설정 메뉴에 접근하며, 이전 화면으로 돌아가는 등 다양한 상호작용을 수행합니다. 따라서 잘 디자인된 앱 바는 직관적인 사용자 경험(UX)의 초석이라 할 수 있습니다.

안드로이드 개발 초기에는 'ActionBar'가 이 역할을 담당했지만, 머티리얼 디자인(Material Design)의 등장과 함께 더욱 유연하고 강력한 'Toolbar'가 그 자리를 대체했습니다. 더 나아가 'AppBarLayout'과 'CoordinatorLayout' 같은 레이아웃과의 조합을 통해, 단순한 정적 바를 넘어 스크롤과 상호작용하는 동적이고 아름다운 UI를 구현할 수 있게 되었습니다. 이 글에서는 안드로이드 앱 바의 진화 과정을 살펴보고, 현대적인 앱 개발의 표준으로 자리 잡은 Toolbar, AppBarLayout, 그리고 CollapsingToolbarLayout의 개념과 실제 구현 방법을 깊이 있게 다룰 것입니다.

1. 과거의 유산: ActionBar의 역할과 한계

Toolbar를 제대로 이해하기 위해서는 그 전신인 ActionBar에 대한 이해가 선행되어야 합니다. ActionBar는 안드로이드 UI의 중요한 이정표였지만, 동시에 명확한 한계를 가지고 있었기에 Toolbar라는 새로운 대안이 등장할 수 있었습니다.

1.1. ActionBar의 등장과 기능

ActionBar는 안드로이드 3.0 (API 레벨 11, 허니콤)에서 처음 도입되었습니다. 당시 태블릿 전용 OS로 출시된 허니콤에서, 넓은 화면을 효율적으로 활용하기 위한 표준화된 UI 패턴으로 제시되었습니다. ActionBar는 기본적으로 다음과 같은 네 가지 주요 기능을 제공했습니다.

  • 앱 아이콘 및 제목: 앱의 브랜딩을 보여주고 현재 화면의 컨텍스트를 제공합니다.
  • 액션 아이템 (Action Items): 화면과 관련된 주요 작업을 수행할 수 있는 버튼이나 아이콘을 배치합니다. 공간이 부족할 경우 '오버플로우 메뉴(overflow menu)' 안으로 자동으로 숨겨집니다.
  • 내비게이션 (Navigation): 'Up' 버튼을 통해 계층 구조상 상위 화면으로 이동하거나, 탭(Tabs) 또는 드롭다운 목록을 통해 화면 내 탐색을 지원합니다.
  • 액션 모드 (Action Modes): 특정 항목을 길게 눌렀을 때 나타나는 컨텍스트 메뉴(Contextual Action Bar)를 제공하여, 선택된 항목에 대한 작업을 수행할 수 있도록 합니다.

이러한 기능 덕분에 개발자들은 일관된 사용자 경험을 제공할 수 있었고, 사용자들은 어떤 앱을 사용하든 비슷한 구조의 인터페이스를 기대할 수 있게 되었습니다. ActionBar는 Support Library (현재의 AndroidX)를 통해 하위 버전에서도 사용할 수 있도록 지원되면서 안드로이드 앱의 표준 UI로 자리매김했습니다.

1.2. ActionBar의 구조적 한계점

ActionBar는 많은 장점에도 불구하고 구조적인 한계점을 명확히 가지고 있었습니다. 가장 큰 문제는 유연성의 부재였습니다.

ActionBar는 액티비티(Activity)의 윈도우 데코레이션(Window Decoration)의 일부로 취급되었습니다. 즉, 개발자가 레이아웃 XML 파일에서 직접 제어할 수 있는 일반적인 뷰(View)가 아니었습니다. 이로 인해 다음과 같은 제약이 발생했습니다.

  • 위치 고정: ActionBar는 항상 화면 최상단에 고정되어 있었습니다. 화면 중간이나 다른 위치에 배치하는 것은 거의 불가능했습니다.
  • 제한적인 커스터마이징: ActionBar의 배경색, 제목 폰트 등을 변경하는 것은 가능했지만, 그 내부에 복잡한 커스텀 뷰(예: 검색창과 버튼을 조합한 뷰)를 자유롭게 추가하거나 레이아웃을 완전히 재구성하는 것은 매우 까다로웠습니다.
  • 애니메이션의 어려움: 스크롤과 같은 사용자 인터랙션에 반응하여 ActionBar가 자연스럽게 사라지거나 나타나는 등의 동적인 애니메이션을 구현하기가 어려웠습니다.

이러한 한계는 머티리얼 디자인이 추구하는 다채롭고 동적인 사용자 인터페이스를 구현하는 데 큰 걸림돌이 되었습니다. 구글은 이 문제를 해결하기 위해 ActionBar의 철학은 계승하되, 일반 뷰처럼 자유롭게 다룰 수 있는 새로운 컴포넌트를 선보이게 되는데, 그것이 바로 'Toolbar'입니다.

1.3. AppCompatActivity와 getSupportActionBar()의 비밀

오늘날 대부분의 안드로이드 앱은 AppCompatActivity를 상속받아 액티비티를 만듭니다. AppCompatActivity를 사용하면 기본적으로 윈도우에 ActionBar와 유사한 동작을 하는 장식이 포함됩니다. 하지만 최신 개발 방식에서는 이 기본 ActionBar를 비활성화하고, 그 자리에 우리가 직접 추가한 Toolbar를 '마치 ActionBar인 것처럼' 사용하도록 설정합니다. 이 과정에서 setSupportActionBar() 메소드가 핵심적인 역할을 합니다.

먼저, 테마에서 기본 ActionBar를 제거해야 합니다. res/values/themes.xml 파일에서 앱의 테마 부모를 Theme.AppCompat.Light.NoActionBar 또는 Theme.MaterialComponents.DayNight.NoActionBar와 같이 .NoActionBar가 붙은 것으로 설정합니다.


<!-- res/values/themes.xml -->
<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- ... other theme attributes -->
    </style>
</resources>

이렇게 하면 액티비티에서 기본적으로 제공하던 상단 바가 사라집니다. 이제 우리는 레이아웃 XML 파일에 Toolbar를 직접 추가하고, 액티비티 코드에서 이 Toolbar를 'Support Action Bar'로 지정해줄 수 있습니다.


// MyActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        val myToolbar: Toolbar = findViewById(R.id.my_toolbar)
        
        // 이 Toolbar를 이 액티비티의 공식 앱 바로 설정합니다.
        setSupportActionBar(myToolbar) 
    }
}

setSupportActionBar(myToolbar)를 호출하는 순간, 이 Toolbar는 과거의 ActionBar가 하던 역할을 위임받습니다. 예를 들어, 메뉴 리소스를 인플레이트하고, 메뉴 아이템 클릭 이벤트를 처리하며, 내비게이션 아이콘을 관리하는 등의 작업을 표준적인 ActionBar API를 통해 수행할 수 있게 됩니다. getSupportActionBar()를 호출하면 이 Toolbar에 연결된 ActionBar 객체를 얻을 수 있습니다. 이는 과거 ActionBar 코드와의 호환성을 유지하면서도 Toolbar의 모든 유연성을 활용할 수 있게 해주는 매우 영리한 설계입니다.

2. 현대적 대안: Toolbar의 유연성과 강력함

안드로이드 5.0 (API 레벨 21, 롤리팝)과 함께 머티리얼 디자인이 도입되면서, ActionBar의 한계를 극복하기 위한 Toolbar가 android.widget.Toolbar 클래스로 등장했습니다. 이후 AndroidX 라이브러리의 androidx.appcompat.widget.Toolbar가 하위 호환성을 제공하며 표준으로 자리 잡았습니다.

2.1. Toolbar란 무엇인가? 왜 ActionBar를 대체하는가?

Toolbar는 본질적으로 하나의 뷰그룹(ViewGroup)입니다. 이것이 Toolbar와 ActionBar의 가장 근본적인 차이점입니다. LinearLayout이나 FrameLayout처럼, Toolbar는 레이아웃 XML 파일의 어느 곳에나 배치할 수 있으며, 내부에 다른 뷰들을 자식으로 포함할 수 있습니다.

이러한 특성 덕분에 Toolbar는 ActionBar가 가졌던 모든 제약에서 자유로워졌습니다.

  • 자유로운 배치: 화면 상단뿐만 아니라 하단, 중간 등 원하는 곳 어디에든 위치시킬 수 있습니다. 하나의 화면에 여러 개의 Toolbar를 배치하는 것도 가능합니다.
  • 완벽한 커스터마이징: Toolbar 내부에 ImageView, EditText, Spinner 등 어떤 뷰 컴포넌트든 자유롭게 추가하여 완전히 새로운 디자인의 앱 바를 만들 수 있습니다. 로고 이미지, 검색창, 사용자 프로필 아이콘 등을 조합하는 것이 매우 간단해졌습니다.
  • 동적인 상호작용: 일반 뷰이기 때문에 속성 애니메이션(Property Animation)을 적용하기가 용이합니다. 스크롤에 따라 크기, 색상, 위치가 변하는 등 풍부한 사용자 경험을 제공할 수 있습니다.

결론적으로, Toolbar는 ActionBar의 핵심 기능(제목, 메뉴, 내비게이션)을 모두 제공하면서도, 개발자에게 UI 구성에 대한 완전한 제어권을 부여합니다. 이것이 바로 현대 안드로이드 개발에서 ActionBar 대신 Toolbar를 사용하는 이유입니다.

2.2. 기본 Toolbar 구현하기 (XML & Kotlin)

가장 기본적인 Toolbar를 구현하는 과정은 간단합니다. 앞서 설명한 것처럼, 먼저 앱 테마를 .NoActionBar로 설정해야 합니다.

1단계: 레이아웃에 Toolbar 추가하기

activity_main.xml과 같은 레이아웃 파일에 androidx.appcompat.widget.Toolbar 위젯을 추가합니다.


<!-- res/layout/activity_main.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/main_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    
    <!-- 화면의 나머지 콘텐츠 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello, Toolbar!"
        android:layout_gravity="center"
        android:padding="16dp"/>

</LinearLayout>

위 코드의 주요 속성들을 살펴보겠습니다.

  • android:id: 코드에서 Toolbar를 참조하기 위한 ID입니다.
  • android:layout_height="?attr/actionBarSize": Toolbar의 높이를 현재 테마에 정의된 표준 액션바 높이로 설정합니다. 이를 통해 다양한 기기에서 일관된 높이를 유지할 수 있습니다.
  • android:background="?attr/colorPrimary": 배경색을 테마의 기본 색상(primary color)으로 지정합니다.
  • android:elevation="4dp": Toolbar 아래에 그림자 효과를 주어 입체감을 표현합니다. (API 21 이상에서 적용)
  • android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar": Toolbar 자체의 테마를 지정합니다. Dark.ActionBar는 어두운 배경에 밝은 색상의 텍스트(제목)와 아이콘이 표시되도록 합니다. 배경이 밝은 색이라면 Light.ActionBar를 사용할 수 있습니다.
  • app:popupTheme: 오버플로우 메뉴와 같이 Toolbar에서 띄우는 팝업 창의 테마를 지정합니다. 보통 Toolbar 자체의 테마와 반대되는 밝기를 가집니다. 여기서는 밝은 배경에 어두운 텍스트를 가진 팝업이 표시됩니다.

2단계: Activity에서 Toolbar 설정하기

MainActivity.kt에서 setSupportActionBar()를 호출하여 이 Toolbar를 액티비티의 앱 바로 지정합니다.


// MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val toolbar: Toolbar = findViewById(R.id.main_toolbar)
        setSupportActionBar(toolbar)

        // ActionBar API를 사용하여 제목 설정
        supportActionBar?.title = "My Application"
        supportActionBar?.subtitle = "Welcome!"
    }
}

이제 앱을 실행하면 레이아웃에 추가한 Toolbar가 화면 상단에 표시되고, "My Application"이라는 제목과 "Welcome!"이라는 부제목이 나타나는 것을 확인할 수 있습니다. supportActionBar는 Nullable 타입이므로, 안전 호출(?.)을 사용하는 것이 좋습니다.

2.3. Toolbar에 메뉴 추가 및 이벤트 처리

앱 바의 핵심 기능 중 하나는 사용자에게 액션을 제공하는 메뉴입니다. Toolbar에 메뉴를 추가하는 과정은 ActionBar와 동일합니다.

1단계: 메뉴 리소스 XML 파일 생성하기

res/menu 디렉터리에 XML 파일을 생성합니다. (디렉터리가 없다면 새로 만드세요.)


<!-- res/menu/main_menu.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search"
        android:title="Search"
        app:showAsAction="ifRoom|collapseActionView" 
        app:actionViewClass="androidx.appcompat.widget.SearchView"/>

    <item
        android:id="@+id/action_settings"
        android:title="Settings"
        app:showAsAction="never" />

    <item
        android:id="@+id/action_share"
        android:icon="@drawable/ic_share"
        android:title="Share"
        app:showAsAction="ifRoom" />

</menu>
  • app:showAsAction 속성은 메뉴 아이템이 Toolbar에 어떻게 표시될지를 결정합니다.
    • ifRoom: 공간이 있으면 아이콘으로 표시하고, 없으면 오버플로우 메뉴로 보냅니다.
    • always: 항상 아이콘으로 표시합니다. (공간이 부족하면 UI가 깨질 수 있으므로 2~3개 이하로 제한하는 것이 좋습니다.)
    • never: 항상 오버플로우 메뉴(세로 점 3개) 안에 텍스트로 표시됩니다.
    • withText: 아이콘과 텍스트를 함께 표시합니다. (공간을 많이 차지하므로 주의해야 합니다.)
    • collapseActionView: 이 아이템이 액션 뷰(ActionView)를 가질 때, 아이콘을 누르면 액션 뷰가 Toolbar 전체로 확장되도록 합니다. 주로 검색 기능에 사용됩니다.

2단계: Activity에서 메뉴를 Toolbar에 채우기(Inflate)

액티비티의 onCreateOptionsMenu 콜백 메소드를 오버라이드하여 메뉴 리소스를 인플레이트합니다.


// MainActivity.kt
// ... (imports)
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast

class MainActivity : AppCompatActivity() {
    // ... (onCreate)

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.main_menu, menu)
        return true // 메뉴가 표시되도록 true를 반환
    }
}

3단계: 메뉴 아이템 클릭 이벤트 처리

onOptionsItemSelected 콜백 메소드를 오버라이드하여 각 메뉴 아이템의 클릭 이벤트를 처리합니다.


// MainActivity.kt
// ...

class MainActivity : AppCompatActivity() {
    // ... (onCreate, onCreateOptionsMenu)

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_search -> {
                Toast.makeText(this, "Search clicked", Toast.LENGTH_SHORT).show()
                true
            }
            R.id.action_settings -> {
                Toast.makeText(this, "Settings clicked", Toast.LENGTH_SHORT).show()
                true
            }
            R.id.action_share -> {
                Toast.makeText(this, "Share clicked", Toast.LENGTH_SHORT).show()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

이제 앱을 실행하면 Toolbar 오른쪽에 공유와 검색 아이콘이 표시되고, 오버플로우 메뉴를 누르면 'Settings' 항목이 나타납니다. 각 항목을 클릭하면 설정된 토스트 메시지가 출력됩니다.

2.4. 내비게이션 아이콘 설정 (Up 버튼과 햄버거 메뉴)

Toolbar의 가장 왼쪽에는 보통 내비게이션을 위한 아이콘이 위치합니다. 대표적으로 'Up' 버튼(뒤로 가기 화살표)과 내비게이션 드로어(Navigation Drawer)를 여는 '햄버거' 아이콘이 있습니다.

'Up' 버튼 활성화하기

상세 화면에서 부모 화면으로 돌아가는 계층적 내비게이션을 위해 'Up' 버튼을 표시하는 것은 매우 일반적입니다. supportActionBar 객체를 통해 간단히 설정할 수 있습니다.


// DetailActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    setSupportActionBar(findViewById(R.id.detail_toolbar))

    // 'Up' 버튼을 활성화하고, 홈 아이콘으로 표시되도록 설정
    supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

'Up' 버튼의 클릭 이벤트는 onOptionsItemSelected에서 android.R.id.home ID로 처리할 수 있습니다.


// DetailActivity.kt

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    if (item.itemId == android.R.id.home) {
        // Up 버튼 클릭 시 현재 액티비티를 종료
        finish() 
        // 또는 NavController를 사용하는 경우
        // findNavController(R.id.nav_host_fragment).navigateUp()
        return true
    }
    return super.onOptionsItemSelected(item)
}

또한, AndroidManifest.xml에서 부모 액티비티를 지정해두면 시스템이 'Up' 내비게이션을 더 자연스럽게 처리할 수 있습니다.


<activity
    android:name=".DetailActivity"
    android:parentActivityName=".MainActivity" />

2.5. Toolbar 내부에 커스텀 뷰 추가하기

Toolbar의 가장 강력한 기능은 내부에 원하는 뷰를 자유롭게 배치할 수 있다는 점입니다. 예를 들어, 로고 이미지와 제목을 함께 표시하고 싶을 때 유용합니다.


<androidx.appcompat.widget.Toolbar
    android:id="@+id/custom_toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/white"
    app:title=""> <!-- 기본 제목은 비워둡니다 -->

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <ImageView
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:src="@drawable/ic_app_logo" />
        
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="My Custom App"
            android:textColor="@color/black"
            android:textSize="18sp"
            android:textStyle="bold"
            android:layout_marginStart="8dp"/>
            
    </LinearLayout>

</androidx.appcompat.widget.Toolbar>

위 예제에서는 Toolbar의 기본 제목을 비워두기 위해 app:title="" 속성을 사용했습니다. 그리고 그 내부에 LinearLayout을 배치하여 로고 ImageView와 커스텀 TextView를 수평으로 나란히 두었습니다. 이제 액티비티 코드에서 이 Toolbar를 setSupportActionBar으로 설정하기만 하면, 완전히 커스텀된 앱 바를 사용할 수 있습니다.

이처럼 Toolbar는 단순한 제목 표시줄을 넘어, 개발자가 상상하는 어떤 디자인이든 구현할 수 있는 강력한 캔버스와 같습니다.

3. 동적 UI의 핵심: CoordinatorLayout과 AppBarLayout

정적인 Toolbar만으로는 머티리얼 디자인이 지향하는 생동감 넘치는 사용자 경험을 구현하기 어렵습니다. 사용자가 화면을 스크롤할 때 앱 바가 자연스럽게 반응하도록 만들기 위해서는 CoordinatorLayoutAppBarLayout이라는 두 가지 핵심 컴포넌트가 필요합니다.

3.1. 상호작용의 지휘자, CoordinatorLayout

CoordinatorLayout은 이름 그대로 자식 뷰(child view)들 간의 상호작용을 '조정(coordinate)'하는 특별한 능력을 가진 FrameLayout입니다. 단순히 뷰를 겹쳐 쌓는 FrameLayout과 달리, CoordinatorLayout은 자식 뷰 중 하나에서 특정 이벤트(예: 스크롤)가 발생했을 때, 다른 자식 뷰들이 그에 맞춰 동작하도록 만들 수 있습니다.

이러한 동작은 CoordinatorLayout.Behavior라는 메커니즘을 통해 이루어집니다. Behavior는 특정 뷰에 연결되어 다른 뷰의 변화를 감지하고, 자신이 연결된 뷰의 레이아웃, 크기, 모양 등을 변경하는 로직을 담고 있습니다. FloatingActionButton(FAB)Snackbar가 나타날 때 위로 스르륵 움직이는 것이나, AppBarLayout이 스크롤에 따라 사라지는 것이 모두 CoordinatorLayoutBehavior 덕분입니다.

따라서 스크롤과 연동되는 앱 바를 만들려면, 반드시 최상위 레이아웃으로 CoordinatorLayout을 사용해야 합니다.

3.2. 스크롤 반응의 시작, AppBarLayout

AppBarLayout은 스크롤 이벤트에 반응하도록 설계된 수직 LinearLayout입니다. CoordinatorLayout의 직접적인 자식으로 사용되어야 하며, 일반적으로 그 안에는 ToolbarTabLayout 같은 앱 바 관련 컴포넌트들이 위치합니다.

AppBarLayout의 역할은 자신과 같은 레벨에 있는 스크롤 가능한 뷰(RecyclerView, NestedScrollView 등)의 스크롤 이벤트를 감지하여, 자신의 자식 뷰들에게 스크롤에 따른 변화를 지시하는 것입니다. 어떤 뷰가 스크롤 이벤트를 발생시킬 '트리거' 역할을 할지는 app:layout_behavior 속성을 통해 지정합니다.

다음은 가장 기본적인 구조입니다.


<androidx.coordinatorlayout.widget.CoordinatorLayout 
    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">

    <!-- 1. 스크롤에 반응할 앱 바 영역 -->
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/my_toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:layout_scrollFlags="scroll|enterAlways" /> 
            <!-- 스크롤 동작을 정의하는 플래그 -->

    </com.google.android.material.appbar.AppBarLayout>

    <!-- 2. 스크롤 이벤트를 발생시킬 콘텐츠 영역 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/my_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
        <!-- 이 뷰가 스크롤될 때 AppBarLayout이 반응하도록 연결 -->

</androidx.coordinatorlayout.widget.CoordinatorLayout>

여기서 핵심은 RecyclerViewapp:layout_behavior="@string/appbar_scrolling_view_behavior" 속성입니다. 이 속성은 RecyclerView의 스크롤 정보를 CoordinatorLayout에 알리고, CoordinatorLayout은 이 정보를 같은 레벨에 있는 AppBarLayout에 전달합니다. 그러면 AppBarLayout은 자신의 자식 뷰(여기서는 Toolbar)에 설정된 app:layout_scrollFlags를 보고 어떻게 동작할지 결정합니다.

3.3. 마법의 속성: layout_scrollFlags 파헤치기

app:layout_scrollFlagsAppBarLayout 내의 자식 뷰가 스크롤에 어떻게 반응할지를 결정하는 가장 중요한 속성입니다. 여러 플래그를 |(파이프) 기호로 조합하여 다양한 효과를 만들 수 있습니다.

  • scroll

    가장 기본이 되는 플래그입니다. 이 플래그가 설정된 뷰는 스크롤 가능한 뷰(e.g., RecyclerView)를 따라 화면 밖으로 스크롤되어 사라집니다. 이 플래그가 없으면 다른 플래그는 동작하지 않습니다. scroll 플래그만 단독으로 사용하면, 사용자가 아래로 스크롤할 때 뷰가 사라지고, 다시 최상단으로 스크롤해야만 뷰가 나타납니다.

  • enterAlways

    이 플래그는 scroll 플래그와 함께 사용됩니다. 아래로 스크롤하여 앱 바가 사라진 상태에서, 사용자가 위로 조금이라도 스크롤하면(리스트의 최상단이 아니더라도) 앱 바가 즉시 다시 나타납니다. 사용자가 언제든지 앱 바의 기능에 빠르게 접근할 수 있도록 해줍니다.

    예시 조합: app:layout_scrollFlags="scroll|enterAlways"

  • enterAlwaysCollapsed

    enterAlways 플래그와 함께 사용됩니다. 이 플래그가 설정된 뷰는 최소 높이(android:minHeight) 속성을 가지고 있어야 합니다. 위로 스크롤하여 뷰가 다시 나타날 때, 설정된 최소 높이만큼만 먼저 나타나고, 스크롤을 계속해서 최상단에 도달했을 때 비로소 전체 높이로 확장됩니다. 복잡한 헤더에서 중요한 정보(예: 제목)만 최소 높이 영역에 배치하여 먼저 보여주고 싶을 때 유용합니다.

    예시 조합: app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"

  • exitUntilCollapsed

    이 플래그도 최소 높이(android:minHeight)가 필요합니다. 아래로 스크롤할 때 뷰가 완전히 사라지지 않고, 최소 높이만큼은 화면 상단에 계속 남아있게 됩니다. 예를 들어, Toolbar는 계속 남아있고 그 위에 있던 다른 뷰들만 사라지게 하는 효과를 만들 수 있습니다. CollapsingToolbarLayout에서 주로 사용됩니다.

    예시 조합: app:layout_scrollFlags="scroll|exitUntilCollapsed"

  • snap

    이 플래그는 스크롤이 끝났을 때 뷰가 '어중간하게' 걸쳐 있는 상태를 방지합니다. 만약 뷰가 50% 이상 보이면 완전히 펼쳐지고, 50% 미만으로 보이면 완전히 사라지도록 자동으로 애니메이션을 적용합니다. 사용자에게 더 깔끔하고 정돈된 느낌을 줍니다.

    예시 조합: app:layout_scrollFlags="scroll|enterAlways|snap"

이러한 플래그들을 조합하여, 앱의 콘텐츠와 디자인에 가장 적합한 동적 앱 바를 설계할 수 있습니다. 예를 들어, 뉴스 피드처럼 빠른 액션 접근이 중요한 앱에서는 scroll|enterAlways|snap 조합이 유용할 것이고, 프로필 화면처럼 상단 이미지를 강조하고 싶을 때는 scroll|exitUntilCollapsed 조합이 더 적합할 것입니다.

4. 시각적 매력을 더하다: CollapsingToolbarLayout

CollapsingToolbarLayoutAppBarLayout의 기능을 한 단계 더 끌어올려, 스크롤에 따라 앱 바가 아름답게 축소/확장되는 'Collapsing Toolbar' 효과를 쉽게 구현할 수 있도록 도와주는 특수한 FrameLayout입니다.

4.1. CollapsingToolbarLayout의 개념과 구조

CollapsingToolbarLayoutAppBarLayout의 직접적인 자식으로 배치되며, 주로 Toolbar와 함께 배경 역할을 할 ImageView를 감싸는(wrapping) 형태로 사용됩니다. 사용자가 콘텐츠를 아래로 스크롤하면 CollapsingToolbarLayout은 높이가 점차 줄어들면서 최종적으로는 표준 Toolbar 크기로 축소됩니다. 이 과정에서 내부에 있는 뷰들의 위치, 크기, 투명도 등이 자연스럽게 변하는 애니메이션이 적용됩니다.

일반적인 구조는 다음과 같습니다.

CoordinatorLayout > AppBarLayout > CollapsingToolbarLayout > (ImageView + Toolbar)

4.2. 스크롤에 따라 변하는 헤더 이미지 구현하기

사용자 프로필, 이벤트 상세 페이지 등에서 흔히 볼 수 있는, 스크롤 시 상단 이미지가 작아지며 Toolbar가 되는 효과를 CollapsingToolbarLayout으로 구현할 수 있습니다.


<androidx.coordinatorlayout.widget.CoordinatorLayout ...>

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="250dp" <!-- 확장되었을 때의 전체 높이 -->
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary" <!-- 축소되었을 때 Toolbar 배경색 -->
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:toolbarId="@id/toolbar"> <!-- 어떤 Toolbar와 연결될지 지정 -->

            <ImageView
                android:id="@+id/header_image"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                android:src="@drawable/header_background"
                app:layout_collapseMode="parallax" /> <!-- 스크롤 시 시차 효과 -->

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" /> <!-- 축소 시 상단에 고정 -->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        
        <!-- 스크롤될 콘텐츠 -->
        
    </androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

4.3. 주요 속성 분석

위 예제 코드에 사용된 CollapsingToolbarLayout의 핵심 속성들은 다음과 같습니다.

  • app:layout_scrollFlags="scroll|exitUntilCollapsed": CollapsingToolbarLayout 자체에 이 플래그를 설정하여, 스크롤 시 Toolbar 높이만큼(exitUntilCollapsed) 남기고 축소되도록 합니다.
  • app:contentScrim="?attr/colorPrimary": 레이아웃이 특정 임계값 이상으로 축소되었을 때(즉, Toolbar 크기가 되었을 때) 표시될 배경색입니다. 이미지가 사라지면서 단색 배경으로 부드럽게 전환되는 효과를 줍니다.
  • app:toolbarId="@id/toolbar": CollapsingToolbarLayout이 어떤 Toolbar를 최종적으로 화면 상단에 고정시킬지 알려주는 중요한 속성입니다.
  • app:expandedTitleMarginStart, app:expandedTitleMarginEnd, app:expandedTitleMarginBottom: 레이아웃이 완전히 확장되었을 때 제목 텍스트의 여백을 지정합니다.
  • app:collapsedTitleTextAppearance, app:expandedTitleTextAppearance: 축소/확장 상태일 때 제목 텍스트의 스타일(크기, 색상 등)을 별도로 지정할 수 있습니다.

또한, CollapsingToolbarLayout의 자식 뷰들은 app:layout_collapseMode 속성을 통해 축소/확장 시 어떻게 동작할지 결정할 수 있습니다.

  • pin: 뷰가 축소될 때 상단에 고정됩니다. Toolbar에 이 속성을 설정하여 항상 상단에 보이도록 합니다.
  • parallax: 뷰가 스크롤될 때 약간의 시차(parallax) 효과를 주며 스크롤 아웃됩니다. 주로 배경 이미지(ImageView)에 사용하여 깊이감을 더합니다. app:layout_collapseParallaxMultiplier 속성(0.0 ~ 1.0)으로 시차 효과의 강도를 조절할 수 있습니다.
  • none (기본값): 일반적인 스크롤 동작을 따릅니다.

4.4. 완전한 예제: CoordinatorLayout부터 Toolbar까지

이제 모든 요소를 종합하여, 동적으로 변화하는 제목을 가진 Collapsing Toolbar를 완성해 보겠습니다.

레이아웃 (activity_profile.xml)

위의 XML 코드와 거의 동일합니다. `AppBarLayout`의 높이를 300dp 정도로 넉넉하게 주고, `NestedScrollView` 안에 충분한 양의 텍스트를 넣어 스크롤이 가능하도록 합니다.

액티비티 (ProfileActivity.kt)


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import com.google.android.material.appbar.CollapsingToolbarLayout

class ProfileActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)

        val toolbar: Toolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true) // Up 버튼 추가

        val collapsingToolbarLayout: CollapsingToolbarLayout = 
            findViewById(R.id.collapsing_toolbar_layout)
        
        // 확장되었을 때와 축소되었을 때의 제목을 설정
        collapsingToolbarLayout.title = "User Profile"
    }

    // Up 버튼 클릭 처리
    override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }
}

이렇게 코드를 작성하고 앱을 실행하면, 처음에는 "User Profile"이라는 제목이 확장된 CollapsingToolbarLayout의 하단에 크게 표시됩니다. 화면을 위로 스크롤하면, 배경 이미지는 시차 효과와 함께 위로 사라지고, 제목 텍스트는 크기가 작아지며 자연스럽게 Toolbar 위치로 이동하여 최종적으로 Toolbar의 제목이 됩니다. 이 모든 복잡한 애니메이션을 CollapsingToolbarLayout이 자동으로 처리해 줍니다.

5. 완성도를 높이는 고급 기법 및 권장사항

기능 구현을 넘어, 사용자가 만족할 만한 고품질의 앱을 만들기 위해서는 디자인의 일관성, 최신 트렌드 반영, 그리고 모든 사용자를 위한 접근성 고려가 필수적입니다.

5.1. 테마와 스타일: 일관성 있는 디자인 적용하기

앱 전체의 Toolbar에 일관된 스타일을 적용하기 위해 매번 XML 속성을 반복해서 작성하는 것은 비효율적입니다. 스타일과 테마를 활용하면 중앙에서 앱 바의 디자인을 관리할 수 있습니다.

Toolbar 스타일 정의하기

res/values/styles.xml (또는 themes.xml)에 Toolbar 전용 스타일을 만듭니다.


<style name="MyToolbarStyle" parent="Widget.MaterialComponents.Toolbar.Primary">
    <item name="android:background">@color/my_app_primary</item>
    <item name="titleTextColor">@color/white</item>
    <item name="subtitleTextColor">@color/white_alpha_70</item>
</style>

앱 테마에 Toolbar 스타일 적용하기

앱의 기본 테마에 toolbarStyle 속성을 사용하여 위에서 정의한 스타일을 적용합니다. 이렇게 하면 앱 내의 모든 Toolbar에 기본적으로 이 스타일이 적용됩니다.


<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    ...
    <item name="toolbarStyle">@style/MyToolbarStyle</item>
    ...
</style>

테마 오버레이(Theme Overlay)

앞서 Toolbar XML에서 사용했던 android:themeapp:popupTheme은 '테마 오버레이'의 한 예입니다. 이는 특정 뷰와 그 자식 뷰들에게만 부분적으로 다른 테마를 적용하는 강력한 기능입니다. 예를 들어, 앱의 전반적인 테마는 밝지만(Light), Toolbar 영역만큼은 어두운 배경에 밝은 아이콘(Dark)을 사용하고 싶을 때 android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"를 사용합니다. 이는 Toolbar의 아이콘(내비게이션 아이콘, 오버플로우 메뉴 아이콘 등) 색상을 자동으로 적절하게 바꿔줍니다. app:popupTheme은 이 Toolbar에서 생성되는 팝업 메뉴의 테마를 지정하는 역할을 합니다.

5.2. Material 3와 TopAppBar의 변화

최신 안드로이드 버전은 Material Design 3(Material You)를 적극적으로 도입하고 있습니다. 이에 따라 앱 바 컴포넌트에도 변화가 생겼습니다.

  • 새로운 컴포넌트: Material 3에서는 com.google.android.material.appbar.MaterialToolbar를 사용하는 것이 권장됩니다. 이 컴포넌트는 Material 3의 디자인 시스템(색상, 타이포그래피, 모양)을 더 잘 지원합니다.
  • 공식 명칭 변경: 이제 'App Bar'보다는 'Top App Bar'라는 용어가 공식적으로 사용됩니다.
  • 다양한 스타일: Material 3는 여러 종류의 Top App Bar를 정의합니다.
    • Center-aligned: 제목이 중앙에 위치하는 스타일입니다. 짧은 제목을 가진 화면에 적합합니다.
    • Small: 표준적인 Top App Bar입니다.
    • Medium & Large: CollapsingToolbarLayout과 유사하게, 스크롤 시 축소되는 큰 헤더를 가진 Top App Bar입니다. CollapsingToolbarLayout보다 더 간단하게 구현할 수 있는 방법을 제공합니다.
  • 스타일링: XML에서 style 속성을 통해 간단하게 스타일을 변경할 수 있습니다.
    <com.google.android.material.appbar.MaterialToolbar
            style="@style/Widget.Material3.Toolbar.Surface"
            ... />
        

새로운 프로젝트를 시작하거나 기존 프로젝트를 리팩토링한다면, Material 3의 가이드라인과 MaterialToolbar를 사용하는 것을 적극적으로 고려해야 합니다. 이는 사용자에게 더 현대적이고 일관된 경험을 제공하는 데 도움이 됩니다.

5.3. 접근성(Accessibility) 고려사항

모든 사용자가 앱을 원활하게 사용할 수 있도록 접근성을 고려하는 것은 매우 중요합니다.

  • 콘텐츠 설명 (Content Description): 아이콘만으로 이루어진 메뉴 아이템이나 내비게이션 아이콘에는 반드시 android:contentDescription 속성을 추가해야 합니다. 스크린 리더(예: TalkBack)는 이 설명을 읽어 시각 장애가 있는 사용자에게 해당 버튼의 기능을 알려줍니다.
  • <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search"
        android:title="Search"
        android:contentDescription="Start a new search"
        app:showAsAction="ifRoom" />
    
  • 터치 영역 확보: 아이콘 버튼의 크기는 최소 48dp x 48dp의 터치 영역을 확보하는 것이 좋습니다. 아이콘 자체의 크기가 작더라도, 패딩을 추가하여 사용자가 쉽게 누를 수 있도록 해야 합니다.
  • 명도 대비: 앱 바의 배경색과 아이콘/텍스트 색상은 충분한 명도 대비를 가져야 합니다. WCAG (Web Content Accessibility Guidelines) 2.0에서는 일반 텍스트의 경우 4.5:1, 큰 텍스트의 경우 3:1 이상의 명도 대비를 권장합니다.

6. 결론: 사용자 경험을 완성하는 앱 바 디자인

안드로이드 앱 바는 ActionBar의 제약에서 벗어나, Toolbar라는 유연한 뷰그룹으로 진화했습니다. 개발자는 이제 단순히 제목과 메뉴를 나열하는 것을 넘어, 앱의 정체성을 표현하고 사용자와 동적으로 상호작용하는 풍부한 경험을 설계할 수 있게 되었습니다.

Toolbar의 기본적인 커스터마이징부터 CoordinatorLayout, AppBarLayout, CollapsingToolbarLayout을 조합하여 만드는 역동적인 스크롤 효과에 이르기까지, 앱 바를 다루는 기술은 현대 안드로이드 UI 개발의 핵심 역량 중 하나입니다. 각각의 컴포넌트가 어떤 역할을 하고 어떻게 유기적으로 연결되는지를 이해하는 것이 중요합니다.

궁극적으로 좋은 앱 바는 단순히 보기 좋은 것을 넘어, 사용자가 앱의 구조를 쉽게 이해하고 원하는 기능에 막힘없이 접근할 수 있도록 돕는 역할을 합니다. 이 글에서 다룬 개념과 기법들을 바탕으로, 사용자의 여정을 세심하게 고려한 직관적이고 매력적인 앱 바를 구현하여 앱의 완성도를 한 단계 높이시길 바랍니다.


0 개의 댓글:

Post a Comment