Kotlin에서 JavaScript 코드 사용하기
Kotlin에서 JavaScript 코드 사용하기
Kotlin은 원래 Java 플랫폼과의 쉬운 상호운용을 위해 설계되었어요. Kotlin은 Java 클래스를 Kotlin 클래스로 보고, Java는 Kotlin 클래스를 Java 클래스로 봅니다.
하지만 JavaScript는 동적 타입 언어라서 컴파일 타임에 타입을 검사하지 않아요. Kotlin에서 동적 타입(dynamic type)을 통해 JavaScript와 자유롭게 대화할 수 있습니다. Kotlin 타입 시스템의 모든 힘을 활용하고 싶다면, Kotlin 컴파일러와 주변 도구가 이해할 수 있는 JavaScript 라이브러리용 외부 선언(external declaration)을 만들면 돼요.
본문
인라인 JavaScript
js() 함수를 사용해 Kotlin 코드 안에 JavaScript 코드를 인라인할 수 있어요:
fun jsTypeOf(o: Any): String {
return js("typeof o")
}
JavaScript 코드 인라인은 ES2015 기능을 완전히 지원합니다:
const및let변수 선언- ES 클래스
- 제너레이터(Generator)
- 람다(화살표 함수)
- 스프레드(Spread) 및 레스트(Rest) 연산자
- 템플릿 문자열
js의 파라미터는 컴파일 타임에 파싱되어 JavaScript 코드로 "있는 그대로" 번역되므로, 문자열 상수여야 합니다. 따라서 다음 코드는 올바르지 않아요:
fun jsTypeOf(o: Any): String {
return js(getTypeof() + " o") // 오류: 인자는 문자열 상수여야 합니다
// 컴파일러는 문자열 연결을 평가할 수 없습니다
}
fun getTypeof() = "typeof"
대신, 예를 들어 레스트 연산자를 인라인하려면 문자열 상수를 사용하세요:
fun runSumExample() {
val sum = js("(...nums) => nums.reduce((a, b) => a + b, 0)")
println(sum(1, 2, 3, 4))
}
js()를 호출하면 동적 타입의 결과가 반환되며, 이는 컴파일 타임에 타입 안전성을 제공하지 않아요.
external 수정자
특정 선언이 순수 JavaScript로 작성되었음을 Kotlin에 알리려면 external 수정자로 표시해야 해요. 컴파일러는 이런 선언을 보면 해당 클래스·함수·프로퍼티의 구현이 외부(개발자 또는 npm 의존성)에서 제공된다고 가정하므로, 그 선언에서 JavaScript 코드를 생성하려고 시도하지 않습니다. 그래서 external 선언은 본문을 가질 수 없어요. 예를 들면:
external fun alert(message: Any?): Unit
external class Node {
val firstChild: Node
fun append(child: Node): Node
fun removeChild(child: Node): Node
// etc
}
external val window: Window
external 수정자는 중첩된 선언에도 상속된다는 점을 기억하세요. 그래서 위 예시의 Node 클래스에는 멤버 함수와 프로퍼티 앞에 external 수정자가 없어요.
external 수정자는 패키지 수준 선언에만 허용됩니다. non-external 클래스의 external 멤버는 선언할 수 없어요.
클래스의 (정적) 멤버 선언하기
JavaScript에서는 프로토타입이나 클래스 자체에 멤버를 정의할 수 있어요:
function MyClass() { ... }
MyClass.sharedMember = function() { /* implementation */ };
MyClass.prototype.ownMember = function() { /* implementation */ };
Kotlin에는 그런 문법이 없어요. 하지만 Kotlin에는 동반 객체(companion object)가 있습니다. Kotlin은 external 클래스의 동반 객체를 특별한 방식으로 취급해요. 객체를 기대하는 대신, 동반 객체의 멤버가 클래스 자체의 멤버라고 가정합니다. 위 예시의 MyClass는 다음과 같이 설명할 수 있어요:
external class MyClass {
companion object {
fun sharedMember()
}
fun ownMember()
}
기본값을 가진 파라미터 선언하기
기본값이 있는 파라미터를 가진 JavaScript 함수에 대한 external 선언을 작성한다면 definedExternally를 사용하세요. 이는 기본값 생성을 JavaScript 함수 자체에 위임합니다:
external fun myFunWithOptionalArgs(
x: Int,
y: String = definedExternally,
z: String = definedExternally
)
이 external 선언으로 myFunWithOptionalArgs를 하나의 필수 인자와 두 개의 선택 인자로 호출할 수 있으며, 기본값은 myFunWithOptionalArgs의 JavaScript 구현이 계산해요.
JavaScript 클래스 확장하기
JavaScript 클래스를 Kotlin 클래스인 것처럼 쉽게 확장할 수 있어요. external open 클래스를 정의하고 non-external 클래스로 확장하기만 하면 됩니다. 예를 들면:
open external class Foo {
open fun run()
fun stop()
}
class Bar : Foo() {
override fun run() {
window.alert("Running!")
}
fun restart() {
window.alert("Restarting")
}
}
몇 가지 제한이 있어요:
- external 기본 클래스의 함수가 시그니처로 오버로드되면, 파생 클래스에서 오버라이드할 수 없습니다.
- 기본값이 있는 파라미터를 포함하는 함수는 오버라이드할 수 없어요.
- non-external 클래스는 external 클래스로 확장될 수 없습니다.
external 인터페이스
JavaScript에는 인터페이스라는 개념이 없어요. 함수가 파라미터가 foo와 bar 두 메서드를 지원하기를 기대한다면, 실제로 이 메서드들이 있는 객체를 그냥 전달하면 됩니다.
정적으로 타입이 지정된 Kotlin에서 이 개념을 표현하기 위해 인터페이스를 사용할 수 있어요:
external interface HasFooAndBar {
fun foo()
fun bar()
}
external fun myFunction(p: HasFooAndBar)
external 인터페이스의 일반적인 사용 사례는 설정(settings) 객체를 기술하는 것이에요. 예를 들면:
external interface JQueryAjaxSettings {
var async: Boolean
var cache: Boolean
var complete: (JQueryXHR, String) -> Unit
// etc
}
fun JQueryAjaxSettings(): JQueryAjaxSettings = js("{}")
external class JQuery {
companion object {
fun get(settings: JQueryAjaxSettings): JQueryXHR
}
}
fun sendQuery() {
JQuery.get(JQueryAjaxSettings().apply {
complete = { (xhr, data) ->
window.alert("Request complete")
}
})
}
external 인터페이스에는 몇 가지 제한이 있어요:
is검사의 오른쪽에 사용할 수 없습니다.- reified 타입 인자로 전달할 수 없어요.
- 클래스 리터럴 표현식(예:
I::class)에 사용할 수 없습니다. - external 인터페이스로의
as캐스트는 항상 성공합니다. external 인터페이스로의 캐스팅은 "Unchecked cast to external interface" 컴파일 타임 경고를 생성해요. 이 경고는@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")애너테이션으로 억제할 수 있습니다.
IntelliJ IDEA는 @Suppress 애너테이션을 자동으로 생성해 줄 수도 있어요. 전구 아이콘이나 Alt-Enter로 의도 목록(intentions menu)을 열고, "Unchecked cast to external interface" 검사 옆의 작은 화살표를 클릭하세요. 여기서 억제 범위를 선택할 수 있고, IDE가 파일에 그에 맞는 애너테이션을 추가해 줍니다.
캐스트(Casts)
캐스트가 불가능할 때 ClassCastException을 던지는 "안전하지 않은" 캐스트 연산자 as 외에도, Kotlin/JS는 unsafeCast<T>()도 제공해요. unsafeCast를 사용하면 런타임에 타입 검사가 전혀 이루어지지 않습니다. 예를 들어 다음 두 메서드를 살펴보아요:
fun usingUnsafeCast(s: Any) = s.unsafeCast()
fun usingAsOperator(s: Any) = s as String
이들은 다음과 같이 컴파일됩니다:
function usingUnsafeCast(s) {
return s;
}
function usingAsOperator(s) {
var tmp$;
return typeof (tmp$ = s) === 'string' ? tmp$ : throwCCE();
}
동등성(Equality)
Kotlin/JS는 다른 플랫폼과 비교해 동등성 검사에 특별한 의미론을 가져요.
Kotlin/JS에서 Kotlin 참조 동등성 연산자(===)는 항상 JavaScript의 엄격한 동등성 연산자(===)로 번역됩니다.
JavaScript의 === 연산자는 두 값이 같은지뿐만 아니라 두 값의 타입도 같은지 검사해요:
fun main() {
val name = "kotlin"
val value1 = name.substring(0, 1)
val value2 = name.substring(0, 1)
println(value1 === value2)
// Kotlin/JS에서는 'true'를 출력
// 다른 플랫폼에서는 'false'를 출력
}
또한 Kotlin/JS에서 Byte, Short, Int, Float, Double 숫자 타입은 모두 런타임에 JavaScript의 Number 타입으로 표현됩니다. 따라서 이 다섯 타입의 값은 서로 구분할 수 없어요:
fun main() {
println(1.0 as Any === 1 as Any)
// Kotlin/JS에서는 'true'를 출력
// 다른 플랫폼에서는 'false'를 출력
}
Kotlin의 동등성에 대한 자세한 내용은 동등성 문서를 확인하세요.