
1. Bottom Sheet Dialog
1-1. 개념
1-2. 종류와 차이
2. Modal Bottom Sheet
2-1. 기본 예제
2-2. 모서리가 둥근 예제
2-3. 버튼이 있는 예제
2-4. Modal 위에 텍스트
2-5. RadioButton 사용 예제
3. Persistent Bottom Sheet
3-1. 기본 예제
3-2. 활용 예제
4. 알아두면 좋은 정보
4-1. Expanding Bottom Sheet
4-2. UI/UX
5. 전체 코드
1. Bottom Sheet Dialog
1-1. 개념

앱을 사용하다 보면 위 사진처럼 아래에서 빼꼼하고 나오는 창이 있다.
화면 가운데에 뜨는 Dialog와 별도로 이 창의 정식 명칭은 Bottom Sheet Dialog이다.
개인적으로 앱 처음 들어갔을 때 광고나 푸시 알림을 허용해주세요! 같은 용도로 많이 봤던 것 같다.
1-2. 종류와 차이
- Modal Bottom Sheet



- Persistent Bottom Sheet



Bottom Sheet Dialog는
Modal Bottom Sheet와 Persistenet Bottom Sheet 이렇게 2가지 종류로 나뉜다.
겉으로만 봤을 때는 무슨 차이가 있는지 가늠이 가지 않으니 다음 표를 통해 차이를 알아보자.
| Modal Bottom Sheet | Persistent Bottom Sheet |
| 인라인 메뉴나 간단한 대화 상자의 대안이다. 기존 콘텐츠와 상호 작용하려면 Modal Bottom Sheet를 종료해야 한다. |
사용자가 기존 화면에 있는 콘텐츠와 상호작용 하면서 Persistent Bottom Sheet도 같이 볼 수 있다. |
| Activity나 Fragment에 속해있지 않으며 요구에 따라 동적으로 화면에 나타난다. (마치 토스트 메시지 처럼) |
Activity나 Fragment 레이아웃에 속해있다. (항상 앱의 하단에 표시되어 컨텐츠를 보여줌) |
| 다른 컴포넌트보다 높은 elevation(높이)를 가져 사용자의 주목을 끌도록 유도한다. |
화면에 존재하는 다른 컴포넌트와 동일한 elevation(높이)를 가진다. |
| Bottom Sheet Dialog Fragment를 상속받아 사용한다. | 화면 내 다른 컴포넌트와 상호작용하기 위해 CoordinatorLayout을 최상단 레이아웃으로 사용한다. |
| Dialog를 열고 닫는 것 밖에 할 수 없다. | 더 많은 정보를 얻기 위해 위로 드래그를 간략한 정보를 얻기 위해서는 아래로 드래그를 한다. |
| 따로 상태가 없다. (열고 닫기가 끝) |
접기, 펼치기, 드래그, 숨기기, 고정과 같은 상태를 컨트롤 할 수 있다. (리스너도 있다) |
영문 문서를 보고 번역했더니 뭔가 조금 어색한 것 같아서 추가 설명을 하자면
우선, Modal은 다이얼로그를 종료해야만 그 뒤에 있는 화면을 사용할 수 있다.
(그래서 Modal 뒤쪽이 불투명한 회색인 것이 특징이다)
반면 Persistent는 위쪽에 있는 화면도 사용하면서 Persistent도 동시에 사용이 가능하다.
또, Modal은 토스트 메시지와 같아서 아무 곳에서나 띄울 수 있다.
Persistent는 그 Activity(혹은 Framgent) 내에 구현한 하나의 View이기 때문에 그 화면에서만 띄울 수 있다.
그리고 Modal은 열고 닫기가 끝인데
Persistent는 살짝 접을 수도 있고 많이 펼칠 수도 있고 여러 상태 조절이 가능하다.
자, 말로만 들어서는 감이 잘 오지 않을 테니 이제 예제와 함께 알아보자.
예제와 필요한 코드의 수가 많기 때문에
설명이 필요한 부분만 포스팅에 작성해두었습니다.
전체 코드가 필요하신 분은 [목차 5]에 있는 Repository를 참고해주세요.
2. Modal Bottom Sheet
2-1. 기본 예제

// ModalBottomSheet.kt
class ModalBottomSheet : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.bottom_sheet_modal, container, false)
}
companion object {
const val TAG = "BasicBottomModalSheet"
}
}
// MainActivity에 작성하여 호출해주세요.
private fun modalBottomSheet() {
val modal = ModalBottomSheet()
modal.show(supportFragmentManager, ModalBottomSheet.TAG)
}
Modal의 가장 기본적인 사용 방법이다.
BottomSheetDialogFragment를 상속받아 띄우고 싶은 xml을 inflate 해주면 된다.
2-2. 모서리가 둥근 예제

<!-- white_round_top_border_20.xml -->
<!-- drawable 폴더에 만들어 주세요 -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
</shape>
<!-- res - values - themes - themes.xml -->
<!-- Modal에 둥근 모서리를 주기 위해 style 정의해주세요 -->
<resources>
...
<style name="RoundCornerBottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
<item name="bottomSheetStyle">@style/RoundCornerModalStyle</item>
</style>
<style name="RoundCornerModalStyle" parent="Widget.Design.BottomSheet.Modal">
<item name="android:background">@drawable/white_round_top_border_20</item>
</style>
...
</resources>
// MainActivity에 작성하여 호출해주세요.
private fun modalWithRoundCorner() {
val modal = ModalBottomSheet()
modal.setStyle(DialogFragment.STYLE_NORMAL, R.style.RoundCornerBottomSheetDialogTheme)
modal.show(supportFragmentManager, ModalBottomSheet.TAG)
}
Modal에 둥근 모서리를 주는 UI는 굉장히 흔하다.
둥근 모서리를 주기 위해서는 drawable을 만들고 이를 사용하는 style을 만들어
코드 상에서 setStyle로 theme를 지정해주면 된다.
xml 자체에서 할 수 있으면 참 좋겠으나 나도 여러 방면으로 시도해봤지만 안된다 🤔
2-3. 버튼이 있는 예제

class ModalExample1 : BottomSheetDialogFragment() {
lateinit var binding: ModalExample1Binding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = ModalExample1Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvReject.apply {
paintFlags = Paint.UNDERLINE_TEXT_FLAG // 밑줄
setOnClickListener {
dismiss()
}
}
binding.ivClose.setOnClickListener {
dismiss()
}
binding.btnEventOn.setOnClickListener {
dismiss()
}
}
companion object {
const val TAG = "ModalExample1"
}
}
버튼이 있는 경우는 일반 프래그먼트 사용하듯이 onViewCreated 안에서 처리해주면 된다.
Modal을 종료하고 싶을 땐 dismiss를 호출하면 된다.
xml 코드는 너무 긴 관계로 [목차 5]의 repository에서 확인할 수 있다.
2-4. Modal 위에 텍스트

<?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="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<!-- 이 레이아웃은 여기서 투명하게 만들 필요가 없습니다 (애초에 불가능) -->
<!-- 코드상에서 직접 style을 적용해 줄 거예요 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_reject_today"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="오늘 하루 보지 않기"
android:textColor="@color/white"
tools:textColor="@color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="닫기"
android:textColor="@color/white"
tools:textColor="@color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 텍스트 아래에 들어갈 View의 background를 둥근 모서리로 지정해주세요 -->
<!-- 들어갈 View가 여러 개라면 layout이나 cardView로 감싸 모서리를 둥글게 해주세요 -->
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@drawable/white_round_top_border_20"
app:layout_constraintTop_toBottomOf="@+id/constraintLayout"
app:srcCompat="@drawable/bg_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- res - values - themes - themes.xml -->
<!-- Modal의 배경을 투명하게 만들기 위해 style 정의해주세요 -->
<resources>
...
<style name="TransParentBottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
<item name="bottomSheetStyle">@style/TransParentModalStyle</item>
</style>
<style name="TransParentModalStyle" parent="Widget.Design.BottomSheet.Modal">
<item name="android:background">@android:color/transparent</item>
</style>
...
</resources>
// MainActivity에 작성하여 호출해주세요.
private fun modalExample2() {
val modal = ModalExample2()
modal.setStyle(DialogFragment.STYLE_NORMAL, R.style.TransParentBottomSheetDialogTheme)
modal.show(supportFragmentManager, ModalExample2.TAG)
}
Modal 위에 텍스트를 올리는 UI도 자주 보여서 만들어 봤다.
인터넷에 예제가 있나 찾아봤지만 못 찾았고... 나름대로 연구해서 만들어봤는데 정석 방법인지는 모르겠다.
style로 투명한 배경을 주는 방법이고 이유는 모르겠으나 xml에서 직접 background를 투명하게 주면 먹히지 않는다.
style로 적용을 해줘야한다.
Activity와 Framgnet 코드는 생략
2-5. RadioButton 사용 예제

// 선택한 옵션 값을 유지하기 위해 싱글톤으로 만듦
object ModalExample3 : BottomSheetDialogFragment() {
lateinit var binding: ModalExample3Binding
private val list = arrayListOf (
Sort(R.drawable.ic_chart, "판매 인기 순"),
Sort(R.drawable.ic_down, "낮은 가격 순"),
Sort(R.drawable.ic_up, "높은 가격 순"),
Sort(R.drawable.ic_star, "상품평 많은 순"),
Sort(R.drawable.ic_cumulative_sales, "누적 판매 순")
)
private val sortAdapter = SortAdapter(list)
const val TAG = "ModalExample3"
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = ModalExample3Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvSort.apply {
layoutManager = LinearLayoutManager(context)
adapter = sortAdapter
sortAdapter.setItemClickListener(object : SortAdapter.ItemClickListener{
override fun onClick(position: Int) {
// 선택한 옵션(position)으로 원하는 작업
dismiss()
}
})
}
binding.ivClose.setOnClickListener {
dismiss()
}
}
}
Modal과 radiobutton을 사용하는 경우도 쇼핑몰 앱 등에서 종종 볼 수 있다.
사용 방법은 특별할 건 없고 recylcerview랑 radiobutton 사용법만 추가된 형태이다.
adapter와 xml 등의 코드는 repository에서 확인할 수 있다.
3. Persistent Bottom Sheet
3-1. 기본 예제

class PersistentActivity : AppCompatActivity() {
private lateinit var binding: ActivityPersistentBinding
lateinit var behavior: BottomSheetBehavior<LinearLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPersistentBinding.inflate(layoutInflater)
setContentView(binding.root)
initEvent()
}
private fun initEvent() {
persistentBottomSheetEvent()
binding.btnCollapsed.setOnClickListener {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
binding.btnExpanded.setOnClickListener {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
private fun persistentBottomSheetEvent() {
behavior = BottomSheetBehavior.from(binding.persistentBottomSheet)
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
// 슬라이드 되는 도중 계속 호출
// called continuously while dragging
Log.d(TAG, "onStateChanged: 드래그 중")
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
when(newState) {
BottomSheetBehavior.STATE_COLLAPSED-> {
Log.d(TAG, "onStateChanged: 접음")
}
BottomSheetBehavior.STATE_DRAGGING-> {
Log.d(TAG, "onStateChanged: 드래그")
}
BottomSheetBehavior.STATE_EXPANDED-> {
Log.d(TAG, "onStateChanged: 펼침")
}
BottomSheetBehavior.STATE_HIDDEN-> {
Log.d(TAG, "onStateChanged: 숨기기")
}
BottomSheetBehavior.STATE_SETTLING-> {
Log.d(TAG, "onStateChanged: 고정됨")
}
}
}
})
}
private val TAG = "PersistentActivity"
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Persistent Bottom Sheet과의 상호작용을 위해 CoordinatorLayout 사용(필수) -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
tools:context=".persistent.PersistentActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp">
<Button
android:id="@+id/btn_collapsed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="20dp"
android:text="collapsed" />
<Button
android:id="@+id/btn_expanded"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="20dp"
android:text="expanded" />
</LinearLayout>
<!-- Persistent Bottom Sheet -->
<!-- behavior 속성을 여기서 적용합니다 -->
<LinearLayout
android:id="@+id/persistent_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#ebebeb"
android:orientation="vertical"
android:padding="16dp"
app:behavior_hideable="false"
app:behavior_peekHeight="100dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<!-- persistent bottom sheet의 Content -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔼 Pull me up! 🔼"
android:textSize="24sp"
android:gravity="center"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Persistent Bottom Sheet"
android:textSize="24sp"
android:paddingTop="100dp"
android:gravity="center"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
설명은 주석으로 다 달아두었고... 유의할 점 몇 가지 적어보자면
Modal과 다른 점은 별도의 BottomSheetDialogFragment를 생성하는 게 아니라
Activity내에 다른 view들과 함께 들어간다는 것.
그래서 다른 view들과의 상호작용을 위해 Coordinator로 감싸야한다는 것
Persistent Bottom Sheet이라는 게 별게 아니라 LinearLayout이든 뭐든 거기에 behavior 속성을 주면 된다는 것
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" 는 꼭 적어야 한다는 것
이 점들을 유의하자.
그리고 여러 behavior 속성을 사용할 수 있는데 그 종류는 다음과 같다.
| 속성 | 설명 | 기본 값 |
| behavior_hideable | 아래로 드래그 시 view를 숨길지 | false |
| behavor_skipCollapsed | view를 숨길 때 접히는 상태를 무시할 지 | false |
| behavior_draggable | 드래그로 view를 펼치고 접을지 | true |
| behavior_fitToContents | 펼쳐진 뷰의 높이가 content를 감쌀지 | true |
| behavior_halfExpandedRatio | 절반만 펼쳐졌을 경우 뷰의 높이 | 0.5 |
| behavior_expandedOffset | 완전히 펼쳐진 상태일 때 뷰의 오프셋 | 0dp |
| behaviro_peekHeight | 뷰가 접힌 상태에서의 높이 | auto |
3-2. 활용 예제

Persistent를 활용해본 예제이다.
네이버 지도와 똑같이 Bottom Sheet를 펼쳤을 때 상세 페이지로 넘어가게 구현했다.
딱히 어려운 기술은 없고 단순히 코드만 길어서 따로 여기에 코드를 첨부하진 않았다.
Repository에서 자세한 코드를 확인할 수 있다.
4. 알아두면 좋은 정보
4-1. Expanding Bottom Sheet

사실 Google Material 문서에서는 Bottom Sheet Dialog의 종류를 하나 더 소개하고 있다.
하지만 개인적으로 사용해본 경험이 거의 없고 사용 빈도가 낮은 것 같아 포스팅에서 소개하지 않았다.
만약 자세히 알아보고 싶다면 여기와 여기를 참고하면 된다. (공식문서 링크임)
그리고 블로거분들 중에서 Expanding Bottom Sheet와 비슷한 느낌으로 구현하신 분도 계셔서 링크를 남겨본다.
4-2. UI/UX


Google Material 공식문서에는 Bottom Sheet의 UI/UX에 대한 많은 정보를 얻을 수 있다.
이렇게는 디자인하지 마세요, 이런 사이즈로 제작하세요와 같은 가이드라인을 제시한다.
앱 개발자로서 읽어보면 좋은 정보들이니 읽어보길 추천한다.
5. 전체 코드
GitHub - juhwankim-dev/SelfStudy: 코틀린으로 공부한 것들을 올리는 공간입니다.
코틀린으로 공부한 것들을 올리는 공간입니다. Contribute to juhwankim-dev/SelfStudy development by creating an account on GitHub.
github.com
전체 코드가 필요하신 분들을 위해 Git Repository를 공유합니다 😊
Bottom Sheet Dialog 폴더에서 총 7가지의 예제를 확인하실 수 있습니다!
💡 느낀 점
- 사실 정리하기 전에는 Modal 종류 하나만 존재하는 줄 알았다. Persistent Bottom Sheet라는 것도 있었군...
- 나는 여태까지 Modal과 Persistent의 사용법을 혼용해서 사용하고 있었다. ㅠㅠ... 나는 멍청이~
- 주로 쓰이는 커스텀 예제들을 이번에 다 만들어놔서 앞으로 해커톤 할 때 더 빠르게 개발할 수 있을 것 같다.
- Google Material을 보면 Modal 사용 시 하지 말아야 할 행동으로 뒷 배경을 투명하게 하는 것(불투명하게 하지 않는 것)을 소개하고 있다. 근데 페이코 앱을 보면... 이를 어기고 있다. 기본 값이 아닌 추가적인 코드를 작성해야 바꿀 수 있었을 텐데 굳이 바꾼 이유가 뭘까??? UI/UX도 진짜 제대로 공부해보고 싶다.
📘 참고한 자료
'오늘은 뭘 배울까? > Android' 카테고리의 다른 글
| 16KB 페이지 크기 지원 - 왜 하는거고 어떻게 하는 것인가? (3) | 2025.11.02 |
|---|---|
| 주니어 개발자가 DTO 설계에서 놓치기 쉬운 실수들 (1) | 2024.10.19 |
| 안드로이드 다이얼로그 만들기(Custom Dialog까지) (7) | 2022.09.09 |
| withContext는 무엇이며 async와 무슨 차이가 있을까? (6) | 2022.09.07 |
| Coroutine Dispatcher, 넌 대체 뭐야? (2) | 2022.08.31 |
댓글