스칼라와 캐패시터를 사용해서 안드로이드 프로그램 만들기 (Cross-Platform mobile development with Scala and Capacitor)

View: 421 0 0
작성자: 달빛제이크
카테고리: Scala Language
발행: 2024-03-02 수정 2024-06-20

안녕하세요. 달빛제이크입니다.
스칼라로 안드로이드 프로그램 만들기 두 번째 방법입니다. 이 번에 소개할 방법은 자바스크립트 라이브러리인 캐패시터 (Capacitor)를 사용해서 앱을 만드는 방법인데 스칼라를 개발언어로 사용하기 위해서 Scala.js를 활용합니다. 순서대로 따라하시면 마지막에 본인 휴대폰 또는 안드로이드 에뮬레이터에서 Hello world!를 확인 하실 수 있습니다.

1. 공식 홈페이지에서 Node.js를 다운로드 받아서 설치 후 Pass 설정을 해 줍니다.

 npm (node package manager)와 npx (node package execute)를 통해 Javascript 관련 library를 Download하고 실행하는 데 필요합니다.    
    // Pass 설정 예시
    export NODEJS_HOME=$HOME/node-v20.11.1-linux-x64

2. sbt에서 scala seed로 project를 만듭니다.

    $ sbt new scala/scala-seed.g8
    // Directory 구조
    root
    ├── build.sbt
    ├── project
    |   ├── plugins.sbt (직접 만들어야 함.)
    |   └── build.properties
    └── src
        └── main
            └── scala
                └── main (직접 만들어야 함.)
                    └── Main.scala (직접 만들어야 함.)       

3. build.sbt, build.properties를 수정하고 plugins.sbt를 만들어서 sbt-scalajs plugin을 추가합니다.

a) build.sbt의 Scala version 및 관련 값들을 수정합니다.    
b) buils.properties의 sbt version을 확인합니다. 최신이면 그대로 유지합니다.    
c) plugins.sbt에 sbt-scalajs plugin을 추가합니다.    
// build.properties
sbt.version=1.9.8
// plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0")  // 공홈에서 최신 Version 확인
// build.sbt
import Dependencies._

ThisBuild / scalaVersion     := "3.3.1"    
ThisBuild / version          := "0.1.0-SNAPSHOT"    
ThisBuild / organization     := "kr.code-snippet"    
ThisBuild / organizationName := "code-snippet"    

lazy val root = (project in file("."))
  .enablePlugins(ScalaJSPlugin)
  .settings(
    name := "ScalaJS-Capacitor",

    // Tell Scala.js that this is an application with a main method
    scalaJSUseMainModuleInitializer := true,

    /* Configure Scala.js to emit modules in the optimal way
     * - emit ECMAScript modules
     */
    scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },

    libraryDependencies += munit % Test,

    /* Depend on the scalajs-dom library.
     * It provides static types for the browser DOM APIs.
     */
    libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0"
  )

4. 간단한 Hello World main program을 작성해서 실행합니다.

// Main.scala    
package main
object Main:
  def main(args: Array[String]) = println("Hello!")
// sbt 실행
[Project Root Directory]$ sbt run
[info] Running main.Main.
Hello!
[success] Total tims: 2 s, completed 2024. 2. 25. 오후 4:12:38

5. npm (node package manager)를 통해 필요한 library들을 설치합니다.

Scala.js를 Javascript library와 함께 사용할 수 있습니다. Mobile Application을 만들기 위해서 필요한 library는 Vite와 Capacitor인데, Vite는 Javascript를 기반으로 하는 Frontend 개발 환경이자 Build Tool이고, Capacitor는 Web Application을 Native Mobile Application으로 변환하는 Open Source Framework입니다. 기존에는 Snowpack을 Build Tool로 사용했으나 개발이 중단되어 대체 환경으로 Vite를 사용합니다. 두 library 모두 npm 명령어를 통해 개별로 설치할 수 있으나 package.json을 작성해서 npm install로 한번에 설치하겠습니다.

// package.json
// npm(Node Package Manger)이 프로젝트의 종속성과 스크립트를 관리하는 데 사용합니다.
{
  "name": "snowpack-capacitor",
  "devDependencies": {
    "vite": "latest"
  }
  "dependencies": {
    "@capacitor/cli": "latest",
    "@capacitor/core": "latest"
    "@capacitor/android": "latest"
    }
}

npm install을 실행하면 node_modules 디렉토리가 생성되고 관련 package들이 설치 됩니다.

6. vite.config.js를 작성합니다.

vite가 project를 build하고 web server를 구동 시키는 데 필요한 정보가 설정됩니다.

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    outDir: "./target/build",
  },
  // defalt 경로는 project의 root 디렉토리이며, 생략 가능합니다.
  root: ".",
  resolve: {
    alias: {
      "/public": "/public",
      "/resources": "/src/main/resources",
      "/fastopt": "/target/scala-3.3.1/scalajs-capacitor-fastopt",
    },
  },
});

default root는 project의 root 디렉토리이며, 변경을 하지 않을 경우에는 따로 설정을 하지 않아도 됩니다.
index.html은 project의 root 디렉토리에 위치하게 됩니다. 따라서 public 디렉토리가 따로 필요하지 않지만, Vite project의 root 디렉토리가 sbt project의 root 디렉토리이기 때문에 설정 파일들과 혼재되지 않도록 하기 위해 index.html 외의 html 파일들을 public 디렉토리에 작성하고자 디렉토리를 추가 합니다.
src/main/resources 디렉토리도 프로젝트에서 사용하기 위해 추가합니다.

7. vite.config.js 설정에 맞게 public과 src/main/resources 디렉토리를 만들어 줍니다.

 root 디렉토리에 index.html을 작성하여 main.js를 연결해 줍니다.    
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta
       name="viewport"
       content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <title>Test Capacitor</title>
</head>
<body>
  <div id="root"></div>
  <script src="/fastopt/main.js" type="module"></script>
</body>
</html>

8. root 디렉토리에서 sbt fastLinkJS 명령어로 Scala.js code를 javascript로 변환하고, npx vite dev를 실행하여 javascript를 build 해 줍니다.

[Project Root]$ sbt fastLinkJS
[Project Root]$ npx vite dev

웹 서버가 실행되면 터미널에 표시된 Link를 열어 결과물을 확인합니다. 빈 화면이 표시되면 index.html이 정상적으로 연결된 것이고, 개발자 도구를 열어 Console tab을 확인해서 Hello!가 표시되어 있으면 이전에 작성한 Scala Code가 정상적으로 실행이 되고 있는 것입니다.

9. Scala.js로 User Interface를 만들기 위해 Native Scala.js library인 Laminar를 설치합니다. build.sbt에 library dependency로 추가 합니다.

libraryDependencies += "com.raquo" %%% "laminar" % "16.0.0"

10. Laminar를 설치했으니 Main.scala를 수정해서 화면에 결과가 표시되도록 합니다.

package main

object Main:
  // Return Type인 Unit을 생략하면 오류 메세지와 함께 컴파일이 되지 않습니다.
  def main(args: Array[String]): Unit = 
    println("Hello!")

    // Laminar와 scalajs.dom을 import 합니다. 
    import com.raquo.laminar.api.L._
    import org.scalajs.dom
    val app = h1("Hello world!")
    render(dom.document.getElementById("root"), app)

npx vite dev가 실행되고 있으면 sbt fastLinkJS 명령어 만으로 브라우저에서 변경된 사항을 확인할 수 있습니다. 자동 갱신이 되지 않을 경우에는 refresh를 해 줍니다.

11. Capacitor 설정을 진행합니다.

Vite build tool을 활용한 Scala.js의 웹 개발 환경이 갖춰졌습니다. 이제는 Android 개발 환경을 만들어주기 위해 Capacitor 설정을 진행합니다.
앞에서 이미 Capacitor를 설치했기 때문에 npx cap init을 실행해서 설정을 진행합니다.

[Project Root]$ npx cap init
[?] What is the name of your app?
    This should be a human-friendly app name, like what you'd see in the App Store.
? Name › vite-capacitor // package.json에서 설정한 name이 default로 표시됩니다.

[?] What should be the Package ID for your app?
    Package IDs (aka Bundle ID in iOS and Application ID in Android) are unique identifiers for apps. They
    must be in reverse domain name notation, generally representing a domain name that you or your company
    owns.
? Package ID › kr.codesnippet.vitecapacitor // App에 대한 package ID를 지정합니다.

Project Root 디렉토리에 capacitor.config.json 파일이 생성되고 npx cap init에서 설정한 값들이 반영됩니다.
설정된 값들 중에서 webDir의 dist를 target/build로 변경해서 Vite의 build 경로와 맞추어 줍니다.

// capacitor.config.json
{
  "appId": "kr.codesnippet.vitecapacitor",
  "appName": "vite-capacitor",
  "webDir": "target/build", // dist에서 target/build로 변경
  "server": {
    "androidScheme": "https"
  }
}

12. npx cap add android를 실행해서 프로젝트에 android platform을 추가합니다.

13. npx vite build를 실행해서 지금까지 작성한 code들을 build 해주고, npx cap run android를 실행해서 Android emulator로 결과물을 확인합니다.

[Project Root]$ npx vite build
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
vite v5.1.4 building for production...
✓ 3 modules transformed.
target/build/index.html                  0.40 kB │ gzip:  0.27 kB
target/build/assets/index-Dcflf3U_.js  340.10 kB │ gzip: 57.26 kB
✓ built in 777ms
[Project Root]$ npx cap run android
✔ Copying web assets from build to android/app/src/main/assets/public in 5.34ms
✔ Creating capacitor.config.json in android/app/src/main/assets in 928.09μs
[info] Inlining sourcemaps
✔ copy android in 21.67ms
✔ Updating Android plugins in 902.95μs
✔ update android in 37.73ms
? Please choose a target device: › - Use arrow-keys. Return to submit.
❯   Pixel 3 API 31 (emulator) (Pixel_3_API_31)
    Pixel_3a_API_34_extension_level_7_x86_64 (emulator) (Pixel_3a_API_34_extension_level_7_x86_64)
    Pixel 4 API 33 (emulator) (Pixel_4_API_33)

세 종류의 emulator 중에서 첫 번째와 세 번째 emulator는 정상 동작하지 않았고, 두 번째 emulator를 선택했을 때 결과물을 확인할 수 있었습니다.
정상 동작하는 emulator를 선택하면 화면에 안드로이드 휴대폰이 나타나고 Hello world! 가 표시됩니다.
안드로이드 스크린의 오른쪽 패널에 조작 패드가 표시되어 휴대폰을 실물처럼 동작 시킬 수 있습니다.

14. npx cap run --list android 명령어로 android device list를 확인할 수 있고, npx cap open android 명령어를 통해 android studio를 실행할 수 있습니다.

npx cap run --list android에서 보여지는 emulator 목록이 npx cap run android를 실행해서 사용할 수 있는 default 목록이고, Capacitor에 새로운 emulator를 추가할 수 있는 방법은 없습니다. Capacitor CLI가 할 수 있는 역할은 여기까지 이며, 새로운 emulator로 개발을 진행하기 위해서는 Android Studio를 활용하거나 실물 Device를 USB로 연결해서 사용할 수 있습니다.

15. Android Studio를 설치하고 npx cap open android 명령어로 android studio를 실행합니다.

(Android Studio 설치 과정은 본 글에서는 생략합니다.) Top Menu에서 Tools를 선택하고 SDK Manager를 열어 줍니다. 새 window의 좌측 메뉴에서 Android SDK가 선택되어 있습니다. 우측 설정 화면에서 SDK Tools tab을 선택하고 Android SDK Build-Tools의 원하는 version을 선택합니다. 화면을 아래로 스크롤링 하면서 Android SDK Command-line Tools의 최신 버전을 선택하고 Android Emulator와 Android SDK Platform-Tools도 선택합니다. 이미 선택되어 있으면 그대로 유지 합니다. SDK Platforms tab으로 이동해서 Build-Tool의 선택한 Version에 맞는 Android SDK Platform과 Google APIs Intel x86_64 Atom System Image를 선택합니다. System Image는 본인의 시스템에 맞는 항목을 선택합니다. 확인을 눌러 선택한 항목들을 설치합니다.

16. Android Studio에서 Device를 만듭니다.

Top Menu의 Tools에서 Device manager를 선택합니다. Android 화면 오른쪽에 Device Manager 창이 표시됩니다. Create device를 클릭하고 원하는 Device를 만들어 줍니다. 정상적으로 설치가 되면 Device를 실행시켰을 때 휴대폰 이미지가 나타나고 작성된 프로그램의 결과물이 화면에 표시됩니다. 화면에 표시되지 않을 경우 Run을 실행시키면 프로그램이 동작합니다. Open 된 emulator에 npx cap run android 명령어를 실행시켜 변경된 프로그램을 반영할 수 있습니다. 실물 Device는 USB로 연결해서 실제 프로그램을 휴대폰에 설치할 수 있습니다. USB 연결 전에 개발자 모드 (Developer options)에서 USB Debugging을 활성화 시키고 USB를 연결하면 npx cap run --list android의 목록에서 연결된 디바이스 정보를 바로 확인할 수 있고, npx cap run android를 실행시키거나, Android Studio에서 바로 프로그램을 실행시켜 휴대폰에서 결과물을 확인할 수 있습니다.
*실제 휴대폰에서 프로그램을 확인하기 위해 adb가 설치되어 있어야 합니다.

17. Capacitor Plugin들은 TypeScript 모듈로 배포됩니다. Geolocation과 같이 Capacitor Plugin을 사용하기 위해서 몇 가지 설정을 추가해 줍니다.

// project/plugins.sbt
addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta44")
// package.json
{
  "name": "vite-capacitor",
  "devDependencies": {
    "vite": "latest",
    "typescript": "latest"   // 추가
  },
  "dependencies": {
    "@capacitor/cli": "latest",
    "@capacitor/core": "latest",
    "@capacitor/android": "latest"
  }
}
// build.sbt (추가)
import Dependencies._
import scala.sys.process.Process  // 추가

ThisBuild / scalaVersion     := "3.3.1"
ThisBuild / version          := "0.1.0-SNAPSHOT"
ThisBuild / organization     := "kr.code-snippet"
ThisBuild / organizationName := "code-snippet"

lazy val root = (project in file("."))
  .enablePlugins(ScalaJSPlugin)
  .enablePlugins(ScalablyTypedConverterExternalNpmPlugin)  // 추가
  .settings(
    name := "ScalaJS-Capacitor",

    // Tell Scala.js that this is an application with a main method
    scalaJSUseMainModuleInitializer := true,

    /* Configure Scala.js to emit modules in the optimal way
     * - emit ECMAScript modules
     */
    scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },

    libraryDependencies += munit % Test,

    /* Depend on the scalajs-dom library.
     * It provides static types for the browser DOM APIs.
     */
    libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0",

    /* Depend on the laminar library.
     * It provides web application interfaces on the Scala.js platform.
     */
    libraryDependencies += "com.raquo" %%% "laminar" % "16.0.0",

    // 추가
    externalNpm := {
      Process("npm", baseDirectory.value).!
      baseDirectory.value
    },
    stIgnore ++= List(
      "@capacitor/android",
      "@capacitor/cli",
      "@capacitor/core"
    )
  )

ScalablyTyped의 sbt-conver는 Capacitor의 TypeScript module을 Scala에서 자동으로 인식하게 해 줍니다. Mobile 개발에 필요한 Capacitor module을 손 쉽게 사용할 수 있습니다.
npm install을 실행해서 TypeScript library를 설치하고, sbt compile을 실행해서 설정이 올바른지 확인합니다.

18. Capacitor의 Geolocation Plugin을 사용해보겠습니다.

Mobile 개발에 필요한 다른 Plugin들도 설치 및 개발에 동일한 Process로 이용하시면 됩니다.
package.json의 dependencies에 @capacitor/geolocation을 추가합니다. (* devDependencies가 아니고 dependencies 입니다.) npm install을 실행해서 library를 설치하고, sbt compile을 실행해서 ScalablyTyped를 다시 적용합니다.

19. main.scala를 다음과 같이 수정합니다.

package main

import com.raquo.laminar.api.L._
import org.scalajs.dom
import typings.capacitorGeolocation.mod.*

object Main:
  def main(args: Array[String]): Unit =
    println("Hello!! Scala.js!!")

    //val app = h1("Hello world!")
    val app = div(h1("Hello world!"),
      child <-- EventStream.fromJsPromise(
        Geolocation.getCurrentPosition()
      )
      .map { position =>
        s"Your position: ${position.coords.latitude}, ${position.coords.longitude}"
      })
    render(dom.document.getElementById("root"), app)

sbt fastLinkJS로 Scala.js code를 다시 javascript code로 변환하고, npx vite dev를 실행해서 browser를 통해 먼저 결과물을 확인합니다.

20. android/app/src/main/AndroidManifest.xml에 다음을 추가합니다. 이는 Android application에서 Geolocation을 사용하기 위한 설정입니다.

<!-- Geolocation API -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />

다음을 실행해서 결과물을 확인합니다.
npx vite build
npx cap sync
npx cap run android

comments 0