JavaScript에서 Kotlin 코드 사용하기
JavaScript에서 Kotlin 코드 사용하기
선택한 JavaScript Module 시스템에 따라 Kotlin/JS 컴파일러는 다른 출력을 생성해요. 하지만 일반적으로 Kotlin 컴파일러는 일반적인 JavaScript 클래스, 함수, 프로퍼티를 생성하며, 이를 JavaScript 코드에서 자유롭게 사용할 수 있어요. 다만 기억해야 할 미묘한 점이 몇 가지 있어요.
plain 모드에서 선언을 별도의 JavaScript 객체로 격리하기
모듈 종류를 명시적으로 plain으로 설정했다면, Kotlin은 현재 모듈의 모든 Kotlin 선언을 담은 객체를 만들어요. 이는 전역 객체를 손상시키지 않기 위함이에요. 즉, myModule 모듈의 경우 모든 선언이 myModule 객체를 통해 JavaScript에서 사용할 수 있어요. 예를 들어:
fun foo() = "Hello"
이 함수는 JavaScript에서 이렇게 호출할 수 있어요.
alert(myModule.foo());
이렇게 함수를 직접 호출하는 것은 Kotlin 모듈을 UMD(browser와 nodejs 타깃 모두의 기본 설정), ESM, CommonJS, AMD 같은 JavaScript 모듈로 컴파일할 때는 적용되지 않아요. 이런 경우 선언은 선택한 JavaScript 모듈 시스템에 따라 노출돼요. 예를 들어 UMD, ESM, CommonJS를 사용할 때 호출 위치는 이렇게 보여요.
alert(require('myModule').foo());
JavaScript 모듈 시스템에 대한 자세한 내용은 JavaScript Modules을 참고하세요.
패키지 구조
대부분의 모듈 시스템(CommonJS, Plain, UMD)에서 Kotlin은 패키지 구조를 JavaScript에 노출해요. 선언을 루트 패키지에 정의하지 않았다면 JavaScript에서 정규화된 이름(fully qualified names)을 사용해야 해요. 예를 들어:
package my.qualified.packagename
fun foo() = "Hello"
예를 들어 UMD나 CommonJS를 사용할 때 호출 위치는 이렇게 보일 수 있어요.
alert(require('myModule').my.qualified.packagename.foo())
모듈 시스템 설정으로 plain을 사용할 때 호출 위치는 다음과 같아요.
alert(myModule.my.qualified.packagename.foo());
ECMAScript Modules(ESM)를 타깃으로 할 때는 애플리케이션 번들 크기를 개선하고 ESM 패키지의 일반적인 레이아웃에 맞추기 위해 패키지 정보가 유지되지 않아요. 이 경우 ES 모듈로 Kotlin 선언을 사용하는 방식은 이렇게 보여요.
import { foo } from 'myModule';
alert(foo());
@JsName 어노테이션
어떤 경우에는(예: 오버로드 지원) Kotlin 컴파일러가 JavaScript 코드에서 생성된 함수와 속성의 이름을 맹글링(mangle)해요. 생성된 이름을 제어하려면 @JsName 어노테이션을 사용할 수 있어요.
// Module 'kjs'
class Person(val name: String) {
fun hello() {
println("Hello $name!")
}
@JsName("helloWithGreeting")
fun hello(greeting: String) {
println("$greeting $name!")
}
}
이제 이 클래스를 JavaScript에서 다음과 같이 사용할 수 있어요.
// If necessary, import 'kjs' according to chosen module system
var person = new kjs.Person("Dmitry"); // refers to module 'kjs'
person.hello(); // prints "Hello Dmitry!"
person.helloWithGreeting("Servus"); // prints "Servus Dmitry!"
@JsName 어노테이션을 지정하지 않았다면 해당 함수의 이름은 함수 시그니처에서 계산된 접미사를 포함하게 될 거예요, 예를 들어 hello_61zpoe$처럼요.
Kotlin 컴파일러가 맹글링을 적용하지 않는 경우도 몇 가지 있다는 점을 유의하세요.
external선언은 맹글링되지 않아요.external클래스에서 상속받는 non-external클래스 안의 override된 함수는 맹글링되지 않아요.
@JsName의 매개변수는 유효한 식별자인 상수 문자열 리터럴이어야 해요. 컴파일러는 식별자가 아닌 문자열을 @JsName에 전달하려는 모든 시도에 대해 오류를 보고해요. 다음 예제는 컴파일 타임 에러를 만들어 내요.
@JsName("new C()") // error here
external fun newC()
@JsExport 어노테이션
최상위 선언(클래스, 인터페이스, 함수 등)에 @JsExport 어노테이션을 적용하면 Kotlin 선언을 JavaScript 또는 TypeScript에서 사용할 수 있게 만들 수 있어요. 이 어노테이션은 Kotlin에서 주어진 이름을 가진 모든 중첩 선언을 내보내요.
예를 들어, 중첩 클래스와 이름 있는 companion object가 있는 Kotlin 인터페이스를 내보내는 방법은 다음과 같아요.
@JsExport
interface Identity {
class Metadata(val tag: String)
companion object Registry {
val defaultTag = "GUEST"
}
}
현재 @JsExport 어노테이션은 함수를 Kotlin에서 보이게 만드는 유일한 방법이에요.
@JsExport 어노테이션은 다음에서도 사용할 수 있어요.
- 멀티플랫폼 프로젝트의 공통 코드에서. JavaScript 타깃으로 컴파일할 때만 효과가 있으며, 플랫폼 특화적이지 않은 Kotlin 선언도 내보낼 수 있게 해 줘요.
@JsName어노테이션과 함께 사용해서 생성되고 내보내진 함수의 이름을 지정해요. 이는 export의 모호성(예: 같은 이름을 가진 함수의 오버로드)을 해결하는 데 도움이 돼요.@file:JsExport를 사용해서 파일 수준에서.
값 클래스(value classes) 내보내기
Kotlin의 인라인 값 클래스를 일반적인 TypeScript 클래스로 내보낼 수 있어요.
값 클래스를 내보내려면 Kotlin 쪽에서 @JsExport 어노테이션으로 표시해요.
// Kotlin
@JsExport
@JvmInline
value class Email(val address: String) {
init { require(address.contains("@")) { "Invalid email" } }
}
@JsExport
class AuthService {
suspend fun login(email: Email): String = ...
}
TypeScript 쪽에서는 일반 클래스처럼 보여요.
// TypeScript
import { AuthService, Email } from "..."
const auth = new AuthService();
console.log(await auth.login(new Email("[email protected]")));
// "Welcome, [email protected]!"
console.log(await auth.login(new Email("not-an-email")));
// "Invalid email"
일시 중단 람다(suspending lambdas) 내보내기
Kotlin의 일시 중단 람다 표현식을 JavaScript async 함수로 내보낼 수 있어요.
이 기능을 활성화하려면 build.gradle.kts 파일에 다음 컴파일러 옵션을 추가해요.
kotlin {
js {
compilations.all {
compileTaskProvider.configure {
compilerOptions {
freeCompilerArgs.add("-Xsuspend-lambda-exporting")
}
}
}
}
}
관련 Kotlin 선언을 @JsExport 어노테이션으로 표시해요.
// Kotlin
@JsExport
class TaskRunner {
suspend fun runTask(task: suspend () -> String): String {
return task()
}
}
TypeScript 쪽에서 suspend 람다는 일반 async 함수로 매핑돼요.
// TypeScript
import { TaskRunner } from "..."
const runner = new TaskRunner();
const result = await runner.runTask(async () => "done");
console.log(result); // "done"
@JsNoRuntime 어노테이션
@JsNoRuntime 어노테이션을 사용하면 Kotlin 인터페이스를 JavaScript/TypeScript로 내보낼 수 있어요. 이는 일반 TypeScript 인터페이스로 직접 매핑할 수 있게 해 줘요.
예를 들어 Kotlin Multiplatform 프로젝트에서 Kotlin 인터페이스를 내보내려면:
공통 코드에서 Kotlin 인터페이스를 @JsNoRuntime으로 어노테이션해요.
// commonMain
import kotlin.js.JsNoRuntime
@JsNoRuntime
expect interface DataProcessor {
fun process(data: String): Int
}
JS 특화 소스 코드에서 @JsNoRuntime으로 실제 구현을 제공해요.
// jsMain
import kotlin.js.JsNoRuntime
@JsNoRuntime
actual interface DataProcessor {
actual fun process(data: String): Int
}
TypeScript 쪽에서 이 인터페이스는 일반 TypeScript 인터페이스로 매핑돼요.
// Generated .d.ts
export interface DataProcessor {
process(data: string): number;
}
Kotlin Multiplatform 프로젝트의 일반 규칙은 다음과 같아요.
expect와actual인터페이스 선언 모두@JsNoRuntime으로 어노테이션되어야 해요. 유일한 예외는 어노테이션이 필요 없는actual쪽 플랫폼 특화 코드의external구현이에요.expect쪽 공통 코드에서external인터페이스 선언을 사용하는 것은 금지돼요. 대신@JsNoRuntime으로 어노테이션된 일반 인터페이스를 사용해요.
@JsNoRuntime으로 Kotlin 인터페이스를 내보내는 것에는 몇 가지 제약이 있어요. 이 어노테이션은 다음에서는 허용되지 않아요.
external인터페이스 — 이미 기본적으로@JsNoRuntime이 있는 것처럼 동작하므로. 추가하면 컴파일러 경고가 발생해요.is와as타입 검사.::class문법을 사용하는 클래스 참조.- 과련 타입 인자(reified type argument)로 전달되는 인터페이스.
@JsStatic
@JsStatic 어노테이션은 컴파일러가 타깃 선언에 대한 추가 정적 메서드를 생성하도록 지시해요. 이는 Kotlin 코드의 정적 멤버를 JavaScript에서 직접 사용하는 데 도움이 돼요.
@JsStatic 어노테이션은 이름 있는 객체에 정의된 함수와, 클래스와 인터페이스 안에 선언된 companion object의 함수에 적용할 수 있어요. 이 어노테이션을 사용하면 컴파일러가 객체의 정적 메서드와 객체 자체의 인스턴스 메서드 둘 다를 생성해요. 예를 들어:
// Kotlin
class C {
companion object {
@JsStatic
fun callStatic() {}
fun callNonStatic() {}
}
}
이제 callStatic() 함수는 JavaScript에서 static이고 callNonStatic() 함수는 그렇지 않아요.
// JavaScript
C.callStatic(); // Works, accessing the static function
C.callNonStatic(); // Error, not a static function in the generated JavaScript
C.Companion.callStatic(); // Instance method remains
C.Companion.callNonStatic(); // The only way it works
@JsStatic 어노테이션을 객체나 companion object의 프로퍼티에 적용해서 그 getter와 setter 메서드를 해당 객체나 companion object를 포함하는 클래스의 static 멤버로 만들 수도 있어요.
이 기능은 Experimental 상태예요. 이슈 트래커 YouTrack에 피드백을 남겨 주세요.
BigInt 타입으로 Kotlin Long 타입 표현하기
Kotlin/JS는 현대적인 JavaScript(ES2020)로 컴파일할 때 Kotlin Long 값을 표현하기 위해 JavaScript 내장 BigInt 타입을 사용해요.
BigInt 타입 지원을 활성화하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가해야 해요.
// build.gradle.kts
kotlin {
js {
...
compilerOptions {
freeCompilerArgs.add("-Xes-long-as-bigint")
}
}
}
이 기능은 Experimental 상태예요. 이슈 트래커 YouTrack에 피드백을 남겨 주세요.
내보낸 선언에서 Long 사용하기
Kotlin의 Long 타입이 JavaScript의 BigInt 타입으로 컴파일될 수 있으므로, Kotlin/JS는 Long 값을 JavaScript로 내보내는 것을 지원해요.
이 기능을 활성화하려면:
- Kotlin/JS에서
Long내보내기를 허용해요.build.gradle(.kts)파일의freeCompilerArgs속성에 다음 컴파일러 옵션을 추가해요.
// build.gradle.kts
kotlin {
js {
...
compilerOptions {
freeCompilerArgs.add("-XXLanguage:+JsAllowLongInExportedDeclarations")
}
}
}
BigInt타입을 활성화해요. 활성화 방법은 UseBigInttype to represent Kotlin'sLongtype에서 확인하세요.
BigInt64Array 타입으로 Kotlin LongArray 타입 표현하기
Kotlin/JS는 JavaScript로 컴파일할 때 Kotlin의 LongArray 값을 표현하기 위해 JavaScript 내장 BigInt64Array 타입을 사용할 수 있어요.
BigInt64Array 타입 지원을 활성화하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가해요.
// build.gradle.kts
kotlin {
js {
...
compilerOptions {
freeCompilerArgs.add("-Xes-long-as-bigint")
}
}
}
이 기능은 Experimental 상태예요. 이슈 트래커 YouTrack에 피드백을 남겨 주세요.
JavaScript에서의 Kotlin 타입
Kotlin 타입이 JavaScript 타입으로 어떻게 매핑되는지 확인해 보세요.
| Kotlin | JavaScript | 설명 |
|---|---|---|
Byte, Short, Int, Float, Double |
Number |
|
Char |
Number |
숫자가 문자 코드를 나타내요. |
Long |
BigInt |
-Xes-long-as-bigint 컴파일러 옵션이 구성되어 있어야 해요. |
Boolean |
Boolean |
|
String |
String |
|
Array |
Array |
|
ByteArray |
Int8Array |
|
ShortArray |
Int16Array |
|
IntArray |
Int32Array |
|
CharArray |
UInt16Array |
$type$ == "CharArray" 프로퍼티를 가져요. |
FloatArray |
Float32Array |
|
DoubleArray |
Float64Array |
|
LongArray |
BigInt64Array |
|
BooleanArray |
Int8Array |
$type$ == "BooleanArray" 프로퍼티를 가져요. |
List, MutableList |
KtList, KtMutableList |
KtList.asJsReadonlyArrayView 또는 KtMutableList.asJsArrayView를 통해 Array를 노출해요. |
Map, MutableMap |
KtMap, KtMutableMap |
KtMap.asJsReadonlyMapView 또는 KtMutableMap.asJsMapView를 통해 ES2015 Map을 노출해요. |
Set, MutableSet |
KtSet, KtMutableSet |
KtSet.asJsReadonlySetView 또는 KtMutableSet.asJsSetView를 통해 ES2015 Set을 노출해요. |
Unit |
Undefined | 반환 타입으로 쓸 때는 내보낼 수 있지만, 매개변수 타입으로 쓸 때는 내보낼 수 없어요. |
Any |
Object |
|
Throwable |
Error |
|
enum class Type |
Type |
열거형 항목은 정적 클래스 프로퍼티로 노출돼요(Type.ENTRY). |
Nullable Type? |
Type | null | undefined |
|
@JsExport로 표시된 것을 제외한 다른 모든 Kotlin 타입 |
지원되지 않음 | Kotlin의 부호 없는 정수 타입을 포함해요. |
추가로 알아두어야 할 중요한 점이 있어요.
- Kotlin은
kotlin.Int,kotlin.Byte,kotlin.Short,kotlin.Char,kotlin.Long에 대한 오버플로 의미론을 보존해요. - Kotlin은 런타임에 숫자 타입을 구분할 수 없으므로(
kotlin.Long제외), 다음 코드가 동작해요.
fun f() {
val x: Int = 23
val y: Any = x
println(y as Float)
}
- Kotlin은 JavaScript에서 지연(lazy) 객체 초기화를 보존해요.