Skip to content

코틀린 멀티플랫폼으로 풀스택 애플리케이션 구축하기

코틀린 멀티플랫폼으로 풀스택 애플리케이션 구축하기

코드 예제: full-stack-task-manager

사용된 플러그인:

Routing
라우팅은 서버 애플리케이션에서 들어오는 요청을 처리하기 위한 핵심 플러그인입니다.
, kotlinx.serialization,
Content Negotiation
ContentNegotiation 플러그인은 클라이언트와 서버 간의 미디어 유형 협상과 특정 형식의 콘텐츠 직렬화/역직렬화라는 두 가지 주요 목적을 수행합니다.
, Compose Multiplatform, Kotlin Multiplatform

이 문서에서는 Ktor를 활용하여 원활한 데이터 처리를 구현하면서 Android, iOS, 웹 및 데스크톱 플랫폼에서 실행되는 Kotlin 기반 풀스택 애플리케이션을 개발하는 방법을 배웁니다.

이 튜토리얼을 마치면 다음 사항을 수행할 수 있게 됩니다:

  • Kotlin Multiplatform을 사용하여 풀스택 애플리케이션 생성.
  • IntelliJ IDEA에서 생성된 프로젝트 구조 이해.
  • Ktor 서비스를 호출하는 Compose Multiplatform 클라이언트 제작.
  • 설계의 여러 계층에서 공유 타입(Shared types) 재사용.
  • 멀티플랫폼 라이브러리의 올바른 포함 및 구성.

이전 튜토리얼들에서는 할 일 관리자(Task Manager) 예제를 사용하여

요청 처리
Ktor와 Kotlin을 사용하여 할 일 관리자 애플리케이션을 빌드하며 라우팅, 요청 처리 및 매개변수의 기초를 배워보세요.
,
RESTful API 생성
JSON 파일을 생성하는 RESTful API 예제를 통해 Kotlin과 Ktor를 사용하여 백엔드 서비스를 빌드하는 방법을 배워보세요.
, 그리고
Exposed를 통한 데이터베이스 통합
Exposed SQL 라이브러리를 사용하여 Ktor 서비스를 데이터베이스 리포지토리에 연결하는 프로세스를 배워보세요.
방법을 살펴보았습니다. 당시 클라이언트 애플리케이션은 Ktor의 핵심 기능을 배우는 데 집중할 수 있도록 최대한 단순하게 유지되었습니다.

이제 표시할 데이터를 가져오기 위해 Ktor 서비스를 사용하는 Android, iOS, 웹 및 데스크톱 플랫폼용 클라이언트를 만들 것입니다. 가능한 한 클라이언트와 서버 간에 데이터 타입을 공유하여 개발 속도를 높이고 오류 발생 가능성을 줄일 것입니다.

사전 요구 사항

이전 문서들과 마찬가지로 IntelliJ IDEA를 IDE로 사용합니다. 환경 설치 및 구성에 대해서는 Kotlin Multiplatform 퀵스타트 를 참조하세요.

Compose Multiplatform을 처음 사용하는 경우, 이 튜토리얼을 시작하기 전에 Compose Multiplatform 시작하기 튜토리얼을 먼저 완료하는 것을 권장합니다. 작업의 복잡도를 줄이기 위해 단일 클라이언트 플랫폼에 집중할 수도 있습니다. 예를 들어 iOS를 사용해 본 적이 없다면 데스크톱이나 Android 개발에 집중하는 것이 현명할 수 있습니다.

새 프로젝트 생성하기

Ktor 프로젝트 생성기 대신 IntelliJ IDEA의 Kotlin Multiplatform 프로젝트 위저드(Wizard)를 사용합니다. 이를 통해 클라이언트와 서비스를 확장해 나갈 수 있는 기본 멀티플랫폼 프로젝트가 생성됩니다. 클라이언트는 SwiftUI와 같은 네이티브 UI 라이브러리를 사용할 수도 있지만, 이 튜토리얼에서는 Compose Multiplatform을 사용하여 모든 플랫폼을 위한 공유 UI를 만들 것입니다.

  1. IntelliJ IDEA를 실행합니다.
  2. IntelliJ IDEA에서 File | New | Project를 선택합니다.
  3. 왼쪽 패널에서 Kotlin Multiplatform을 선택합니다.
  4. New Project 창에서 다음 필드를 지정합니다:
    • Name : full-stack-task-manager
    • Project ID : com.example.ktor
  5. 대상 플랫폼으로 Android, Desktop, Web, 그리고 Server를 선택합니다.

  6. Mac을 사용 중이라면 iOS도 선택하세요. Share UI 옵션이 선택되어 있는지 확인합니다. Kotlin Multiplatform 위저드 설정

  7. Create 버튼을 클릭하고 IDE가 프로젝트를 생성하고 임포트할 때까지 기다립니다.

서비스 실행하기

  1. IntelliJ IDEA에서 ApplicationKt 실행 구성을 선택합니다. Run & Debug 창
  2. Run 버튼 (IntelliJ IDEA 실행 아이콘)을 클릭하여 해당 구성을 실행합니다.

    Run 도구 창에 새 탭이 열립니다.

  3. 브라우저에서 http://0.0.0.0:8080/로 접속하여 애플리케이션을 엽니다. 브라우저에 Ktor가 표시하는 메시지가 나타나야 합니다. Ktor 서버 브라우저 응답

프로젝트 살펴보기

server 폴더는 프로젝트에 있는 세 개의 Kotlin 모듈 중 하나입니다. 나머지 두 개는 coreapp입니다.

server 모듈의 구조는 Ktor 프로젝트 생성기에서 생성된 구조와 매우 유사합니다. 플러그인과 의존성을 선언하기 위한 전용 빌드 파일이 있으며, Ktor 서비스를 빌드하고 실행하기 위한 코드가 포함된 소스 세트가 있습니다:

Kotlin Multiplatform 프로젝트 내 server 폴더 내용

Application.kt 파일의 라우팅 지침을 살펴보면 sayHello() 함수를 호출하는 것을 볼 수 있습니다:

kotlin

sayHello() 함수는 core 모듈에 정의되어 있습니다. 이곳이 서버와 모든 다양한 클라이언트 플랫폼 간에 공유될 공통 코드를 두는 곳입니다.

app/shared/src/commonMain 모듈 내의 Greeting.kt 파일을 열어보면 sayHello() 함수가 그곳에서도 사용되고 있음을 확인할 수 있습니다:

kotlin

app 모듈에는 다음 서브모듈들이 포함되어 있습니다:

  • androidApp, desktopApp, iosApp, webApp 서브모듈은 각각 Android, 데스크톱, iOS, 웹 클라이언트 앱을 위한 플랫폼별 코드를 담고 있습니다. 현재 이 클라이언트 앱들 중 어느 것도 Ktor 서비스와 연결되어 있지 않습니다.
  • shared 서브모듈은 클라이언트를 제공하려는 각 플랫폼에 대한 소스 세트를 포함합니다. 이는 commonMain 내에 선언된 타입들이 대상 플랫폼마다 다른 기능을 필요로 하기 때문입니다.

    예를 들어, Greeting 타입에서 현재 플랫폼의 이름은 기대(expected) 및 실제(actual) 선언을 통해 플랫폼별 API를 사용하여 가져옵니다.

    shared 서브모듈의 commonMain 소스 세트에서 getPlatform() 함수는 expect 키워드와 함께 선언되어 있습니다:

    kotlin

    그런 다음 아래와 같이 각 대상 플랫폼은 getPlatform() 함수의 actual 선언을 제공합니다:

    kotlin
    kotlin
    kotlin
    kotlin

클라이언트 애플리케이션 실행하기

대상 플랫폼에 대한 실행 구성을 실행하여 클라이언트 애플리케이션을 구동할 수 있습니다. iOS 시뮬레이터에서 애플리케이션을 실행하려면 아래 단계를 따르세요:

  1. IntelliJ IDEA에서 iosApp 실행 구성과 시뮬레이션 장치를 선택합니다. Run & Debug 창
  2. Run 버튼 (IntelliJ IDEA 실행 아이콘)을 클릭하여 구성을 실행합니다.
  3. iOS 앱을 실행하면 내부적으로 Xcode로 빌드되어 iOS 시뮬레이터에서 실행됩니다. 앱에는 클릭 시 이미지를 토글하는 버튼이 표시됩니다. iOS 시뮬레이터에서 앱 실행 중

    버튼을 처음 누르면 현재 플랫폼의 세부 정보가 텍스트에 추가됩니다. 이를 구현하는 코드는 app/shared/src/commonMain/kotlin/com/example/ktor/App.kt에서 찾을 수 있습니다:

    kotlin

    이것은 Composable 함수이며, 이 문서의 뒷부분에서 수정할 예정입니다. 지금 중요한 것은 이것이 UI를 표시하고 공유 Greeting 타입을 활용하며, 이 타입은 다시 공통 Platform 인터페이스를 구현하는 플랫폼별 클래스를 사용한다는 점입니다.

생성된 프로젝트의 구조를 이해했으므로, 이제 할 일 관리자 기능을 점진적으로 추가할 수 있습니다.

모델 타입 추가하기

먼저 모델 타입을 추가하고 클라이언트와 서버 모두에서 접근 가능한지 확인합니다.

  1. gradle/libs.versions.toml 파일로 이동하여 다음 kotlinx.serialization 의존성을 정의합니다:
    toml
  2. core/build.gradle.kts 파일로 이동하여 직렬화(serialization) 플러그인을 추가합니다:

    kotlin
  3. 같은 파일에서 commonMain 소스 세트에 새 의존성을 추가합니다:

    kotlin
  4. IntelliJ IDEA에서 Build | Sync Project with Gradle Files를 선택하여 업데이트를 적용합니다. Gradle 임포트가 완료되면 Task.kt 파일이 성공적으로 컴파일되는 것을 확인할 수 있습니다.
  5. core/src/commonMain/kotlin/com/example/ktor 폴더로 이동하여 model이라는 새 패키지를 생성합니다.
  6. 새 패키지 안에 Task.kt라는 새 파일을 생성합니다.
  7. 우선순위를 나타내는 enum과 할 일을 나타내는 클래스를 추가합니다. Task 클래스는 kotlinx.serialization 라이브러리의 Serializable 어노테이션을 가집니다:

    kotlin

서버 생성하기

다음 단계는 할 일 관리자를 위한 서버 구현을 만드는 것입니다.

  1. server/src/main/kotlin/com/example/ktor 폴더로 이동하여 model이라는 서브 패키지를 생성합니다.
  2. 이 패키지 안에 TaskRepository.kt 파일을 새로 만들고 리포지토리를 위한 다음 인터페이스를 추가합니다:

    kotlin
  3. 같은 패키지에 InMemoryTaskRepository.kt라는 새 파일을 만들고 다음 클래스를 작성합니다:

    kotlin
  4. server/src/main/kotlin/.../Application.kt로 이동하여 기존 코드를 아래 구현으로 교체합니다:

    kotlin

    이 구현은 단순화를 위해 모든 라우팅 코드를 Application.module() 함수 안에 배치했다는 점을 제외하면 이전 튜토리얼의 내용과 매우 유사합니다.

    이 코드를 입력하고 임포트를 추가하면 컴파일 에러가 여러 개 발생할 것입니다. 이는 코드에서 웹 클라이언트와의 상호 작용을 위한

    CORS
    필요한 의존성: io.ktor:%artifact_name%
    플러그인을 포함하여 의존성으로 추가해야 할 여러 Ktor 플러그인을 사용하고 있기 때문입니다.

  5. gradle/libs.versions.toml 파일을 열고 다음 라이브러리들을 정의합니다:
    toml
  6. 서버 모듈 빌드 파일(server/build.gradle.kts)을 열고 다음 의존성들을 추가합니다:

    kotlin
  7. 다시 한번 메인 메뉴에서 Build | Sync Project with Gradle Files를 실행합니다. 임포트가 완료되면 ContentNegotiation 타입과 json() 함수에 대한 임포트가 정상적으로 작동하는 것을 확인할 수 있습니다.
  8. 서버를 다시 실행합니다. 브라우저에서 경로에 접근할 수 있음을 확인할 수 있습니다.
  9. 로 접속하여 JSON 형식의 할 일 목록이 담긴 서버 응답을 확인하세요. 브라우저에서의 서버 응답

클라이언트 생성하기

클라이언트가 서버에 접근할 수 있도록 하려면 Ktor Client를 포함해야 합니다. 여기에는 세 가지 유형의 의존성이 관련됩니다:

  • Ktor Client의 핵심(Core) 기능.
  • 네트워킹을 처리하기 위한 플랫폼별 엔진.
  • 콘텐츠 협상(Content Negotiation) 및 직렬화 지원.
  1. gradle/libs.versions.toml 파일에 다음 라이브러리들을 추가합니다:
    toml
  2. app/shared/build.gradle.kts로 이동하여 다음 의존성들을 추가합니다:
    kotlin

    이 작업이 완료되면 클라이언트에서 Ktor Client를 감싸는 얇은 래퍼(wrapper) 역할을 할 TaskApi 타입을 추가할 수 있습니다.

  3. 메인 메뉴에서 Build | Sync Project with Gradle Files를 선택하여 빌드 파일의 변경 사항을 임포트합니다.
  4. app/shared/src/commonMain/kotlin/com/example/ktor 폴더로 이동하여 network라는 새 패키지를 생성합니다.
  5. 새 패키지 안에 클라이언트 설정을 위한 HttpClientManager.kt 파일을 생성합니다:

    kotlin

    1.2.3.4를 현재 머신의 IP 주소로 바꾸세요. Android 가상 장치나 iOS 시뮬레이터에서 실행되는 코드에서는 0.0.0.0 또는 localhost로 호출할 수 없습니다.

    TIP

    IP 주소 찾기:

    모바일 시뮬레이터는 localhost에 도달할 수 없으므로 머신의 실제 IP 주소가 필요합니다. IP 주소를 확인하려면 다음 명령어 중 하나를 실행하세요:

    • macOS: ifconfig | grep "inet " | grep -v 127.0.0.1
    • Linux: hostname -I | awk '{print $1}'
    • Windows: ipconfig 실행 후 "IPv4 Address" 확인
  6. 같은 app/shared/.../network 패키지에 다음 구현이 담긴 TaskApi.kt 파일을 생성합니다:

    kotlin
  7. app/shared/.../App.kt로 이동하여 코드를 아래 구현으로 교체합니다. 이 코드는 TaskApi 타입을 사용하여 서버에서 할 일 목록을 가져온 다음, 각 할 일의 이름을 컬럼(Column)에 표시합니다:

    kotlin
  8. 서버가 실행 중인 상태에서 iosApp 실행 구성을 사용하여 iOS 애플리케이션을 테스트합니다.

  9. Fetch Tasks 버튼을 클릭하여 할 일 목록을 표시합니다: iOS에서 실행 중인 앱

    NOTE

    이 데모에서는 명확성을 위해 프로세스를 단순화했습니다. 실제 애플리케이션에서는 네트워크를 통해 암호화되지 않은 데이터를 전송하는 것을 피하는 것이 매우 중요합니다.
  10. Android 플랫폼에서는 애플리케이션에 네트워킹 권한을 명시적으로 부여하고 일반 텍스트(cleartext) 데이터를 주고받을 수 있도록 허용해야 합니다. 이 권한을 활성화하려면 app/androidApp/src/main/AndroidManifest.xml을 열고 다음 설정을 추가하세요:

    xml
  11. app.androidApp 실행 구성을 사용하여 Android 애플리케이션을 실행합니다. 이제 Android 클라이언트도 정상적으로 실행되는 것을 볼 수 있습니다: Android에서 실행 중인 앱

  12. 데스크톱 클라이언트의 경우, 창에 크기와 타이틀을 지정할 것입니다. app/desktopApp/src/.../main.kt 파일을 열고 title을 변경하고 state 속성을 설정하여 코드를 수정합니다:

    kotlin
  13. app [hot] 🔥 실행 구성을 사용하여 데스크톱 애플리케이션을 실행합니다: 데스크톱에서 실행 중인 앱

  14. 다음 실행 구성 중 하나를 사용하여 웹 클라이언트를 실행합니다:

    • app [js]: Kotlin/JS 애플리케이션을 실행합니다.
    • app [wasmJs]: Kotlin/Wasm 애플리케이션을 실행합니다.
    웹에서 실행 중인 앱

UI 개선하기

이제 클라이언트가 서버와 통신하고 있지만, 아직 매력적인 UI라고 하기는 어렵습니다.

  1. app/shared/src/commonMain/.../ktor에 위치한 App.kt 파일을 열고 기존 App을 아래의 AppTaskCard Composable로 교체합니다:

    kotlin

    이 구현을 통해 클라이언트는 기본적인 기능을 갖추게 되었습니다.

    LaunchedEffect 타입을 사용하여 시작 시 모든 할 일을 로드하고, LazyColumn Composable을 사용하여 사용자가 할 일 목록을 스크롤할 수 있게 했습니다.

    마지막으로 별도의 TaskCard Composable을 만들어 Card를 사용하여 각 Task의 세부 정보를 표시했습니다. 할 일을 삭제하거나 업데이트하기 위한 버튼들도 추가되었습니다.

  2. 클라이언트 애플리케이션(예: Android 앱)을 다시 실행합니다. 이제 할 일 목록을 스크롤하고 세부 정보를 확인하며 삭제할 수 있습니다: 개선된 UI로 Android에서 실행 중인 앱

업데이트 기능 추가하기

클라이언트를 완성하기 위해 할 일의 세부 정보를 업데이트할 수 있는 기능을 통합합니다.

  1. app/shared/src/commonMain/.../ktor에 있는 App.kt 파일로 이동합니다.
  2. 아래와 같이 UpdateTaskDialog Composable과 필요한 임포트를 추가합니다:

    kotlin

    이 Composable은 다이얼로그 박스로 Task의 세부 정보를 표시합니다. descriptionpriorityTextField Composable 안에 배치되어 업데이트가 가능합니다. 사용자가 업데이트 버튼을 누르면 onConfirm() 콜백이 호출됩니다.

  3. 같은 파일에서 App Composable을 업데이트합니다:

    kotlin

    선택된 현재 할 일을 저장하기 위해 추가적인 상태(state)를 관리합니다. 이 값이 null이 아니면 UpdateTaskDialog Composable을 호출하고, onConfirm() 콜백이 TaskApi를 사용하여 서버에 POST 요청을 보내도록 설정합니다.

    마지막으로 TaskCard Composable을 생성할 때 onUpdate() 콜백을 사용하여 currentTask 상태 변수를 설정합니다.

  4. 클라이언트 애플리케이션을 다시 실행합니다. 이제 버튼을 사용하여 각 할 일의 세부 정보를 업데이트할 수 있습니다. Android에서 할 일 삭제하기

다음 단계

이 문서에서는 Kotlin Multiplatform 애플리케이션의 맥락 내에서 Ktor를 사용해 보았습니다. 이제 다양한 플랫폼을 대상으로 하는 여러 서비스와 클라이언트가 포함된 프로젝트를 만들 수 있습니다.

살펴보았듯이 코드 중복이나 낭비 없이 기능을 구축할 수 있습니다. 프로젝트의 모든 계층에서 필요한 타입은 core 멀티플랫폼 모듈에 배치할 수 있습니다. 서비스에만 필요한 기능은 server 모듈에, 클라이언트에만 필요한 기능은 app 모듈에 배치합니다.

이러한 방식의 개발은 클라이언트와 서버 기술 모두에 대한 지식이 필요합니다. 하지만 Kotlin Multiplatform 라이브러리와 Compose Multiplatform을 사용하면 새로 배워야 할 내용의 양을 최소화할 수 있습니다. 처음에는 단일 플랫폼에만 집중하더라도 애플리케이션에 대한 수요가 늘어남에 따라 다른 플랫폼을 쉽게 추가할 수 있습니다.