코틀린 클래스를 알아보자. 그리고 만들어보자 htmlBuilder

이누의 벨로그·2022년 5월 15일
0

이 글은 코드스피츠 82 코틀린JS로 배우는 코틀린 기초 4회차 내용의 요약 글입니다.

자바는 기본 클래스가 상속이 가능하도록 되어있고, final 키워들을 붙여야지만 상속이 불가능하도록 막을 수 있다. 하지만 코틀린은 정확히 그 반대로 되어있다. 코틀린의 기본 클래스는 상속이 불가능하며, open 키워드를 붙여야지만 상속이 가능하다. 심지어 open 키워드가 붙은 클래스라고 하더라도, 메소드를 오버라이드 하기 위해서는 메소드에도 open 키워드를 붙여야 한다. 즉, open 클래스의 open 메소드 만을 오버라이드 할 수 있다. 상속을 자주하지 말라는 의도가 보이는 언어차원의 방향이라고 볼 수 있다.

또한, 자바에서는 클래스의 접근 제한자를 생략하면 접근자가 internal로 설정되지만, 코틀린에서는 기본값이 public 이므로 접근자를 생략해도 public 클래스가 된다. 즉 아무 접근자가 없는 클래스는 자바 에서의 final public 클래스가 된다. 또한 명시적으로 constructor 라는 키워드로 생성자를 사용한다. 생성자 오버로딩 또한 가능하다. 따라서, 생성자가 여러개일 때는 다른 생성자를 전혀 호출하지 않는 디폴트 생성자는 오직 1개만 만들 수 있으며, 나머지 생성자는 무조건 다른 생성자를 호출하는 형태여야 한다.

open public class ClassTest0{
	private val propA:String
	private val propB:String
	public constructor(a:String, b:String){
		println("constructor1")
		propA = a
		propB = b
	}
	public constructor(a:String):this(a,"b"){
		println("constructor2")		
	}
}

상속 받은 클래스의 super의 체이닝이나 생성자 체이닝을 생략할 시에는 보통 메서드 마지막에 자동으로 이를 호출해준다. 자바에서는 이러한 방식을 채택하고 있다. 반면 자바스크립트 같은 경우, 생성자 또는 super 체이닝을 먼저 호출하여 생성자를 체이닝하기 위한 의존성을 먼저 해결한 뒤에 호출하는 클래스의 생성자를 접근할 수 있게 하고 있다. 이는 현대 언어의 흐름이기도 하다. 코틀린 또한 이러한 추세를 따라서, 생성자 체이닝의 경우 이를 반환타입으로 먼저 명시한 뒤에 오버로딩할 생성자를 접근할 수 있게 하고 있다. 즉, 생성자 체인을 먼저 호출하고, 이 때는 오버로딩할 생성자 내부의 변수나 지역변수는 사용할 수 없게 한다. (생성자의 인자는 사용할 수 있다)

또한, 기본 생성자에 한해서만, 클래스 바로 옆에 올 수 있다. 이 때 기본 생성자는 시그니쳐만 올 수 있으며 생성자 몸체는 올 수 없다. 시그니쳐에는 public 등의 접근 제한자 또한 포함된다. constructor 키워드는 접근 제한자가 public인 경우에만 생략 가능하다.

class ClassTest0 constructor(a:String, b:String){
	private val propA:String
	private val propB:String
	init{
		println("constructor1")
		propA = a
		propB = b
	}
}

따라서, 시그니처 옮기고 난 자리에 생성자 몸체를 init이라는 키워드로 가지게 된다. 코틀린이 이러한 생성자 문법을 만든 이유는 클래스 사용(호출) 코드와 만들 때의 코드를 비슷한 모양으로 일치시키기 위함이다.

코틀린은 여기에서 그치지 않고 생성자 함수 내부에서 가장 많이 일어나는 할당 작업 또한 제거할 수 있다. 방법은 다음과 같다

class ClassTest(private val propA:String, private val propB:String){
	init{
		println("constructor1")
	}
	constructor(a:String):this(a,"b"){
		println("constructor2")
	}
}

class 구문 옆에 오는 constructor 구문에서 var나 val로 선언된 변수들은 즉시 인스턴스의 속성이 된다. 따라서 init에서도, 내부 속성 선언도 필요하지 않다. 심지어 constructor는 몸체가 하는 일이 없다면 생략할 수도 있다.

class ClassTest(private val propA:String, private val propB:String){
	constructor(a:String):this(a,"b")
}

Super 체인

다음과 같은 상속 가능한 open 클래스가 있다고 하자.

open class ClassTest0(private val a:String, private val b:String){
	constructor(a:String):this(a,"b")
}

이를 상속받는 구문은 다음과 같다.

class ClassText1(a:String, b:String, private val c:String):ClassTest0(a,b)

constructor 내에서 val이나 var로써 속성선언이 되지 않는 인자들은 두 가지로 사용될 수 있다.

  1. init 몸체에서 자신이 가진 속성에 할당하기 위해서
  2. 자신의 속성과 관계없이 super 생성자에 전달하기 위해서

생성자의 인자로 아예 속성선언을 하거나 하지 않고 그냥 인자로 사용하는 것을 혼재해서 사용하기 때문에 주의가 필요하다.

Operator Overloading

오퍼레이터 오버로딩은 어떠한 클래스 객체에 대해 코틀린이 제공하는 기본 연산자를 사용자가 새롭게 정의하는 것으로, 반드시 클래스 내부에 있는 메소드에서만 가능하다. 우리가 어떠한 객체 내부에 컬렉션을 가지고 있을 때, 이 컬렉션에 접근하는 방법을 좀 더 간략화해서, 예를들어 [] 를 통해 사용하고 싶다고 하자. 실제로 우리가 자바스크립트나 C# 등에서 [] 로 속성을 get 할 때는 내부적으로는 get 메서드로 이를 바꿔주는 형태로 연산이 일어난다. 코틀린에서는 언어에서 제공하는 여러가지 연산자들(operator)에 대해 사용자가 이러한 연산자에 대한 행위를 새롭게 정의할 수 있는 operator overloading 을 제공한다.

class Map{
	private val map = mutableMapOf<String, String>()
	operator fun get(key:String) = map[key]
	operator fun set(key:String, value:String){map[key]=value}
}
val m = Map()
m["test"] = "123
println(m["test"])

mutableMapOf는 mutableMap 컬렉션이 제공하는 함수 로써 인자로 받아들이는 원소들을 가지고 태어난다. 코틀린에서는 new 없이 컬렉션 생성함수인 ~Of로 만드는 것이 일반적이다.

Map 객체는 분명히 map 컬렉션이 아니라 속성으로 map 컬렉션을 가지고 있을 뿐인데도, 객체 자체에 대한 get, set 접근을 map 에 접근하는 행위로 새롭게 정의하여 사용하고 있다. get과 set 뿐만 아니라 코틀린에서 제공하는 모든 연산자에 대한 사용자 정의가 가능하다.

실제로 코틀린의 모든 연산자는 각각 메소드 이름을 가지고 있어, 이를 operator로 명시하고 오버라이딩할 수 있다. 기본연산자라면 모두 오버로딩 할 수 있다. 그 종류가 실로 무궁무진하다. 아래의 공식문서에서는 오버로딩 가능한 연산자 종류를 볼 수 있다.

Operator overloading | Kotlin

언어 기본 연산자를 오버로딩할 수 있다는 것은 실로 굉장한 자유도가 아닐 수 없다. 하지만 operator overloading 은 클래스 밖에서 operator를 선언할 수는 없다는 점에 주의하자. 컴파일러가 그대로 메서드로 변환해주기 때문에 오버헤드도 없다.

Getter와 Setter

모던 언어에서는 대부분 Getter와 Setter를 지원한다. 코틀린 또한 마찬가지이다.

다음과 같이 속성에 대한 getter를 선언할 수 있다.

class Map {
    private val map = mutableMapOf<String, String>()
    val name: String? get() = this["name"]
    var job: String?
        get() = map["job"]
        set(value) {
            value?.let { this["job"] = it }
        }

    operator fun get(key: String) = map[key]
    operator fun set(key: String, value: String) {
        map[key] = value
    }
}

name과 job은 mutableMap에 존재하지 않을 수도 있으므로 nullable 로 선언하고, 속성 바로 옆에 get() 함수로 getter 를 선언하였다. setter 또한 동일하게 선언한다. job 속성의 setter 는 value에 대한 safe call을 하기 위해서 let으로 할당하도록 했다.

다음과 같이 사용할 수 있다

fun main() {
    println(Map().run {
        this["name"] = "Inu"
        job = "developer"
        name + ":" + job
    })
}

앞서 만들었던 operator를 사용해 set을 하고, 이 속성에 대한 getter와 setter를 사용해서 할당하고 있다.

그런데 한가지 의문이 든다. getter나 setter를 만들지 않은 일반적인 속성값에서, 코틀린 컴파일러는 과연 실제로는 이 속성에 내부적으로는 getter함수를 사용하여 값을 호출하는 걸까 아니면 속성값을 그냥 사용하는 걸까? 코틀린은 디폴트로 private이 아닌 모든 속성들에 대해 getter를 만든다. 만약 getter, setter를 만들지 않으려면 private 으로 만들어야 한다.

물론 언어마다 약간의 차이는 있다. 자바스크립트로 컴파일 할 때는 getter/setter를 만들지 않지만, JVM 플랫폼에서는 무조건 getter/setter를 속성마다 만든다.

by, by lazy

by는 두 군데에서 사용할 수 있다.

  1. 클래스의 끝부분
  2. 클래스 필드

= 은 객체에 할당할 값이 오는 연산자이다. 즉, 언어 자체에서 할당이라는 행위를 하는 약속된 연산자인 것이다. by 또한 동일하게 생각하면 된다. 단지 by 는 값을 그대로 할당하지 않는다는 차이가 있다. by 는 일종의 Proxy 객체가 할당된다. 코틀린에서 정확하게는 delegate, 위임 객체라고 부르는 것이다.

by 는 항상 delegate 객체와 같이 사용되며, by 를 이해하기 위해서는 delegate 객체의 스펙을 알아야 한다. 코틀린 delegate 객체가 하는 일은 단순하다. delete객체를 상속한 Lazy 객체를 살펴보면, value 라는 인터페이스를 상속하여 외부에서 value를 호출하였을 때 getter함수로 생성자로 받아들인 initializer 람다를 실행하여 값을 반환한다. 즉 lazy 하게 값을 반환하는 역할을 한다. by 는 단지 delegate 인터페이스의 value 라는 메소드 호출을 대신해주는 키워드에 지나지 않는다. 그렇다면 gettersettter 로도 이러한 Proxy, 혹은 placeholder 의 역할이 가능한데 delegate 객체를 사용하는 이유는, gettersetter 는 함수이기 때문에 어떠한 동일한 동작에 대해서도 이를 속성마다 전부 명시해줘야 하지만 delegate 객체를 사용하면 인스턴스를 한번만 사용해서 이를 재활용할 수 있기 때문이다.

lazy 함수의 인터페이스는 다음과 같다.

acutal 키워드는 멀티 플랫폼을 위한 바이트 코드를 생성하도록 하는 시스템 키워드이다. 사용빈도가 높지 않은 키워드 이므로 지나가도 좋다.

Delegate 객체를 상속받은 Lazy의 객체를 반환하는 UnsafeLazyImpl의 스펙은 다음과 같다.

이 Lazy 객체는 초기화시 UNINITIALIZED_VALUE 라는 상속받은 값으로 value를 초기화한다. delegate 객체는 valueOverride 할 책임을 가지고, 처음으로 value에 대한 get() 을 호출할 때만 value를 initializer 람다의 결과값으로 할당하고 이를 리턴한다. 앞서 언급한대로 lazy하게 속성을 할당하여 반환하는 역할만을 하며, Delegate 객체는 오버라이드 해야할 value 를 자식에게 제공해줄 뿐이다. 단지 이를 by 키워드와 같이 사용하면 컴파일러가 자동으로 value 의 반환값을 할당해주게 된다. 즉 by.value 라는 인터페이스 호출을 한단계만 축약해주는 역할을 한다.

위 객체는 이름 그대로 Thread safe하지 않기 때문에 싱글쓰레드를 사용하는 자바스크립트로의 컴파일시에 변환되는 객체이다. JVM의 멀티쓰레드를 위한 Thread-safe Lazy 객체도 있으며 스펙은 다음과 같다.

동작은 똑같지만 volatile과 lock을 사용해 훨씬 무거워졌다. 코틀린은 이처럼 쓰레드 동기화 비용을 지불하더라도 안전성을 보장하여 프로그래머가 쓰레드 동시성을 신경쓰지 않고 사용하는 것을 지향하고 있다. Lazy를 이용하면 할당된 값을 생성하는 비용을 호출시점으로 미룬다는 것만 기억하자.

다시 by lazy 구문으로 돌아가 실제 사용을 살펴보자.

class Map{
	private val map = mutableMapOf<String,String>()
	operator fun get(key:String) = map[key]
	operator fun set(key:String, value:String){ map[key]=value}
	val name by lazy{map["firstname"]+" " + map["lastname"]}
}

name을 호출하기 전까지는 반환값을 연산하지 않으며, 따라서 호출하는 시점에 이미 map[”firstname”]map[”lastname”]이 생성되어 있지 않다면 null을 리턴한다. 또한, 형을 확정해야하는 getter, setter와 달리 lazy 객체는 생성시점에 람다로 리턴 타입 <T> 가 확정이기 때문에 타입을 캐스팅할 필요도 없다.

무거운 작업, 쓰레드 safe인 경우 lazy 객체를 사용해 연산을 미루는 경우가 많다. 특히 자바스크립트로 컴파일 되는 경우에는 신경쓰지 않고 lazy 객체를 사용할 수 있다.

Object & Companion Object

자바에서 static 메서드를 사용할 때는 다음과 같이 사용한다.

class Parent{
	static void action(){}
}
Parent.action();

똑같은 코틀린에서 할 때는 Companion Object를 생성한다. 우리말로는 동반 객체 이다.

class Parent{
	companion object{
		fun action(){}
	}
}
Parent.action();

companion 객체에 메서드나 속성을 사용하면 똑같이 Parent.action을 호출할 수 있다. 즉 Companion Object 블럭 내부에 선언한 속성이나 메서드들은 전부 static이다.

이렇게 사용하면 어떤점이 자바와 다를까? Companion object 블럭 안에 선언하도록 강제되기 때문에 static 속성/메서드가 응집된다. 코틀린 이러한 응집을 통해 static의 중복이나 잘못된 사용을 최대한 줄이려고 의도하고 있다.

static은 런타임에 인스턴스를 만드는 것이 아니라 컴파일 타임에 컴파일러가 생성하는 클래스 객체에 귀속된다. 코틀린에서는 이를 아예 인스턴스로부터 분리하여 다른 컨텍스트 를 가지게 한 것이다. 문법적으로 static 클래스 객체를 분리하여 보다 쉽게 구분하도록 한 것이라고 할 수 있다.

익명 클래스

자바에서 익명 클래스는 abstract 혹은 interface 만 가능한 문법이다.

런타임에 이 두가지 객체의 구현을 명시하면서 인스턴스를 생성하는 것이다

abstract class Parent{}
Parent child1= new Parent(){}

자바에서 익명 클래스를 만드는 이유는 재활용할 필요가 없는 하나의 인스턴스만을 익명으로 생성하기 위함이라고 할 수 있다.

자바스크립트에서도 이는 동일하다.

const Parent = class{}
const instance = new (class extends Parent){}

하지만 코틀린에서는 익명클래스를 만드는 전용키워드인 object 가 존재한다.

abstract class Parent
class ClassTest2{
	val child1 = object:Parent(){}
}

object를 선언하고, 상속과 마찬가지로 상속받을 부모를 : 뒤에 명시하고 구현체를 명시한다. 마찬가지로 new 키워드는 필요없다.

object를 선언하여 클래스 구문처럼 이름을 명시할 수도 있다.

object Child1:Parent(){}

이렇게 만든 클래스는 패키지 전역에서 사용할 수 있는 전역변수가 된다. 이 전역 object 의 정체는 무엇인지, 클래스랑은 뭐가 다른지는 천천히 살펴보고, 우선 싱글톤 객체를 하나 만들어보도록 하자. 앞서 배웠던 by lazy 로 lazy하게 할당해볼 것이다.

class SingleTon{
	companion object{
		val INSTANCE by lazy{SingleTon()}
	}
}

이렇게 만들어진 SingleTon.INSTANCE 는 호출이 됐을 때 만들어지는 하나의 전역 싱글톤이 될 것이다. 앞서 알아본 익명 클래스 선언으로 만들어진 전역 인스턴스와 완전히 동일하다.

class Child1:Parent(){
	companion object{
		val INSTANCE by lazy{Child1()}
	}
}
object Child2:Parent(){}

Child1.INSTANCEChild2두 객체 모두 전역 싱글톤 객체를 컴파일타임에 확정하고, 이를 실제로 호출할 때에만 lazy하게 생성한다. 즉 기명 object 는 생성되지 않은 싱글톤 객체가 된다.심지어 쓰레드 안전성까지 확보해준다. 주의할 점은,기명 object 객체는 컴파일 타임에는 존재 하지만 런타임에는 호출전에는 존재하지 않기 때문에 호출하기 전에 참조할 수 없다는 점이다.

익명 object 에서 사용한 object expression 과 기명 object 선언을 object declaration 을 구분해야 한다. object expression 은 디폴트로 코틀린의 Any 를 상속하며 호출 즉시 인스턴스를 생성한다. 반면 object declaration 은 그 자체로 Thread-safe를 코틀린 컴파일러가 보장하는, 싱글톤을 선언 한다. 생성은 lazy하게 이루어진다.

Companion Object 는 이러한 object declaration 을 클래스 내부에서 선언한 것이다. 즉, 일종의 중첩 클래스로서 외부 클래스 네임스페이스를 그대로 사용해서 참조할 수 있는 싱글톤을 만든 것이다. 따라서 클래스 내부에 Companion Object 는 이름을 별도로 부여하더라도 반드시 단 1개 만 사용 가능하다. 따라서 상속받았을 때도 Companion Object에 있는 속성이나 변수명이 같다면 이를 shadowing 한다.

따라서 object는 싱글톤 을 위한 클래스 선언이라고 이해하면 쉽다. 익명 object는 싱글톤이 될 수 없고 인스턴스를 바로 할당해주는 역할을 한다.

이처럼 object는 굉장히 편리하지만 사용하지 않으면 참조할 수 없다는 점에서 참조할 때 순서나 타이밍을 주의해야 한다.

Sealed Class & enum

코틀린에서 enum은 아예 class라는 키워드와 같이 생성된다. enum은 컴파일러가 static 초기화 타이밍에 인스턴스를 같이 생성한다. 따라서 다른 인스턴스가 만들어지거나 클래스가 준비되는 시점보다도 전에 생성되기 때문에, 컴파일 타임이 늦어져 안드로이드에서 앱의 부팅 속도를 느리게 만드는 주범으로 지목되곤 한다. 특히, 한번 초기화 하면 계속해서 점유하는 JVM과는 달리 OS가 앱을 계속해서 종료하고 로드하는 것을 반복하는 안드로이드에서는 enum 에 따른 컴파일 속도의 저하가 두드러진다. 다만 인스턴스를 생성하지 않는 자바스크립트로 컴파일할 때는 아무 상관이 없다.

enum은 항상 초기화가 확정되어 있기 때문에 어느 시점에서도 안심하고 사용할 수 있다는 장점이 있다

enum class Color(val code:String){
	Red("#f00"), Blue("00f"),Green("#0f0")
}

코틀린에서 , 를 쓰는 경우는 enum의 구분기호와 함수의 인자 구분 뿐이므로 알아두는 것이 좋다. enum에서 생성하고 싶은 인스턴스를 , 로 구분한다. 생성할 instance 들과 enum class 자체의 속성과 메서드 사이는 ; 세미콜론으로 구분한다. enum 클래스에서 만든 인스턴스는 전부 싱글톤 컨텍스트로써 컴파일 시점에 확정되기 때문에 더 이상 인스턴스를 만들거나 할 수 없다. enum class 끼리는 서로 상속이 가능하다.

앞서 살펴본 object 키워드로 비슷한 역할을 하는 추상 클래스를 만들 수 있다.

abstract class Color(val code:String){
	object Red:Color("#f00")
	object Blue:Color("#0f0")
	object Green:Color("#00f")
}

이렇게 만들면 어떤 점이 다를까? 우선 lazy하게 생성되기 때문에 호출하기 전까지는 초기화되지 않는다. 또한, object 는 선언만 하고 할당하지 않은 구문으로 companion object 객체 속성이 된다. static 컨텍스트가 companion object 에 들어갈 필요가 없는 코틀린 상의 예외라고 할 수 있다. 또한, 클래스 내부에 클래스 선언을 하는 일종의 중첩 클래스가 된다. 코틀린에서는 자바와는 달리 클래스 내부에 선언한 클래스를 중첩 클래스로 취급하며 Inner 클래스 를 금지한다. 즉 모든 클래스는 독립이다.

또한, 클래스 내에 선언한 object 외에도 또다른 object를 얼마든지 확장할 수 있다는 점도 다르다. 인스턴스를 제한할 수 있는 enum 에 반해 내부에 object 선언으로 생성하는 이러한 방식은 익명 클래스를 얼마든지 외부에서도 선언할 수 있다. 그렇다면 이 문제는 어떻게 해결할 수 있을까?

sealed 키워드를 사용하는 방법을 사용할 수 있다. sealed 클래스는 내부의 중첩 클래스들을 제외한 외부 클래스는 상속을 불가능하게 막는다. 따라서, sealed 클래스를 사용하면 컴파일 타임에 싱글톤 객체를 확정하면서도 더 이상의 인스턴스를 추가하는 것을 막을 수 있으며, lazy하게 초기화되기 때문에 enum class가 가지는 컴파일 타임 지연 문제에서 자유롭다. 따라서 코틀린에서는 enum 대신 sealed 를 사용하고 내부에 싱글톤을 선언하는 것을 권장하고 있다.

또한 sealed 클래스는 리플레션을 했을 때 자식 클래스의 리스트를 얻을 수 있다는 점도 기억하자. 이는 sealed 클래스의 자식 클래스들이 컴파일 시점에 확정되기 때문이다.

여기까지 쉬지 않고 달려왔다. 이제 클래스에 대해 배운 점들을 토대로 HTML builder 를 만들어보자.

HTML Builder

abstract class El(val tagName: String) {
    protected val el = when (tagName) {
        "body" -> document.body ?: throw Throwable("no body")
        else -> document.createElement(tagName) as HTMLElement
    }
		var html: String
        get() = el.innerHTML
        set(value) {
            el.innerHTML = value
        }
		operator fun get(key: String) = el.getAttribute(key) ?: ""
    operator fun set(key: String, value: String) = el.setAttribute(key, "$value")
    operator fun invoke() = el
}

다루지는 않았지만 when 또한 다른 제어문과 마찬가지로 이며, 블록의 결과값을 반환한다. document.body는 nullable이므로 엘비스 연산자로 throw 한다. Nothing은 타입이 아니므로 null 타입인 경우에는 아예 작동을 하지 않게 되므로, document.body가 null이 아닌 것으로 타입 추론이 되고 when 이 반환하는 타입이 HTMLElement로 동일하여 el의 타입이 스마트 캐스팅 된다.

물론 document.createElement로 body를 마찬가지로 만들 수도 있겠지만 여기서는 실제 document에 생성된 body만을 이용하도록 하기 위해 예외처리를 했다.

속성 할당자인 el의 값이 기본 생성자에서 할당한 tagName값에 따라 결정되고 있는데 이는 코틀린의 기본 생성자가 자바처럼 속성 할당자보다 먼저 실행된다는 것을 보여준다. el의 innerHTML에 대한 getter와 setter를 가지는 html속성 또한 정의해준다.

연산자 오버로딩을 통해 El 의 getter와 setter를 w3c html attribute 메서드로 매핑시켜주었고, 마지막으로 invoke 연산자를 호출하면 el 속성을 반환하도록 오버로딩 했다. invoke 연산자는 인스턴스를 () 를 붙여 호출했을 시 발동되는 호출 연산자 이다. 즉, val tag = El("div"), tag() 와 같은 형태로 사용할 수 있다.

내친김에 사용할 연산자를 조금 더 확장해보자.

	operator fun plusAssign(child: El) {
        el.appendChild(child())
    }

  operator fun minusAssign(child: El) {
      el.removeChild(child())
  }

  val style: CSSStyleDeclaration get() = el.style

plusAssign과 minusAssign은 각각 +=-= 에 대응된다. El 객체는 이를 각각 appendChild와 removeChild로 맵핑해주었다. child() 는 앞서 오버로딩한 invoke 연산자에 따라 child.el 과 같다.

그럼 El 클래스를 상속한 객체들을 만들어보자.

object Body : El("body")
class Div : El("body")
class Canvas : El("canvas") {
    val context: CanvasRenderingContext2D? get() = 
(el as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
}

Body는 document 전체에 단 하나이므로 싱글턴으로 선언할 수 있다. Div는 재활용을 위해 class로 만들어 생성한다.

Canvas 또한 마찬가지 이지만, 다른 El에 없는 속성인 context를 가지는데 context getter를 살펴보면 부모의 속성은 el 을 형변환을 해주면서 as? 라는 문법을 사용하고 있다. as? 는 코틀린의 형변환 연산자로써 형변환이 실패할 시 null을 변환해주는 마찬가지로 null safe 한 연산자이다. 형변환의 결과를 모두 런타임에 알게 되므로 as? 를 사용해 런타임에 null이 나오더라도 안전하도록 보장해주는 것이다. 따라서 context의 반환타입인 CanvasRenderingContext2D 도 마찬가지로 nullable 이 된다. null-safe한 as? 연산자가 없다면 try-catch 를 두번 사용했어야 할 것이다.

fun htmlBuilder() {
    (0..5).map { Div().apply { html = "div-$it" } }.forEach { Body += it }
    Body += Canvas().apply {
        this["width"] = 500
        this["height"] = 500
        context?.run {
            lineWidth = 10.0
            strokeRect(75.0, 140.0, 150.0, 110.0)
            fillRect(130.0, 190.0, 40.0, 60.0)
            moveTo(50.0, 140.0)
            lineTo(150.0, 60.0)
            lineTo(250.0, 140.0)
            closePath()
            stroke()
        }
    }
}

(0..5).. 를 사이에 넣어 range를 만들 수 있는 코틀린의 문법이며, range는 list 객체이다. 따라서 map 을 사용할 수 있다. Div 인스턴스를 만들어서 apply 를 사용했으니, 리턴값은 당연히 Div 객체가 될 것이며 따라서 map 으로 반환하는 리스트노 Div 객체 리스트이다. 그 Div 리스트를 forEach 를 돌면서 Body에 appendChild 한다.

그 뒤 Body에 Canvas 객체를 하나 appendChild 하고, 높이와 넓이를 부여한 뒤, CanvasRendering2Dcontext 의 속성들을 설정해준다.

profile
inudevlog.com으로 이전해용

0개의 댓글