본문 바로가기

Spring + Kotlin API Server 만들기 (7) : 간단한 Page추가, CSR & SSR

어차피 UI 구성은 React로 할 계획이지만 MVC pattern에서 View 계층을 위해 간단한 페이지를 추가한다.

CSR & SSR이란?

CSR , SSR은 Client Side Rendering , Server Side Rendering의 약자로 단어 그대로

화면 rendering을 각각 client , server에서 하는 것을 의미한다.

프로젝트에서의 CSR & SSR

이번 프로젝트에서 CSR에 해당하 는건 react이고, SSR에 해당하는건 지금 포스팅하는 mustache이다.

Controller 설계

기존 Controller는 RestController로, 화면을 응답으로 주는 controller가 필요하다. 새로운 controller를 만들기 전에 기존 controller를 APIController라고 이름을 바꿔주자.

  • 기존 DeviceController → DeviceAPIController 변경
package com.studuy.study.device

import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.MediaType
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class DeviceAPIContorller(
    private val deviceService: DeviceService
) {
    @Operation(summary = "Device list를 주는 API")
    @GetMapping("/v1/devices")
    fun getDevices(): Iterable<Device> {
        return deviceService.getAllDevice()
    }

    @Operation(summary = "Device를 생성하는 API")
    @PostMapping("/v1/devices")
    fun createDevice(): Device {
        return deviceService.createDevice()
    }

    @Operation(summary = "Device data를 주는 API")
    @GetMapping("/v1/devices/{deviceId}")
    fun getDevice(@PathVariable("deviceId") deviceId: Long): Device {
        return deviceService.getDevice(deviceId)
    }

    @Operation(summary = "Device의 value를 수정하는 API")
    @PatchMapping("/v1/devices/{deviceId}/value")
    fun updateDeviceValue(
        @PathVariable("deviceId") deviceId: Long,
        @RequestBody @Validated value: String
    ): Device {
        return deviceService.updateDeviceValue(deviceId, value)
    }

    @Operation(summary = "Device의 command를 수정하는 API")
    @PatchMapping("/v1/devices/{deviceId}/command")
    fun updateDeviceCommand(
        @PathVariable("deviceId") deviceId: Long,
        @RequestBody @Validated command: String
    ): Device {
        return deviceService.updateDeviceCommand(deviceId, command)
    }
}
  • 새로운 DeviceContoller 작성
package com.studuy.study.device

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable

@Controller
class DeviceController(
    private val deviceService: DeviceService
) {
    @GetMapping("/devices")
    fun getDeviceList(model: Model): String {
        val devices = deviceService.getAllDevice()
        val devicesDto =  devices.map { device: Device ->
            mapOf(
                "id" to device.id!!,
                "value" to (device.value ?: "null"),
                "command" to (device.command ?: "null"),
                "url" to "/devices/${device.id!!.toString()}"
            )
        }
        model["title"] = "Device list"
        model["devices"] = devicesDto
        return "device/devices"
    }

    @GetMapping("/devices/{deviceId}")
    fun getDevice(
        @PathVariable("deviceId") deviceId: Long,
        model:Model
    ) : String{
        val device = deviceService.getDevice(deviceId)
        model["title"] = "Device ${device.id}"
        model["id"] = device.id!!.toString()
        model["command"] = device.command ?: "null"
        model["value"] = device.value ?: "null"
        return "device/device"
    }
}
  • 설명
    • @Controller
      • RestController와는 다르게 html file을 리턴하는 class인걸 명시해준다.
      • 각 함수의 return은 String이며, return 된 String과 같은 이름의 파일(확장자 제외)을 resources/templates에서 찾아 랜더링 한 뒤 응답으로 보낸다.
    • Model
      • ui data를 보내기 위한 그릇이라고 생각하면 된다.
      • key를 설정하고 안에 data를 넣어서 보내면 view계층에서 사용할 수 있다.

Page 작성

기본적으로 header, footer를 작성해 준다.

  • header.mustache
<html>
<head>
    <title>{{title}}</title>
</head>
<body>
  • footer.mustache
</body>
</html>

그다음으로 메인 화면인 home, Device list들을 보여주는 devices, Device의 정보를 보여주는 device페이지를 다음과 같이 작성한다.

  • home.mustache
{{> header}}
<h1 style="width=100%; height=auto; margin=auto; align-items=center;">{{comment}}</h1>
<h2>Entity</h2>
<p> - {{entity}}</p>
<p>
    <a href="/api">API Documents</a>
</p>
<p>
    <a href="/devices">Device lists</a>
    <!--
        react로 redircet 할 예정
       <a href="/devices">Device lists</a>
    -->
</p>
{{> footer}}
  • devices.mustache
{{> header}}
<h1>Device list</h1>
{{#devices}}
<div style="height: auto; margin-top: 30px; margin-bottom: 30px;">
    <p>id : {{id}}</p>
    <p>value : {{value}}</p>
    <p>command : {{command}}</p>
    <p>
        <a href={{url}} role="button">go to device {{id}}</a>
    </p>
</div>
{{/devices}}

{{> footer}}
  • device.mustache
{{> header}}
<h1>Device ID: {{id}}</h1>
<h1>Value : {{value}}</h1>
<h1>Command : {{command}}</h1>
{{> footer}}

이전에 CI / CD를 구성해 놨으니 git push만 하면 자동으로 배포 되고, route53으로 호스팅했던 url로 접속하면 화면을 다음과 같이 볼 수 있다.

배포가 완료된 모습
잘 작동하는 모습