대화형 Dice Roller 앱 만들기

1. 시작하기 전에

이 Codelab에서는 사용자가 Button 컴포저블을 탭하여 주사위를 굴릴 수 있는 대화형 Dice Roller 앱을 만듭니다. 주사위 굴리기의 결과는 Image 컴포저블로 화면에 표시됩니다.

Kotlin과 함께 Jetpack Compose를 사용하여 앱 레이아웃을 빌드하고 Button 컴포저블을 탭할 때 발생하는 작업을 처리하는 비즈니스 로직을 작성합니다.

기본 요건

  • Android 스튜디오에서 기본 Compose 앱을 만들고 실행하는 능력
  • 앱에서 Text 컴포저블을 사용하는 방법에 관한 지식
  • 문자열 리소스로 텍스트를 추출하여 더 쉽게 앱을 번역하고 문자열을 재사용하는 방법 이해
  • Kotlin 프로그래밍 기본사항에 관한 지식

학습할 내용

  • Compose를 사용하여 Button 컴포저블을 Android 앱에 추가하는 방법
  • Compose를 사용하여 Android 앱에서 Button 컴포저블에 동작을 추가하는 방법
  • Android 앱의 Activity 코드를 열고 수정하는 방법

빌드할 항목

  • 사용자가 주사위를 굴릴 수 있고 그 결과를 사용자에게 표시하는 Dice Roller라는 대화형 Android 앱

필요한 항목

  • Android 스튜디오가 설치된 컴퓨터

이 Codelab을 완료하면 다음과 같이 앱이 완성됩니다.

3e9a9f44c6c84634.png

2. 기준 설정

프로젝트 만들기

  1. Android 스튜디오에서 File > New > New Project를 클릭합니다.
  2. New Project(새 프로젝트) 대화상자에서 Empty Activity(빈 활동)를 선택한 다음 Next(다음)를 클릭합니다.

39373040e14f9c59.png

  1. Name(이름) 필드에 Dice Roller를 입력합니다.
  2. Minimum SDK(최소 SDK) 필드의 메뉴에서 최소 API 수준 24(Nougat)를 선택한 다음 Finish(완료)를 클릭합니다.

8fd6db761068ca04.png

3. 레이아웃 인프라 만들기

프로젝트 미리보기

프로젝트를 미리 보려면 다음 안내를 따르세요.

  • Split(분할) 또는 Design(디자인) 창에서 Build & Refresh(빌드 및 새로고침)를 클릭합니다.

9f1e18365da2f79c.png

이제 Design(디자인) 창에 미리보기가 표시됩니다. 크기가 작아 보여도 걱정하지 마세요. 레이아웃을 수정할 때 변경됩니다.

b5c9dece74200185.png

샘플 코드 재구성

Dice Roller 앱의 테마와 더 비슷하도록 생성된 코드 중 일부를 변경해야 합니다.

최종 앱 스크린샷에는 주사위 이미지와 주사위를 굴리는 버튼이 있습니다. 이 아키텍처를 반영하도록 구성 가능한 함수를 구조화합니다.

샘플 코드를 재구성하는 방법은 다음과 같습니다.

  1. GreetingPreview() 함수를 삭제합니다.
  2. @Composable 주석이 있는 DiceWithButtonAndImage() 함수를 만듭니다.

이 구성 가능한 함수는 레이아웃의 UI 구성요소를 나타내며 버튼 클릭 및 이미지 표시 로직도 보유합니다.

  1. Greeting(name: String, modifier: Modifier = Modifier) 함수를 삭제합니다.
  2. @Preview@Composable 주석이 있는 DiceRollerApp() 함수를 만듭니다.

이 앱은 버튼과 이미지로만 구성되므로 이 구성 가능한 함수를 앱 자체로 생각하면 됩니다. 이러한 이유로 DiceRollerApp() 함수라고 합니다.

MainActivity.kt

@Preview
@Composable
fun DiceRollerApp() {

}

@Composable
fun DiceWithButtonAndImage() {

}

Greeting() 함수를 삭제했으므로 DiceRollerTheme() 람다 본문에서 Greeting("Android") 호출이 빨간색으로 강조표시됩니다. 컴파일러가 더 이상 이 함수 참조를 찾을 수 없기 때문입니다.

  1. onCreate() 메서드에 있는 setContent{} 람다 내의 모든 코드를 삭제합니다.
  2. setContent{} 람다 본문에서 DiceRollerTheme{} 람다를 호출한 후 DiceRollerTheme{} 람다 내에서 DiceRollerApp() 함수를 호출합니다.

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        DiceRollerTheme {
            DiceRollerApp()
        }
    }
}
  1. DiceRollerApp() 함수에서 DiceWithButtonAndImage() 함수를 호출합니다.

MainActivity.kt

@Preview
@Composable
fun DiceRollerApp() {
    DiceWithButtonAndImage()
}

수정자 추가

Compose는 Compose UI 요소의 동작을 장식하거나 수정하는 요소 모음인 Modifier 객체를 사용합니다. 이 객체는 Dice Roller 앱 구성요소의 UI 구성요소 스타일을 지정하는 데 사용합니다.

수정자를 추가하려면 다음 안내를 따르세요.

  1. Modifier 유형의 modifier 인수를 허용하고 기본값 Modifier를 할당하도록 DiceWithButtonAndImage() 함수를 수정합니다.

MainActivity.kt

@Composable 
fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
}

이전 코드 스니펫은 혼동을 줄 수 있으므로 자세히 살펴보겠습니다. 이 함수를 사용하면 modifier 매개변수를 전달할 수 있습니다. modifier 매개변수의 기본값은 Modifier 객체이므로 메서드 서명의 = Modifier 부분입니다. 매개변수의 기본값을 사용하면 향후 이 메서드를 호출하는 모든 사용자가 매개변수의 값을 전달할지 결정할 수 있습니다. 자체 Modifier 객체를 전달하면 UI의 동작과 장식을 맞춤설정할 수 있습니다. Modifier 객체를 전달하지 않으면 기본값인 일반 Modifier 객체를 가정합니다. 이 방법을 모든 매개변수에 적용할 수 있습니다. 기본 인수에 관한 자세한 내용은 기본 인수를 참고하세요.

  1. 이제 DiceWithButtonAndImage() 컴포저블에 수정자 매개변수가 있으므로 컴포저블이 호출될 때 수정자를 전달합니다. DiceWithButtonAndImage() 함수의 메서드 서명이 변경되었으므로 원하는 장식이 포함된 Modifier 객체를 호출 시 전달해야 합니다. Modifier 클래스는 DiceRollerApp() 함수에서 컴포저블을 장식하거나 컴포저블에 동작을 추가합니다. 여기서는 DiceWithButtonAndImage() 함수에 전달되는 Modifier 객체에 추가할 중요한 장식이 있습니다.

기본값이 있는데 굳이 Modifier 인수를 전달해야 하는지 의문이 드실 수도 있습니다. 그 이유는 컴포저블이 재구성을 거칠 수 있기 때문입니다. 재구성이란 기본적으로 @Composable 메서드의 코드 블록이 다시 실행된다는 의미입니다. Modifier 객체가 코드 블록에서 만들어지면 다시 만들어질 수 있으며 이는 효율적이지 않습니다. 재구성은 이 Codelab의 후반부에서 다룰 예정입니다.

MainActivity.kt

DiceWithButtonAndImage(modifier = Modifier)
  1. fillMaxSize() 메서드를 Modifier 객체에 체이닝하여 레이아웃이 전체 화면을 채우도록 합니다.

이 메서드는 구성요소가 사용 가능한 공간을 채우도록 지정합니다. 이 Codelab 앞부분에서 Dice Roller 앱의 최종 UI 스크린샷을 확인했습니다. 주요 특징은 주사위와 버튼이 화면 중앙에 배치된 점입니다. wrapContentSize() 메서드는 사용 가능한 공간이 최소한 내부에 있는 구성요소만큼 커야 한다고 지정합니다. 하지만 fillMaxSize() 메서드가 사용되므로 레이아웃 내부의 구성요소가 사용 가능한 공간보다 작으면 Alignment 객체를 사용 가능한 공간 내에서 구성요소를 정렬해야 하는 방식을 지정하는 wrapContentSize() 메서드에 전달할 수 있습니다.

MainActivity.kt

DiceWithButtonAndImage(modifier = Modifier
    .fillMaxSize()
)
  1. wrapContentSize() 메서드를 Modifier 객체에 체이닝하고 Alignment.Center를 인수로 전달하여 구성요소를 중앙에 배치합니다. Alignment.Center는 구성요소가 세로와 가로로 모두 중앙에 배치되도록 지정합니다.

MainActivity.kt

DiceWithButtonAndImage(modifier = Modifier
    .fillMaxSize()
    .wrapContentSize(Alignment.Center)
)

4. 세로 레이아웃 만들기

Compose에서는 세로 레이아웃이 Column() 함수로 만들어집니다.

Column() 함수는 하위 요소를 세로 순서로 배치하는 컴포저블 레이아웃입니다. 예상 앱 디자인을 보면 주사위 이미지가 Roll 버튼 위에 세로로 표시되어 있습니다.

7d70bb14948e3cc1.png

세로 레이아웃을 만들려면 다음 단계를 따르세요.

  1. DiceWithButtonAndImage() 함수에 Column() 함수를 추가합니다.
  1. DiceWithButtonAndImage() 메서드 서명의 modifier 인수를 Column()의 수정자 인수로 전달합니다.

modifier 인수는 Column() 함수의 컴포저블이 modifier 인스턴스에서 호출된 제약 조건을 준수하도록 합니다.

  1. horizontalAlignment 인수를 Column() 함수에 전달한 후 Alignment.CenterHorizontally 값으로 설정합니다.

이렇게 하면 열 내에 있는 하위 요소가 너비에 따라 기기 화면의 중앙에 배치됩니다.

MainActivity.kt

fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
    Column (
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {}
}

5. 버튼 추가

  1. strings.xml 파일에서 문자열을 추가하고 Roll 값으로 설정합니다.

res/values/strings.xml

<string name="roll">Roll</string>
  1. Column()의 람다 본문에서 Button() 함수를 추가합니다.
  1. MainActivity.kt 파일에서 Text() 함수를 함수의 람다 본문에 있는 Button()에 추가합니다.
  2. roll 문자열의 문자열 리소스 ID를 stringResource() 함수에 전달하고 그 결과를 Text 컴포저블에 전달합니다.

MainActivity.kt

Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = { /*TODO*/ }) {
        Text(stringResource(R.string.roll))
    }
}

6. 이미지 추가

앱의 또 다른 필수 구성요소는 주사위 이미지로, 사용자가 Roll 버튼을 탭하면 결과를 표시합니다. Image 컴포저블을 사용하여 이미지를 추가하지만 그러려면 이미지 리소스가 필요하므로 이 앱에 제공된 이미지를 먼저 다운로드해야 합니다.

주사위 이미지 다운로드

  1. 이 URL을 열어 주사위 이미지 ZIP 파일을 컴퓨터에 다운로드하고 다운로드가 완료될 때까지 기다립니다.

컴퓨터에서 파일을 찾습니다. Downloads 폴더에 있을 가능성이 큽니다.

  1. ZIP 파일의 압축을 풀고 주사위 값 1~6이 표시된 주사위 이미지 파일 6개가 포함된 새 dice_images 폴더를 만듭니다.

앱에 주사위 이미지 추가

  1. Android 스튜디오에서 View > Tool Windows > Resource Manager를 클릭합니다.
  2. + > Import Drawables(+ > 드로어블 가져오기)를 클릭하여 파일 브라우저를 엽니다.

12f17d0b37dd97d2.png

  1. 주사위 이미지 폴더 6개를 찾아 선택하고 업로드를 진행합니다.

업로드된 이미지는 다음과 같이 표시됩니다.

4f66c8187a2c58e2.png

  1. Next(다음)를 클릭합니다.

688772df9c792264.png

Import drawables(드로어블 가져오기) 대화상자가 표시되고 파일 구조 내에 리소스 파일이 위치한 곳을 보여줍니다.

  1. Import를 클릭하여 이미지 6개를 가져옵니다.

이미지가 Resource Manager 창에 표시됩니다.

c2f08e5311f9a111.png

훌륭합니다. 다음 작업에서는 앱에서 이러한 이미지를 사용합니다.

Image 컴포저블 추가

주사위 이미지는 Roll 버튼 위에 표시되어야 합니다. Compose는 기본적으로 UI 구성요소를 순차적으로 배치합니다. 즉, 먼저 선언된 컴포저블이 먼저 표시됩니다. 이는 첫 번째 선언이 그다음에 선언된 컴포저블 위 또는 앞에 표시된다는 의미일 수 있습니다. Column 컴포저블 내부의 컴포저블은 기기에서 서로 위/아래에 표시됩니다. 이 앱에서는 Column을 사용하여 컴포저블을 세로로 쌓습니다. 따라서 Column() 함수 내에서 먼저 선언된 컴포저블이 동일한 Column() 함수에서 그다음에 선언된 컴포저블보다 먼저 표시됩니다.

Image 컴포저블을 추가하려면 다음 안내를 따르세요.

  1. Column() 함수 본문에서 Button() 함수보다 먼저 Image() 함수를 만듭니다.

MainActivity.kt

Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Image()
    Button(onClick = { /*TODO*/ }) {
      Text(stringResource(R.string.roll))
    }
}
  1. Image() 함수에 painter 인수를 전달하고 드로어블 리소스 ID 인수를 허용하는 painterResource 값을 할당합니다. 지금은 다음 리소스 ID R.drawable.dice_1 인수를 전달합니다.

MainActivity.kt

Image(
    painter = painterResource(R.drawable.dice_1)
)
  1. 앱에서 이미지를 만들 때마다 '콘텐츠 설명'을 제공해야 합니다. 콘텐츠 설명은 Android 개발에서 중요한 부분입니다. 접근성을 높이기 위해 각 UI 구성요소에 설명을 첨부합니다. 콘텐츠 설명에 관한 자세한 내용은 각 UI 요소 설명을 참고하세요. 이미지에 콘텐츠 설명을 매개변수로 전달할 수 있습니다.

MainActivity.kt

Image(
    painter = painterResource(R.drawable.dice_1),
    contentDescription = "1"
)

이제 필요한 모든 UI 구성요소가 표시됩니다. 하지만 ButtonImage 사이의 공간이 비좁습니다.

54b27140071ac2fa.png

  1. 이 문제를 해결하려면 구성 가능한 함수 ImageButton 사이에 구성 가능한 함수 Spacer를 추가합니다. SpacerModifier를 매개변수로 사용합니다. 여기서는 ImageButton 위에 있으므로 둘 사이에 세로 공간이 있어야 합니다. 따라서 Modifier의 높이를 설정하여 Spacer에 적용할 수 있습니다. 높이를 16.dp로 설정해 보세요. 일반적으로 dp 측정기준은 4.dp 단위로 증가합니다.

MainActivity.kt

Spacer(modifier = Modifier.height(16.dp))
  1. Preview 창에서 Build & Refresh를 클릭합니다.

이미지가 다음과 같이 표시됩니다.

73eea4c166f7e9d2.png

7. 주사위 굴리기 로직 빌드

이제 필요한 모든 컴포저블이 있으므로 버튼을 탭하면 주사위를 굴리도록 앱을 수정합니다.

Button을 대화형으로 만들기

  1. DiceWithButtonAndImage() 함수에서 Column() 함수 앞에 result 변수를 만들고 1 값과 동일하게 설정합니다.
  2. Button 컴포저블을 살펴봅니다. 중괄호 한 쌍으로 설정되고 중괄호 안에는 /*TODO*/ 주석이 있는 onClick 매개변수에 전달되는 것을 확인할 수 있습니다. 여기서 중괄호는 람다를 나타내며 중괄호 내부 영역은 람다 본문입니다. 함수가 인수로 전달되면 '콜백'이라고도 합니다.

MainActivity.kt

Button(onClick = { /*TODO*/ })

람다는 함수 리터럴입니다. 이는 다른 함수와 같은 함수지만 fun 키워드를 사용하여 별도로 선언되는 대신 인라인으로 작성되어 표현식 형태로 전달됩니다. 구성 가능한 함수 Button은 함수가 onClick 매개변수로 전달될 것으로 예상합니다. 따라서 람다를 사용하기에 적합한 위치이므로 이 섹션에서 람다 본문을 작성합니다.

  1. Button() 함수에서 onClick 매개변수의 람다 본문 값에서 /*TODO*/ 주석을 삭제합니다.
  2. 주사위 굴리기는 무작위입니다. 이를 코드에 반영하려면 올바른 문법을 사용하여 랜덤 숫자를 생성해야 합니다. Kotlin에서는 숫자 범위에 random() 메서드를 사용할 수 있습니다. onClick 람다 본문에서 result 변수를 1에서 6 사이의 범위로 설정하고 이 범위에서 random() 메서드를 호출합니다. Kotlin에서는 범위가 범위의 첫 번째 숫자와 범위의 마지막 숫자 사이에 두 개의 마침표로 지정됩니다.

MainActivity.kt

fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
    var result = 1
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(R.drawable.dice_1),
            contentDescription = "1"
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { result = (1..6).random() }) {
            Text(stringResource(R.string.roll))
        }
    }
}

이제 버튼을 탭할 수 있지만 버튼을 탭해도 아직 시각적으로 변경되는 것이 없습니다. 이 기능을 빌드해야 하기 때문입니다.

Dice Roller 앱에 조건문 추가

이전 섹션에서는 result 변수를 만들어 1 값으로 하드 코딩했습니다. 궁극적으로 result 변수의 값은 Roll 버튼을 탭하면 재설정되므로 표시되는 이미지를 결정해야 합니다.

컴포저블은 기본적으로 스테이트리스(Stateless)입니다. 즉, 값을 보유하지 않고 시스템에서 언제든지 다시 구성할 수 있어 값이 재설정됩니다. 그러나 Compose를 사용하면 이를 간단하게 방지할 수 있습니다. 구성 가능한 함수는 remember 컴포저블을 사용하여 메모리에 객체를 저장할 수 있습니다.

  1. result 변수를 remember 컴포저블로 만듭니다.

remember 컴포저블을 사용하려면 함수를 전달해야 합니다.

  1. remember 컴포저블 본문에서 mutableStateOf() 함수를 전달하고 함수에 1 인수를 전달합니다.

mutableStateOf() 함수는 observable을 반환합니다. observable은 나중에 자세히 알아봅니다. 지금은 기본적으로 result 변수 값이 업데이트되면 재구성이 트리거되고 결과 값이 반영되어 UI가 새로고침된다는 의미입니다.

MainActivity.kt

var result by remember { mutableStateOf(1) }

이제 버튼을 탭하면 result 변수가 랜덤 숫자 값으로 업데이트됩니다.

이제 result 변수를 표시할 이미지를 결정하는 데 사용할 수 있습니다.

  1. result 변수의 인스턴스화 아래에서 result 변수를 허용하는 when 표현식으로 설정된 변경 불가능한 imageResource 변수를 만들고 가능한 각 결과를 드로어블로 설정합니다.

MainActivity.kt

val imageResource = when (result) {
    1 -> R.drawable.dice_1
    2 -> R.drawable.dice_2
    3 -> R.drawable.dice_3
    4 -> R.drawable.dice_4
    5 -> R.drawable.dice_5
    else -> R.drawable.dice_6
}
  1. Image 컴포저블의 painterResource 매개변수로 전달된 ID를 R.drawable.dice_1 드로어블에서 imageResource 변수로 변경합니다.
  2. result 변수를 toString()이 있는 문자열로 변환하고 contentDescription으로 전달하여 result 변수의 값을 반영하도록 Image 컴포저블의 contentDescription 매개변수를 변경합니다.

MainActivity.kt

Image(
   painter = painterResource(imageResource),
   contentDescription = result.toString()
)
  1. 앱을 실행합니다.

이제 Dice Roller 앱이 완전히 작동합니다.

3e9a9f44c6c84634.png

8. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 이 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dice-roller.git

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.

  1. 프로젝트에 제공된 GitHub 저장소 페이지로 이동합니다.
  2. 브랜치 이름이 Codelab에 지정된 브랜치 이름과 일치하는지 확인합니다. 예를 들어 다음 스크린샷에서 브랜치 이름은 main입니다.

1e4c0d2c081a8fd2.png

  1. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 팝업을 엽니다.

1debcf330fd04c7b.png

  1. 팝업에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open을 클릭합니다.

d8e9dbdeafe9038a.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > Open 메뉴 옵션을 대신 선택합니다.

8d1fda7396afe8e5.png

  1. 파일 브라우저에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 8de56cba7583251f.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.

9. 결론

Compose를 사용하여 Android용 대화형 Dice Roller 앱을 만들었습니다.

요약

  • 구성 가능한 함수를 정의합니다.
  • 컴포지션으로 레이아웃을 만듭니다.
  • Button 컴포저블을 사용하여 버튼을 만듭니다.
  • drawable 리소스를 가져옵니다.
  • Image 컴포저블을 사용하여 이미지를 표시합니다.
  • 컴포저블을 사용하여 대화형 UI를 만듭니다.
  • remember 컴포저블을 사용하여 컴포지션의 객체를 메모리에 저장합니다.
  • mutableStateOf() 함수로 UI를 새로고침하여 observable을 만듭니다.

자세히 알아보기