티스토리 뷰

개요

    • Kotlin 언어를 접해본 사람은 다른 언어로 돌아가기 어려워한다. 언어의 저변을 떠나 굉장한 생산성과 편의성을 제공하기 때문이다. 이런 가정을 해보자. 만약, 백엔드에 추가로 프론트엔드까지 한 프로젝트를 전부 Kotlin 언어로 제작할 수 있다고 생각해보자. Kotlin 숙련자라면 굉장히 빠른 속도로 프로젝트를 제작할 수 있을 것이다. 이번 글에서 소개할 KVision은 바로 그런 의도로 탄생하였다.
    • KVisionKotlin 언어가 가진 강력한 DSL 기능을 이용하여 개발된 오픈 소스 풀스택 Kotlin 웹 프레임워크이다. 하나의 프로젝트 내에서 백엔드, 프론트엔드 모든 영역을 Type-Safe가 보장되는 Kotlin 언어로 작성할 수 있다는 장점이 있다.
    • KVisionUI 디자인은 Bootstrap을 기반으로 설계되어 관련 기능이 모두 Kotlin DSL로 제공된다. (추가적으로 PatternFly를 적용할 수도 있다.)
    • KVision의 백엔드 영역은 다양한 프레임워크에 대응할 수 있도록 설계되어 있다. 이번 글에서는 대부분 개발자들에게 익숙한 Spring Boot를 선정하여 프로젝트 제작 방법을 소개하고 자한다.

    프로젝트 생성

    • KVision + Spring Boot 프로젝트를 처음부터 작성하려면 손이 무척 많이 간다. 아래와 같이 제작자가 제공하는 템플릿 프로젝트를 내려 받아 시작하는 방법을 추천한다.
    # 샘플 프로젝트 복제
    $ git clone https://github.com/rjaros/kvision-examples.git
    
    # Spring Boot 샘플 프로젝트로 이동
    $ cd kvision-examples/template-fullstack-spring-boot
    

    애플리케이션 실행

    • 풀스택 프로젝트는 아래와 같이 애플리케이션을 실행할 수 있다.
    # 8080 포트로 백엔드 실행
    $ gradlew backendRun
    
    # 백엔드 변경점을 런타임에서 반영하기 위한 모니터링 실행
    $ gradlew -t compileKotlinBackend  
    
    # 3000 포트로 프론트엔드 실행
    $ gradlew frontendRun
    

    애플리케이션 배포

    • 풀스택 프로젝트는 아래와 같이 백엔드와 프론트엔드가 모두 통합된 .jar 파일을 빌드할 수 있다.
    $ gradlew clean jar
    
    • Docker 이미지를 생성하고자 할 경우 아래와 같이 Spring Boot의 기본 방법을 그대로 따르면 된다. (보다 자세한 내용은 본 블로그의 이 글을 참고한다.)
    # Docker 이미지 빌드
    $ gradlew bootBuildImage --imageName=kvision/example
    
    # 빌드된 Docker 이미지 실행
    $ docker run -d -p 8080:8080 kvision/example
    
    • Docker 컨테이너 실행시 8080 포트를 맵핑해야 작동한다. 실제 운영 환경에서 로드 밸런서 연동시 참고한다.

    PatternFly 적용

    • PatternFly는 일관된 모던 디자인을 제공하기 위해 RedHat에서 오픈 소스로 개발한 인터페이스 디자인 시스템이다. KVision에서도 지원하기 때문에 손쉽게 적용할 수 있다. 프로젝트 내 /build.gradle.kts 파일에 아래 내용을 추가한다.
    kotlin {
        ...
        sourceSets {
            ...
            val frontendMain by getting {
                ...
                dependencies {
                    ...
                    implementation(npm("@patternfly/patternfly", "4.87.3"))
                    implementation(npm("@patternfly/react-core", "4.97.2"))
                }
                ...
            }
        }
    }
    
    • @patternfly/react-core 패키지를 추가하면 PatternFlyReact 컴포넌트를 이용할 수 있어 매우 편리하다. (React 컴포넌트 연동 예는 아래 작성하였다.)
    • 이제 프론트엔드에 스타일을 적용할 차례이다. 프로젝트 내 /src/frontendMain/kotlin/App.kt에 아래 내용을 추가하면 적용이 완료된다.
    import io.kvision.require
    
    class App : Application() {
    
        init {
            require("@patternfly/patternfly/patternfly.min.css")
            require("@patternfly/patternfly/patternfly-addons.css")
            ...
        }
        ...
    }
    

    비지니스 로직 작성 순서

    • KVision의 비지니스 로직 작성은 아래 순서를 따른다.
    1. /src/commonMain/kotlin 폴더에 @Serializable이 명시된 Model 클래스 작성
    2. /src/commonMain/kotlin 폴더에 @KVService이 명시된 Service 인터페이스 작성
    3. /src/backendMain/kotlin 폴더에 @Service, @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)이 명시된 Service 구현체 클래스 작성
    4. /src/frontEndMain/kotlin 폴더에 작성된 App 클래스에서 해당 구현체의 인스턴스를 싱글턴으로 사용
    
    • 개발자는 위 순서대로 풀스택 영역에 걸쳐 전부 Kotlin 언어로로 코드를 작성하고 호출하기만 하면 된다. 빌드 과정에서 프레임워크에 의해 내부적으로 서비스 호출부가 AJAX 통신의 형태로 자동 변환된다.

    비지니스 로직 작성 예

    • 가장 먼저 백엔드와 프론트엔드에서 공통으로 사용 가능한 모델 클래스를 작성할 차례이다. /src/commonMain/kotlin 경로에 작성한다.
    @Serializable
    data class Book(
    
        val title: String,
        val author: String,
        val year: Int,
        val rating: Int
    )
    
    • 아래는 @KVService 서비스 인터페이스의 작성 예이다. 일반적인 Spring Boot에서의 구현 방법과 약간의 차이가 있을 뿐 쉽게 적응할 수 있는 구조이다. /src/commonMain/kotlin 경로에 작성한다.
    • 유의할 점은 인터페이스 이름을 지을 때 반드시 IXXXService 형태의 네이밍 컨벤션을 따라야 한다.
    @KVService
    interface IBookService {
        suspend fun getBooks(): List<Book>
    }
    
    • 아래는 앞의 인터페이스를 기반으로 @Service 서비스 구현체를 작성한 예이다. /src/backendMain/kotlin 경로에 작성한다.
    @Service
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    actual class BookService: IBookService {
    
        override suspend fun getBooks(): List<Book> {
    
            return listOf(
                Book("넷플릭스의 시대", "라몬 로바토", 2020, 5),
                Book("THIS IS TOYOTA 도요타 이야기", "노지 츠네요시", 2019, 4)
            )
        }
    }
    
    • 작성된 서비스 클래스가 프론트엔드에서 사용 가능한 형태가 되기 위해서는 백엔드의 매니저 목록에 추가되어야 한다. 백엔드 빌드 후 /src/backendMain/kotlin/Main.kt 파일에 아래 자동 생성된 매니저 클래스를 추가하면 이제 프론트엔드에서 서비스 클래스를 사용할 준비가 완료된다.
    class KVApplication {
        @Bean
        fun getManagers() = listOf(BookServiceManager, ...)
    }
    
    • 마지막으로 프론트엔드에서 앞서 작성한 서비스를 이용할 차례이다. /src/frontEndMain/kotlin/App.kt 파일에 아래 내용을 추가한다.
    class App : Application() {
    
        private val bookService = BookService()
    
        private lateinit var aText: Div
    
        override fun start() {
            aText = div {
                fontSize = 32.px
                content = bookService.getBooks().first().title
            }
        }
    }
    

    테이블 작성 예

    • 다음은 프론트엔드에서 테이블을 작성하는 방법이다. 먼저 프론트엔드에 모델 오브젝트를 작성한다. ObservableList<T>를 이용하여 테이블에 연동할 데이터의 상태를 저장하는 역할을 수행한다.
    object BookModel {
    
        private val bookService: BookService = BookService()
        val books: ObservableList<Book> = observableListOf()
    
        suspend fun getBooks() {
            GlobalScope.launch {
                books.syncWithList(bookService.getBooks())
            }
        }
    }
    
    • 다음으로 실제 테이블 렌더링을 담당할 패널 오브젝트를 작성할 차례이다. 사전 작성된 모델을 이용하여 직접적으로 DOM을 제어하지 않고, 데이터의 변경점이 자동으로 테이블에 반영되도록 설계했다.
    object BookListPanel : SimplePanel() {
    
        init {
            // 페이지 로드시 Model을 백엔드로부터 미리 획득
            GlobalScope.launch { BookModel.getBooks() }
    
            // table 생성 및 Model과 상태 연동
            val bookTable = table(
                Model.books,
                types = setOf(TableType.STRIPED, TableType.HOVER),
                responsiveType = ResponsiveType.RESPONSIVE
            ) { books ->
                headerCell("Title")
                headerCell("Author")
                headerCell("Year")
                headerCell("Rating")
                books.forEach {
                    row {
                        cell(it.title)
                        cell(it.author)
                        cell(it.year.toString())
                        cell(it.rating.toString())
                    }
                }
            }
    
            // button 생성 및 Model에 데이터 추가 연동, table에도 변경된 Model의 데이터가 반영
            button("책 추가하기", style = ButtonStyle.PRIMARY).onClick {
                GlobalScope.launch {
                    BookModel.books.add(Book("커리어 스킬", "존 손메즈", 2016, 10))
                }
            }
        }
    }
    
    • 마지막으로 아래와 같이 작성한 패널을 추가하면 테이블 작성이 완료된다.
    class App : Application() {
    
        override fun start() {
            root("kvapp") {
                add(BookListPanel())
            }
        }
    

    PatternFly + React: DatePicker 작성 예

    • 아래는 PatternFlyReact 컴포넌트를 이용하여 DatePicker 컴포넌트를 작성한 예이다.
    // Type-Safe의 props 인터페이스를 설계
    external interface PFDatePickerProps : RProps {
        var value: String
        var placeholder: String
        var onChange: (String) -> Unit
    }
    
    // KVision에서 작동 가능한 PFDatePicker 컴포넌트를 생성
    val PFDatePicker: RClass<PFDatePickerProps> = require("@patternfly/react-core").DatePicker
    
    // state 초기 값을 부여하여 DatePicker 렌더링
    val datePicker = react(state = "2021-02-19") { getState, changeStatus ->
        PFDatePicker {
            attrs.value = getState()
            attrs.placeholder = "yyyy-MM-dd"
            attrs.onChange = { value -> changeStatus { value } }
        }
    }
    
    // 클릭시 state 값을 변경하는 Button 렌더링
    button("Change Date", style = ButtonStyle.PRIMARY)
        .onClick {
            datePicker.state = "2021-01-01"
        }
    

    참고 글

댓글
댓글쓰기 폼