시간표와 공지사항 두가지 기능을 담을 예정이므로, 사이드바를 이용해서 페이지를 구분하도록 구현해보겠다.
기본 설정으로 생성한다.
build.gradle.kts
build.gradle.kts에 다음과 같이 의존성을 추가한다.
// Navigation Compose 추가
implementation(libs.androidx.navigation.compose) // 네비게이션 지원
// Material Icons 추가 (선택 사항)
implementation(libs.androidx.material.icons.core) // 기본 아이콘
implementation(libs.androidx.material.icons.extended) // 확장 아이콘
주석과 같이, 네비게이션을 지원하기 위한 종속성을 추가하였다. Material Icons는 Google의 Material Design 가이드라인에 따라 설계된 아이콘 세트를 제공하는 라이브러리이다. 기본 아이콘인 core는 홈, 뒤로가기, 설정 등의 아이콘을 제공한다.
확장 아이콘인 extended는 그 외 다양한 아이콘을 지원하는데, 당장 사용하진 않지만 함께 추가해주었다.
우선 기본 페이지를 구성해준다.
*Screen.kt
package com.example.scheschedule.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
// 홈 화면 UI를 정의하는 Composable 함수
@Composable
fun HomeScreen() {
// Box: 단일 레이아웃 컨테이너로, 자식 view를 배치하고 스타일링할 수 있음
Box(
modifier = Modifier.fillMaxSize(), // Box가 화면 전체를 채우도록 설정
contentAlignment = Alignment.Center // 자식 view를 박스 중앙에 정렬
) {
// 중앙에 텍스트 표시
Text(text = "Home Screen") // 화면 중앙에 "Home Screen" 텍스트 출력
}
}
Composable은 Jetpack Compose에서 UI를 구성하는데 사용되는 함수이다. @composable 어노테이션을 선언해주면 되며, Compose 런타임이 Composable 함수를 인식하고 UI를 생성 및 갱신하는데 사용된다.
즉, 위에서 HomeScreen함수가 호출될 때 새로운 UI를 생성하며, 필요한 경우에만 다시 렌더링(갱신)한다. HomeScreen과 동일하게 NotificationScreen, ScheduleScreen, SettingsScreen을 만들어주었다.
다음으로 NavGraph를 정의한다.
NavGraph.kt
package com.example.scheschedule.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.example.scheschedule.ui.HomeScreen
import com.example.scheschedule.ui.NotificationScreen
import com.example.scheschedule.ui.ScheduleScreen
import com.example.scheschedule.ui.SettingsScreen
// NavGraph: 네비게이션 그래프 정의
@Composable
fun NavGraph(navController: NavHostController) {
// NavHost: 네비게이션 호스트를 정의하며, 다양한 경로(route)와 이를 처리할 화면을 매핑
NavHost(
navController = navController, // 네비게이션 컨트롤러
startDestination = "home" // 초기 경로를 "home"으로 설정
) {
// "home" 경로에 대해 HomeScreen composable과 연결
composable("home") {
HomeScreen() // 홈 화면을 표시
}
// "schedule" 경로에 대해 " "
composable("schedule") {
ScheduleScreen() // 시간표 화면을 표시
}
// "notification" 경로에 대해 " "
composable("notification") {
NotificationScreen() // 공지사항 화면을 표시
}
// "settings" 경로에 대해 " "
composable("settings") {
SettingsScreen() // 설정 화면을 표시
}
}
}
NavGraph는 앱 내에서 여러 페이지를 이동하기 위한 라우터 역할을 하며, Jetpack Navigation Compose를 활용해 경로(route)와 각 화면을 연결한다. 이를 위해 NavHost를 사용하며, 각 경로에 대해 composable 함수를 매핑하여 특정 화면을 렌더링한다.
예를 들어, "home" 경로는 HomeScreen과 연결되어 사용자가 해당 경로로 이동하면 HomeScreen이 표시된다.
다음으로 Sidebar.kt를 작성한다.
Sidebar.kt
// 사이드바 안의 개별 버튼 구성
@Composable
fun SidebarButton(
label: String, // 버튼 텍스트
navController: NavController, // 네비게이션 컨트롤러
route: String, // 이동할 네이게이션 경로
drawerState: DrawerState, // DrawerState 객체
scope: kotlinx.coroutines.CoroutineScope, // 코루틴 스코프
icon: @Composable (() -> Unit)? = null // 아이콘 Composable을 선택적으로 전달
)
SidebarButton은 사이바 안의 개별 버튼을 구성한다. 각 매개변수는 주석과 같다. (() -> Unit)?는 반환값이 없는 함수 타입으로 nullable로 선언되어 있어 필요하지 않을 때는 null로 설정할 수 있다. 즉, 기본값을 null로 설정한다.
fun SidebarButton(...) {
Button(
onClick = {
// 버튼 클릭 시 동작
scope.launch {
drawerState.close() // 버튼 클릭 시 사이드바를 닫음
}
navController.navigate(route) // 지정된 네비게이션 경로로 이동
},
modifier = Modifier
.fillMaxWidth() // 버튼이 사이드바의 가로를 완전히 채우도록 설정
.height(48.dp), // 버튼의 높이를 고정하여 균일한 크기 유지
shape = Shapes().small.copy(CornerSize(0.dp)) // 버튼 모서리를 직각으로 설정
) {
Row(
modifier = Modifier.fillMaxWidth(), // 아이콘과 텍스트를 가로로 배치
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically // 세로 중앙 정렬
) {
if (icon != null) {
Box(modifier = Modifier.padding(end = 8.dp)) { // 텍스트와의 간격 설정
icon() // 아이콘 표시
}
}
Text(text = label) // 버튼에 텍스트 표시
}
}
}
onClick은 버튼 클릭 시에 수행할 동작을 정의한다. 사이드바의 버튼은 사이바를 완전히 채우는 직사각형이 되도록 구성하였다. Row 안에 아이콘과 텍스트를 함께 작성하여 각 버튼이 아이콘 - 텍스트로 표시되도록 한다.
// Sidebar: 사이드바 구성
@Composable
fun Sidebar(navController: NavController, drawerState: DrawerState) {
val scope = rememberCoroutineScope() // 코루틴을 사용하여 비동기 동작 제어
// 사이드바의 전체 레이아웃
Box(
modifier = Modifier
.fillMaxHeight() // 화면의 세로를 모두 채우도록 설정
.width(250.dp) // 사이드바의 가로 크기를 250dp로 고정
.background(Color.LightGray) // 사이드바의 배경색을 연회색으로 설정
.padding(vertical = 16.dp) // 내부 수직 패딩 추가
) {
// 버튼을 세로로 배치하기 위한 Column
Column(
modifier = Modifier.padding(vertical = 16.dp) // 버튼 간 여유 공간
) {
// 각각의 버튼을 호출하여 사이드바에 추가
SidebarButton("Home", navController, "home", drawerState, scope) {
Icon(Icons.Default.Home, contentDescription = "Home Icon") // 아이콘 추가
}
SidebarButton("Schedule", navController, "schedule", drawerState, scope) {
Icon(Icons.Default.CalendarToday, contentDescription = "Home Icon")
}
SidebarButton("Notification", navController, "notification", drawerState, scope) {
Icon(Icons.Default.Notifications, contentDescription = "Notification Icon")
}
SidebarButton("Settings", navController, "settings", drawerState, scope) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
}
}
Sidebar는 사이드바의 전체적인 구성을 정의한다. 사이드바는 왼쪽에서 오른쪽으로 펼쳐지며, 화면을 모두 덮지는 않는 크기로 구성했다. 그리고 각 버튼을 배치하는데, 이때 Icon으로 각 버튼에 해당하는 아이콘을 추가해주었다.
이때 scope로 코루틴을 사용하는데, 코루틴(Coroutine)은 경량 비동기 프로그래밍 도구로, 작업을 일시 중단(suspend)하고 재개(resume)한다. 코루틴은 스레드보다 가벼워서 효율적인 비동기 작업 처리가 가능하고 사용이 간단하다는 장점이 있다.
코루틴은 네트워크 요청 등의 작업을 메인 스레드를 차단하지 않고 수행하며, CoroutineScope로 실행 범위를 관리하고, launch와 async로 비동기 작업을 실행한다.
SidebarButton()과 SideBar() 모두 Sidebar.kt에 위치한다.
package com.example.scheschedule.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerState
import androidx.compose.material3.Icon
import androidx.compose.material3.Shapes
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
// 사이드바 안의 개별 버튼 구성
@Composable
fun SidebarButton(
label: String, // 버튼 텍스트
navController: NavController, // 네비게이션 컨트롤러
route: String, // 이동할 네이게이션 경로
drawerState: DrawerState, // DrawerState 객체
scope: kotlinx.coroutines.CoroutineScope, // 코루틴 스코프
icon: @Composable (() -> Unit)? = null // 아이콘 Composable을 선택적으로 전달
) {
Button(
onClick = {
// 버튼 클릭 시 동작
scope.launch {
drawerState.close() // 버튼 클릭 시 사이드바를 닫음
}
navController.navigate(route) // 지정된 네비게이션 경로로 이동
},
modifier = Modifier
.fillMaxWidth() // 버튼이 사이드바의 가로를 완전히 채우도록 설정
.height(48.dp), // 버튼의 높이를 고정하여 균일한 크기 유지
shape = Shapes().small.copy(CornerSize(0.dp)) // 버튼 모서리를 직각으로 설정
) {
Row(
modifier = Modifier.fillMaxWidth(), // 아이콘과 텍스트를 가로로 배치
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically // 세로 중앙 정렬
) {
if (icon != null) {
Box(modifier = Modifier.padding(end = 8.dp)) { // 텍스트와의 간격 설정
icon() // 아이콘 표시
}
}
Text(text = label) // 버튼에 텍스트 표시
}
}
}
// Sidebar: 사이드바 구성
@Composable
fun Sidebar(navController: NavController, drawerState: DrawerState) {
val scope = rememberCoroutineScope() // 코루틴을 사용하여 비동기 동작 제어
// 사이드바의 전체 레이아웃
Box(
modifier = Modifier
.fillMaxHeight() // 화면의 세로를 모두 채우도록 설정
.width(250.dp) // 사이드바의 가로 크기를 250dp로 고정
.background(Color.LightGray) // 사이드바의 배경색을 연회색으로 설정
.padding(vertical = 16.dp) // 내부 수직 패딩 추가
) {
// 버튼을 세로로 배치하기 위한 Column
Column(
modifier = Modifier.padding(vertical = 16.dp) // 버튼 간 여유 공간
) {
// 각각의 버튼을 호출하여 사이드바에 추가
SidebarButton("Home", navController, "home", drawerState, scope) {
Icon(Icons.Default.Home, contentDescription = "Home Icon") // 아이콘 추가
}
SidebarButton("Schedule", navController, "schedule", drawerState, scope) {
Icon(Icons.Default.CalendarToday, contentDescription = "Home Icon")
}
SidebarButton("Notification", navController, "notification", drawerState, scope) {
Icon(Icons.Default.Notifications, contentDescription = "Notification Icon")
}
SidebarButton("Settings", navController, "settings", drawerState, scope) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
}
}
이제 MainActivity에서 사이드바를 사용할 수 있도록 설정해보자.
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // 전체 화면을 사용하는 레이아웃을 활성화
setContent {
ScheScheduleTheme {
MainScreen() // 메인 화면 렌더링
}
}
}
}
기본 생성된 코드에서 setContent를 수정한다. 앱 실행 시 MainScreen을 렌더링한다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
val navController = rememberNavController() // 네비게이션 컨트롤러 생성
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) // 사이드바의 초기 상태 설정 (닫힌 상태)
val scope = rememberCoroutineScope() // 코루틴 스코프 생성
ModalNavigationDrawer(
drawerState = drawerState, // 사이드바 상태
drawerContent = {
Sidebar(navController = navController, drawerState = drawerState) // 사이드바 composable
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("ScheSchedule") }, // App Bar 제목 설정
navigationIcon = {
IconButton(onClick = {
scope.launch {
drawerState.open() // 메뉴 버튼 클릭 시 사이드바 열기
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Menu") // 햄버거 메뉴 아이콘
}
}
)
},
content = { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
NavGraph(navController = navController) // 네비게이션 그래프 추가
}
}
)
}
}
MainScreen 함수는 사이드바를 포함한 기본 화면 구조를 구축하고 라우트에 따라 화면을 렌더링한다. ModalNavigationDrawer는 모달 스타일의 네비게이션 드로어(사이드바)를 제공하며 drawerContent는 사이드바에 표시할 콘텐츠를 정의하고 drawerState는 사이드바의 상태(열림/닫힘)를 제어한다.
topBar는 화면 상단의 App Bar를 설정하며 content는 메인 콘텐츠 영역을 구성한다. content 안의 NavGraph를 통해 네비게이션 라우팅을 관리하며 navController를 전달하여 각 화면 전환을 제어한다.
전체 코드
package com.example.scheschedule
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import com.example.scheschedule.components.Sidebar
import com.example.scheschedule.navigation.NavGraph
import com.example.scheschedule.ui.theme.ScheScheduleTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // 전체 화면을 사용하는 레이아웃을 활성화
setContent {
ScheScheduleTheme {
MainScreen() // 메인 화면 렌더링
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
val navController = rememberNavController() // 네비게이션 컨트롤러 생성
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) // 사이드바의 초기 상태 설정 (닫힌 상태)
val scope = rememberCoroutineScope() // 코루틴 스코프 생성
ModalNavigationDrawer(
drawerState = drawerState, // 사이드바 상태
drawerContent = {
Sidebar(navController = navController, drawerState = drawerState) // 사이드바 composable
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("ScheSchedule") }, // App Bar 제목 설정
navigationIcon = {
IconButton(onClick = {
scope.launch {
drawerState.open() // 메뉴 버튼 클릭 시 사이드바 열기
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Menu") // 햄버거 메뉴 아이콘
}
}
)
},
content = { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
NavGraph(navController = navController) // 네비게이션 그래프 추가
}
}
)
}
}
이제 실행하면 다음과 같이 동작하는 것을 확인할 수 있다.
'Side Project > Application' 카테고리의 다른 글
[Application][Backend] ScrapingScheduler 설정 및 Caching 정책 (0) | 2025.02.11 |
---|---|
[Application] [Backend] 공지사항 스크래핑 역할 분리 (0) | 2025.01.30 |
[Application] 엑셀 파일을 Parsing해서 DB로 저장하기 (0) | 2025.01.16 |