Jetpack Compose 초심자 가이드 6: Scaffold로 앱 기본 골격 구성하기 🚀

윤성현·2025년 6월 10일
post-thumbnail

📌 개요

이전 글에서는 버튼 클릭, 텍스트 입력, 제스처 인식 등 다양한 사용자 입력 이벤트를 처리하는 방법을 배웠습니다. 개별 컴포저블들을 다루는 방법을 익혔으니, 이제는 이들을 조합해서 실제 앱처럼 보이는 완성된 화면을 구성해볼 차례입니다.

Text나 Button만으로도 앱을 만들 수 있지만, 많은 상용 앱들을 보면 상단 앱바, 하단 네비게이션, 플로팅 액션 버튼 등 표준적인 UI 구조를 활용하고 있습니다. 이번 글에서는 Jetpack Compose에서 이런 앱의 기본 골격을 구성하는 핵심 도구인 Scaffold를 중심으로, 체계적이고 일관성 있는 화면 레이아웃을 만드는 방법을 알아보겠습니다.

1. Scaffold란 무엇인가?

Scaffold는 앱 화면의 기본 구조를 제공하는 컴포저블입니다. 건축에서 '발판, 골조'를 의미하는 단어 그대로, UI의 골격 역할을 담당합니다.

기존 View 시스템에서는 Activity의 레이아웃을 XML로 정의하고, AppBar, Navigation Drawer, BottomNavigation 등을 각각 별도로 설정해야 했습니다. 하지만 Compose의 Scaffold는 이런 공통 UI 요소들을 한 곳에서 선언적으로 배치할 수 있게 해줍니다.

1-1. Scaffold의 핵심 장점

  • 구조적 일관성과 유지보수성
    • 모든 화면이 동일한 Scaffold 구조로 구성되어 일관성 확보
    • 앱바나 네비게이션 변경 시 한 곳만 수정하면 전체 앱에 반영
  • 자동 레이아웃 관리
    • TopBar, BottomBar, FAB가 서로 겹치지 않도록 자동 배치
    • 키보드가 올라올 때나 화면 회전 시 자동으로 레이아웃 조정
    • innerPadding으로 콘텐츠 영역이 다른 UI 요소와 겹치지 않도록 보장

1-2. Scaffold의 기본 구조

Scaffold는 다음과 같은 주요 슬롯(slot)들을 제공합니다.

슬롯설명주요 용도
topBar화면 상단 앱바제목, 뒤로가기 버튼, 메뉴 아이콘
bottomBar화면 하단 바탭 네비게이션, 액션 버튼들
floatingActionButton플로팅 액션 버튼주요 액션 (글쓰기, 추가 등)
snackbarHost스낵바 표시 영역알림, 피드백 메시지
content메인 콘텐츠 영역실제 화면 내용

2. Scaffold 사용하기

이제 Scaffold의 구성 요소들을 하나씩 추가해가며 시작해보겠습니다.

2-1. 기본 TopAppBar가 있는 Scaffold

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun ScaffoldExample() {
    Scaffold(
      topBar = {
        TopAppBar(
          title = { Text("My App") },
          colors = TopAppBarDefaults.topAppBarColors(
            containerColor = MaterialTheme.colorScheme.primaryContainer,
            titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
          )
        )
      }
    ) { innerPadding ->
        Text(
            text = "메인 콘텐츠 영역입니다",
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
        )
    }
}

코드 해설

  • topBarTopAppBar를 배치하여 상단에 제목을 표시
  • title 파라미터에는 주로 Text 컴포저블이 사용되지만, @Composable 함수이므로 Row, Icon, Image 등 다양한 컴포저블을 활용한 자유로운 구성도 가능
  • innerPadding은 Scaffold가 자동으로 계산해주는 패딩값으로, TopAppBar 높이만큼 콘텐츠가 밀려나지 않도록 보정해줌
  • padding(innerPadding)을 적용하면 콘텐츠가 TopAppBar와 겹치지 않게 됨

💡 innerPadding이 중요한 이유
Scaffold를 사용할 때 반드시 innerPadding을 콘텐츠에 적용해야 합니다. 그렇지 않으면 TopAppBar나 BottomBar와 내용이 겹쳐서 보이게 됩니다.

2-2. TopAppBar 커스터마이징

TopAppBar는 다양한 옵션으로 커스터마이징할 수 있습니다.

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults

@Composable
fun ScaffoldExample() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("My App") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer,
                    titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
                ),
                navigationIcon = {
                    IconButton(onClick = {/* 뒤로가기 */ }) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = "뒤로가기"
                        )
                    }
                },
                actions = {
                    IconButton(onClick = {/* 검색 */ }) {
                        Icon(
                            imageVector = Icons.Default.Search,
                            contentDescription = "검색"
                        )
                    }
                    IconButton(onClick = {/* 메뉴 */ }) {
                        Icon(
                            imageVector = Icons.Default.MoreVert,
                            contentDescription = "메뉴"
                        )
                    }
                }
            )
        }
    ) { innerPadding ->
        Text(
            text = "뒤로가기와 액션 버튼이 있는 앱바입니다",
            modifier = Modifier.padding(innerPadding)
        )
    }
}

코드 해설

위 코드에서는 기본 TopAppBar에 두 가지 구성 요소를 추가했습니다.

  • navigationIcon: 앱바 왼쪽에 배치되는 아이콘으로, 주로 뒤로가기나 메뉴(햄버거) 버튼으로 사용됩니다. IconButton으로 감싸서 클릭 이벤트를 처리할 수 있습니다.
  • actions: 앱바 오른쪽에 배치되는 액션 버튼들입니다. 검색, 메뉴, 설정 등 현재 화면에서 수행할 수 있는 주요 기능들을 배치합니다. 여러 개의 IconButton을 나열할 수 있으며, 공간이 부족하면 자동으로 오버플로우 메뉴로 처리됩니다.

2-3. BottomBar와 FAB 추가하기

이제 하단 바와 플로팅 액션 버튼을 추가해 좀 더 완성된 앱 구조를 만들어보겠습니다.

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
 fun ScaffoldExample() {
     Scaffold(
         topBar = {
             TopAppBar(
                 title = { Text("My App (문서 뷰어)") },
                 colors = TopAppBarDefaults.topAppBarColors(
                     containerColor = MaterialTheme.colorScheme.primaryContainer,
                     titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
                 ),
             )
         },
         bottomBar = {
             BottomAppBar {
                 Row(
                     modifier = Modifier.fillMaxWidth(),
                     horizontalArrangement = Arrangement.SpaceEvenly
                 ) {
                     IconButton(onClick = {/* 편집 */ }) {
                         Icon(Icons.Default.Edit, contentDescription = "편집")
                     }
                     IconButton(onClick = {/* 공유 */ }) {
                         Icon(Icons.Default.Share, contentDescription = "공유")
                     }
                     IconButton(onClick = {/* 삭제 */ }) {
                         Icon(Icons.Default.Delete, contentDescription = "삭제")
                     }
                 }
             }
         },
         floatingActionButton = {
             FloatingActionButton(
                 onClick = {/* 새 문서 추가 */ }
             ) {
                 Icon(Icons.Default.Add, contentDescription = "새 문서")
             }
         }
     ) { innerPadding ->
         Text(
             text = "TopBar, BottomBar, FAB가 모두 있는 구조입니다.\n현재 문서에 대한 액션들이 BottomAppBar에 배치되어 있습니다",
             modifier = Modifier.padding(innerPadding)
         )
     }
 }

코드 해설

위 코드에서는 BottomAppBar와 FloatingActionButton이 추가되었습니다.

  • BottomAppBar: 현재 화면(문서)에서 수행할 수 있는 액션들을 배치 - 편집, 공유, 삭제 등
  • BottomAppBar + Row 조합: 하단 바 내부에 Row를 배치하고 Arrangement.SpaceEvenly를 적용해 아이콘 버튼들을 화면 전체 너비에 걸쳐 균등하게 분산 배치
  • fillMaxWidth(): Row가 BottomAppBar의 전체 너비를 사용하도록 설정
  • FloatingActionButton: 화면의 주요 액션(추가)을 담당하는 플로팅 버튼으로, BottomAppBar 위쪽에 자동으로 배치됨

구성 요소별 상세 역할

구성 요소특징사용 예시배치 규칙
BottomAppBar + Row하단 고정 툴바, 균등 분산 배치편집, 공유, 삭제, 즐겨찾기 등 현재 콘텐츠 관련 액션화면 하단에 고정, 여러 버튼 배치 가능
FloatingActionButton주요 액션 버튼새 글쓰기, 사진 촬영, 전화걸기 등 핵심 기능기본적으로 우하단 배치, BottomBar 위로 자동 조정

구성 요소별 상세 역할

레이아웃 자동 조정 기능

  • FAB는 기본적으로 오른쪽 하단에 배치되지만, BottomBar가 있으면 자동으로 BottomBar 위쪽으로 이동하여 겹치지 않게 됩니다
  • 키보드가 올라오거나 스낵바가 표시될 때도 자동으로 위치가 조정되어 사용성을 보장합니다
  • Scaffold가 이런 복잡한 레이아웃 계산을 자동으로 처리해주므로 개발자는 UI 구성에만 집중할 수 있습니다

3. BottomAppBar vs NavigationBar

하단 영역에는 두 가지 주요 선택지가 있으며, 각각은 서로 다른 목적과 사용 사례를 가지고 있습니다. 어떤 것을 선택할지는 앱의 구조와 사용자 경험 설계에 따라 달라집니다.

3-1. BottomAppBar (액션 버튼들)

BottomAppBar는 2-3에서 본 예시와 같이, 현재 화면에서 수행할 수 있는 액션들을 배치하는 영역입니다. 사진 편집 앱의 자르기-필터-조정-저장 버튼이나, 문서 뷰어의 공유-북마크-검색-인쇄 같은 기능들을 생각하면 됩니다.

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign

@Composable
fun ScaffoldExample() {
    Scaffold(
        bottomBar = {
            BottomAppBar {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceEvenly,
                ) {
                    IconButton(onClick = { /* 편집 */ }) {
                        Icon(Icons.Default.Edit, contentDescription = "편집")
                    }
                    IconButton(onClick = { /* 공유 */ }) {
                        Icon(Icons.Default.Share, contentDescription = "공유")
                    }
                    IconButton(onClick = { /* 삭제 */ }) {
                        Icon(Icons.Default.Delete, contentDescription = "삭제")
                    }
                }
            }
        },
    ) { innerPadding ->
        Box(
            modifier =
                Modifier
                    .fillMaxSize()
                    .padding(innerPadding),
            contentAlignment = Alignment.Center,
        ) {
            Text(
                text = "현재 화면에서 수행할 수 있는\n액션을 배치하는 BottomAppBar 예시입니다.",
                textAlign = TextAlign.Center,
            )
        }
    }
}

BottomAppBar의 특징과 장점

  • 컨텍스트 액션: 현재 보고 있는 콘텐츠와 직접 관련된 액션들만 표시
  • 유연한 배치: RowArrangement를 활용해 버튼 배치를 자유롭게 조정
  • FAB와 조화: FloatingActionButton과 함께 사용할 때 자동으로 레이아웃 조정
  • 동적 변경: 화면 상태나 선택된 항목에 따라 버튼 구성을 동적으로 변경 가능

BottomAppBar 구성 방법

  • 기본적으로 BottomAppBar 내부에 여러 IconButton을 배치
  • RowArrangement.SpaceEvenly로 버튼들을 균등하게 분산 배치
  • 텍스트가 필요한 경우 IconButton 대신 TextButton 사용 가능
  • 조건부 렌더링으로 상황에 따라 다른 액션 세트 표시

활용 팁

  • 너무 많은 액션을 배치하면 사용성이 떨어지므로, 주요 액션 3-5개 정도가 적절
  • 파괴적 액션(삭제 등)은 확인 다이얼로그와 함께 사용하는 것이 좋습니다
  • enabled 속성을 활용해 특정 조건에서만 버튼을 활성화할 수 있습니다

3-2. NavigationBar (탭 네비게이션)

NavigationBar는 여러 화면 간 이동을 위한 탭 구조로, 앱의 주요 섹션들을 빠르게 전환할 수 있게 해줍니다. Instagram의 홈-검색-릴스-쇼핑-프로필이나 Twitter의 홈-검색-알림-메시지 같은 구조를 생각하면 됩니다.

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

@Composable
fun ScaffoldExample() {
    var selectedTab by remember { mutableStateOf(0) }

    Scaffold(
        bottomBar = {
            NavigationBar {
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Home, contentDescription = null) },
                    label = { Text("홈") },
                    selected = selectedTab == 0,
                    onClick = { selectedTab = 0 }
                )
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Favorite, contentDescription = null) },
                    label = { Text("즐겨찾기") },
                    selected = selectedTab == 1,
                    onClick = { selectedTab = 1 }
                )
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Settings, contentDescription = null) },
                    label = { Text("설정") },
                    selected = selectedTab == 2,
                    onClick = { selectedTab = 2 }
                )
            }
        }
    ) { innerPadding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            contentAlignment = Alignment.Center
        ) {
            when (selectedTab) {
                0 -> Text(
                    text = "홈 화면",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold
                )
                1 -> Text(
                    text = "즐겨찾기 화면",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold
                )
                2 -> Text(
                    text = "설정 화면",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold
                )
            }
        }
    }
}

NavigationBar의 특징과 장점

  • 선택 상태 표시: selected 속성으로 현재 활성화된 탭을 시각적으로 강조
  • 아이콘 + 라벨: 직관적인 아이콘과 명확한 텍스트 라벨을 함께 제공
  • Material Design 준수: 표준 탭 네비게이션 패턴을 자동으로 따름
  • 상태 관리 간편: selectedTab 상태 하나로 전체 화면 전환 제어

NavigationBarItem 구성 요소

  • icon: 각 탭을 나타내는 아이콘 (필수)
  • label: 탭 이름을 표시하는 텍스트 (선택사항, 하지만 접근성을 위해 권장)
  • selected: 현재 선택된 탭인지 여부를 나타내는 Boolean 값
  • onClick: 탭 클릭 시 실행될 동작

활용 팁

  • 일반적으로 3-5개의 탭이 적절하며, 그 이상은 사용성이 떨어질 수 있습니다
  • 각 탭은 독립적인 화면 흐름을 가져야 하며, 서로 관련 없는 기능들을 분리하는 용도로 사용합니다
  • when 표현식을 사용해 선택된 탭에 따라 다른 컴포저블을 표시할 수 있습니다

3-3. 선택 기준

  • NavigationBar: 여러 화면/섹션을 오가는 앱 (Instagram, Twitter 등) NavigationBar를 선택해야 하는 경우
    • 다중 섹션 앱: 홈, 검색, 프로필 등 독립적인 화면 섹션이 여러 개 있는 경우
    • 정보 소비 앱: 뉴스, 소셜미디어, 쇼핑몰처럼 다양한 카테고리의 콘텐츠를 탐색하는 앱
    • 대시보드형 앱: 각 탭이 서로 다른 데이터나 기능을 제공하는 비즈니스 앱
    • 사용자 여정이 분리된 앱: 각 탭에서의 사용자 행동이 독립적인 경우
  • BottomAppBar: 현재 화면에서 다양한 액션이 필요한 앱 (에디터, 뷰어 등) BottomAppBar를 선택해야 하는 경우
    • 콘텐츠 중심 앱: 문서 뷰어, 이미지 갤러리, 동영상 플레이어 등
    • 편집/창작 도구: 그림 그리기, 텍스트 편집, 음악 제작 등의 크리에이티브 앱
    • 단일 화면 앱: 주로 하나의 메인 화면에서 다양한 액션을 수행하는 앱
    • 워크플로우 기반 앱: 특정 작업을 단계별로 수행해야 하는 생산성 앱

4. 실습 예제: 간단한 프로필 카드 앱 화면

지금까지 배운 내용을 종합해서 실제 앱처럼 보이는 프로필 카드 앱의 메인 화면을 만들어보겠습니다.

이번 예제에서는 다음과 같은 기능을 구현해볼 예정입니다.

  • TopAppBar: 앱 제목과 메뉴 기능
  • BottomAppBar: 편집, 공유 등 프로필 관련 액션들
  • FloatingActionButton: 프로필 수정 기능
  • 기본 컴포저블 조합: Text, Image, Button, Column, Row, Box를 활용한 프로필 화면

이를 통해 Scaffold의 모든 구성 요소를 활용해보고, 이전 글에서 배운 기본 컴포저블들을 실제로 조합하는 경험을 할 수 있습니다.

4-1. 예시화면

4-2. 전체 코드

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.composebeginnerguide.R

@Composable
fun ScaffoldExample() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        "내 프로필",
                        fontWeight = FontWeight.Bold
                    )
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer,
                    titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
                ),
                actions = {
                    IconButton(onClick = { /* 메뉴 */ }) {
                        Icon(
                            imageVector = Icons.Default.MoreVert,
                            contentDescription = "메뉴"
                        )
                    }
                }
            )
        },
        bottomBar = {
            BottomAppBar {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceEvenly
                ) {
                    IconButton(onClick = { /* 편집 */ }) {
                        Icon(Icons.Default.Edit, contentDescription = "편집")
                    }
                    IconButton(onClick = { /* 공유 */ }) {
                        Icon(Icons.Default.Share, contentDescription = "공유")
                    }
                    IconButton(onClick = { /* 좋아요 */}) {
                        Icon(Icons.Default.FavoriteBorder, contentDescription = "좋아요")
                    }
                }
            }
        },
        floatingActionButton = {
            FloatingActionButton(
                onClick = { /* 프로필 수정 */ }
            ) {
                Icon(Icons.Default.Edit, contentDescription = "프로필 수정")
            }
        }
    ) { innerPadding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            contentAlignment = Alignment.Center
        ) {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(16.dp),
                modifier = Modifier.padding(24.dp)
            ) {
                Image(
                    painter = painterResource(id = R.drawable.ic_launcher_background),
                    contentDescription = "프로필 사진",
                    modifier = Modifier
                        .size(120.dp)
                        .clip(CircleShape),
                    contentScale = ContentScale.Crop
                )

                Text(
                    text = "김개발자",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold
                )

                Text(
                    text = "Android Developer",
                    fontSize = 16.sp,
                    color = Color.Gray
                )

                Text(
                    text = "Jetpack Compose를 좋아합니다.\n새로운 기술을 배우고 성장하려고 노력합니다!",
                    fontSize = 14.sp,
                    textAlign = TextAlign.Center,
                    modifier = Modifier.padding(horizontal = 16.dp)
                )

                Row(
                    horizontalArrangement = Arrangement.spacedBy(32.dp)
                ) {
                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        Text(
                            text = "128",
                            fontSize = 20.sp,
                            fontWeight = FontWeight.Bold
                        )
                        Text(
                            text = "팔로워",
                            fontSize = 12.sp,
                            color = Color.Gray
                        )
                    }

                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        Text(
                            text = "42",
                            fontSize = 20.sp,
                            fontWeight = FontWeight.Bold
                        )
                        Text(
                            text = "좋아요",
                            fontSize = 12.sp,
                            color = Color.Gray
                        )
                    }

                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        Text(
                            text = "45",
                            fontSize = 20.sp,
                            fontWeight = FontWeight.Bold
                        )
                        Text(
                            text = "게시물",
                            fontSize = 12.sp,
                            color = Color.Gray
                        )
                    }
                }
            }
        }
    }
}

4-3. 코드 설명

  • Scaffold: 앱 화면의 기본 레이아웃을 구성하며 TopAppBar, BottomAppBar, FAB, 콘텐츠 영역을 함께 선언함
  • TopAppBar: 앱 제목과 메뉴 아이콘을 포함한 상단 앱바를 구성함
  • BottomAppBar: 편집, 공유, 좋아요 등 현재 프로필과 관련된 액션 버튼들을 균등하게 배치함
  • FloatingActionButton: 프로필 수정 기능을 담당하며 화면 우측 하단에 배치됨
  • Image + CircleShape: 원형 프로필 이미지를 표시함
  • Column + Row: 텍스트와 통계 정보를 정렬하여 화면 중앙에 배치함
  • Box: 전체 콘텐츠를 가운데 정렬하는 데 사용함
  • Modifier.padding(innerPadding): TopAppBar와 BottomAppBar에 겹치지 않도록 콘텐츠 영역의 여백을 자동으로 조정함

4-4. 실습 포인트

  • Scaffold는 앱의 전체 구조를 체계적으로 관리할 수 있게 해줍니다
  • innerPadding을 사용하면 콘텐츠가 TopBar/BottomBar와 겹치지 않습니다
  • 다양한 컴포저블을 조합하여 활용할 수 있습니다
  • 상태와 Scaffold를 결합하면 인터랙티브한 앱 화면을 쉽게 만들 수 있습니다

5. 정리

이번 글에서는 Jetpack Compose에서 앱 화면의 기본 구조를 잡아주는 핵심 컴포저블인 Scaffold를 집중적으로 살펴봤습니다. Scaffold를 사용하면 다음과 같은 장점이 있습니다.

  • 일관된 UI 구조: TopAppBar, BottomAppBar, FAB 등 주요 UI 요소를 쉽게 구성하고 유지보수가 용이합니다.
  • 자동 레이아웃 관리: Compose가 TopBar, BottomBar, FAB 등의 요소 간 레이아웃 충돌을 자동으로 처리해줍니다.
  • 구성 요소의 편리한 선언: 각각의 UI 컴포넌트를 개별적으로 선언적으로 배치할 수 있습니다.

또한 Scaffold의 핵심 슬롯(slot)들을 통해 다음의 UI 요소들을 배치했습니다.

슬롯설명주요 용도
topBar화면 상단 앱바제목, 뒤로가기 버튼, 액션 메뉴
bottomBar화면 하단 바탭 네비게이션, 액션 버튼들
floatingActionButton플로팅 액션 버튼새 글쓰기, 추가 등 핵심 액션
snackbarHost스낵바 표시 영역알림, 피드백 메시지
content메인 콘텐츠 영역실제 화면 콘텐츠

이를 통해 프로필 화면 구성 예제를 실습하면서 Scaffold의 실제 활용 방법을 익혔습니다.

또한 하단 UI를 구성할 때, NavigationBar와 BottomAppBar의 차이를 명확히 구분하고 사용 목적과 선택 기준을 설명했습니다.

  • NavigationBar: 여러 독립적인 화면을 전환하는 앱에 적합합니다.
  • BottomAppBar: 특정 화면의 콘텐츠 관련 액션들을 표시할 때 효과적입니다.

Compose로 화면 구조를 관리할 때 Scaffold를 제대로 활용하면 복잡한 UI 구성도 깔끔하고 유지보수하기 쉽게 만들어줄 수 있습니다.


🎯 다음 글 예고: Navigation을 이용한 화면 전환과 관리

이번 글을 통해 앱의 화면을 구성하는 방법을 익혔습니다. 다음 글에서는 Jetpack Compose 앱에서 여러 화면 간의 전환과 내비게이션 구조를 구성하는 방법을 배워볼 예정입니다.

Jetpack Compose에서 제공하는 NavHostNavController를 이용해 어떻게 화면 전환을 구현하고, 화면 간 데이터 전달 및 상태 관리를 할 수 있는지 단계적으로 학습할 예정입니다. 하나의 앱에서 여러 화면을 연결하고, 사용자의 이동 흐름을 어떻게 제어할 수 있는지 알아가보도록 합시다! 기대해주세요! 🚀

0개의 댓글