Kotlin 2.4.20의 새로운 기능

Kotlin 2.4.20의 새로운 기능

Kotlin 2.4.20은 표준 라이브러리의 코루틴 스택 추적 복구 지원과 컬렉션·테스트 단언 함수의 새 기능, Kotlin/Native의 새 Swift export 기능과 SwiftPM Package.swift 자동 생성, Kotlin/Wasm·Kotlin/JS의 여러 개선, Gradle 9.7.0 지원, Build tools API의 새 대상 지원 등을 담은 릴리스예요.

출처: What's new in Kotlin 2.4.20

본문

출시일: 2026년 9월 7일

Kotlin 2.4.20이 나왔어요! 이번 릴리스의 하이라이트는 다음과 같아요:

이 업데이트에 대한 개요는 이 영상에서도 확인할 수 있어요.

Kotlin의 릴리스 주기에 대한 자세한 내용은 Kotlin release process를 참고하세요.

Kotlin 2.4.20으로 업데이트하기

최신 버전의 Kotlin은 최신 버전의 IntelliJ IDEAAndroid Studio에 포함되어 있어요.

새 Kotlin 버전으로 업데이트하려면 IDE가 최신 버전인지 확인하고 빌드 스크립트에서 Kotlin 버전을 2.4.20으로 변경하면 돼요.

새로운 기능

Kotlin 2.2.20은 JVM 21 이상에서 invokedynamic으로 when 식을 컴파일하는 실험적 지원을 도입했어요.

Kotlin 2.4.20에서 이 기능은 이제 Stable로 승격되어 기본적으로 활성화됐어요.

자세한 내용은 문서를 참고하세요.

새로운 기능

이번 릴리스에서는 Beta, Alpha, Experimental 상태를 포함한 다음과 같은 pre-stable 기능을 사용할 수 있어요:

표준 라이브러리

Kotlin 2.4.20은 코루틴 스택 추적 복구를 지원하고, 컬렉션 요소의 동등성·유일성을 확인하는 새 함수와 kotlin.test 단언 함수의 새 오버로드를 도입했어요.

코루틴 스택 추적 복구 지원

Kotlin 2.4.20은 표준 라이브러리에 StackTraceRecoverable 인터페이스를 추가했어요. 이 인터페이스 덕분에 kotlinx.coroutines에 의존성을 추가하지 않고도 스택 추적 복구를 위한 새 예외 인스턴스를 만드는 방법을 직접 정의할 수 있어, kotlinx.coroutines 라이브러리와의 통합이 개선돼요.

스택 추적 복구는 한 코루틴이 예외를 던지고 다른 코루틴이 그 예외를 다시 던질 때 디버깅에 도움을 줘요. 예외가 어디서 발생했는지, 다른 코루틴이 어디서 다시 던지는지 확인할 수 있게 해주거든요.

kotlinx.coroutines 라이브러리는 추가 코루틴 스택 추적 정보가 담긴 새 예외 인스턴스를 만들어 스택 추적 복구를 수행해요. 이는 예외 메시지·cause만 받거나 둘 다 받거나 아무 인자도 받지 않는 생성자를 가진 예외에 자동으로 적용돼요.

예외 생성자에 줄 번호나 오류 코드 같은 추가 필수 인자가 있다면 StackTraceRecoverable 인터페이스를 구현해서 kotlinx.coroutines 라이브러리가 해당 예외의 새 인스턴스를 만드는 방식을 정의할 수 있어요.

인터페이스를 구현하려면 copyForStackTraceRecovery() 함수를 override하면 돼요. override에서 스택 추적 복구용 새 예외 인스턴스를 반환하거나, kotlinx.coroutines 라이브러리가 예외를 복사하길 원하지 않으면 null을 반환하세요.

StackTraceRecoverable 인터페이스는 모든 대상에서 사용할 수 있지만, kotlinx.coroutines 라이브러리는 JVM에서만 스택 추적 복구에 이 인터페이스를 사용해요.

이 API는 Experimental 상태이며 @OptIn(ExperimentalStdlibCoroutineSupportApi::class) 애노테이션으로 opt-in이 필요해요.

스택 추적 복구를 위해 새 인스턴스를 만들 때 line 속성을 보존하는 커스텀 예외의 예시를 볼게요:

import kotlin.coroutines.ExperimentalStdlibCoroutineSupportApi
import kotlin.coroutines.debug.StackTraceRecoverable

@OptIn(ExperimentalStdlibCoroutineSupportApi::class)
class FileEditException
// 구현에는 private 생성자가 필요해요
// cause를 IllegalStateException 생성자로 전달하기 위해서요
private constructor(
    val line: Int,
    private val detail: String,
    cause: Throwable?,
) : IllegalStateException("When editing line $line: $detail", cause),
    // 스택 추적 복구를 위해 StackTraceRecoverable을 구현해요
    StackTraceRecoverable<FileEditException> {

    constructor(line: Int, detail: String) : this(line, detail, null)

    // 줄 번호와 메시지 세부 정보를 복사해요
    override fun copyForStackTraceRecovery(): FileEditException =
        FileEditException(line, detail, this)
}

fun main() {
    val original = FileEditException(15, "Unexpected token")

    // 보통은 이 함수를 직접 호출할 필요가 없어요. 동작을 테스트할 때만 호출하면 되죠
    // kotlinx.coroutines 라이브러리는 스택 추적 복구 중에 이 함수를 자동으로 호출해요
    val copy = original.copyForStackTraceRecovery()

    println(copy.message)
    // When editing line 15: Unexpected token

    println(copy.cause == original)
    // true
}

자세한 내용은 이 기능의 KEEP을 참고하세요.

YouTrack에서 피드백을 주시면 감사하겠어요.

컬렉션 요소의 동등성·유일성을 확인하는 새 함수

Kotlin 2.4.20 이전에는 컬렉션 요소가 모두 서로 다르거나(유일) 모두 같은지(동등) 확인하려면 비효율적인 코드 패턴을 써야 했어요.

Kotlin 2.4.20은 이 공백을 메우는 실험적 함수를 도입했어요:

함수 확인 내용
allDistinct() 컬렉션의 모든 값이 유일한지 확인해요.
allDistinctBy() 모든 객체가 선택한 속성에 대해 유일한 값을 갖는지 확인해요.
allEqual() 컬렉션의 모든 값이 같은지 확인해요.
allEqualBy() 모든 객체가 선택한 속성에 대해 같은 값을 갖는지 확인해요.

이 함수들은 컬렉션, 시퀀스, 배열에 사용할 수 있어요. 다른 컬렉션 연산처럼 구조적 동등성으로 요소를 비교해요.

이 함수들은 Experimental 상태이며 @OptIn(ExperimentalStdlibApi::class) 애노테이션이나 -opt-in=kotlin.ExperimentalStdlibApi 컴파일러 옵션으로 opt-in이 필요해요:

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    data class Response(
        val participantId: String,
        val answer: String,
        val responseDate: String
    )

    val responses = listOf(
        Response("P001", "Yes", "2026-07-21"),
        Response("P002", "Maybe", "2026-07-21"),
        Response("P003", "No", "2026-07-21")
    )

    // 모든 참가자가 같은 답변을 했는지 확인해요
    println(responses.allEqualBy { it.answer })
    // false

    // 중복 참가자가 있는지 확인해요
    println(responses.allDistinctBy { it.participantId })
    // true

    // 모든 응답이 같은 날짜에 제출됐는지 확인해요
    println(responses.allEqualBy { it.responseDate })
    // true

    val answers = responses.map { it.answer }

    // 답변이 모두 같은지 확인해요
    println(answers.allEqual())
    // false

    // 답변이 모두 다른지 확인해요
    println(answers.allDistinct())
    // true
}

KEEP에서 피드백을 주시면 감사하겠어요.

kotlin.test 단언 함수의 새 오버로드

Kotlin 2.4.20은 kotlin.test 단언 함수에 새 오버로드를 추가했어요. 이 오버로드는 단언이 실패할 때만 지연 생성되는 오류 메시지를 만드는 람다를 받아요.

이전에는 assertTrue()assertEquals() 같은 kotlin.test 단언 함수는 단언이 성공했을 때도 매번 사전 형식화된 오류 메시지를 만들었고, 실제로는 그 메시지가 전혀 사용되지 않았어요.

새 오버로드는 kotlin.test API를 JUnit 5와 정렬하고, 일반 문자열 대신 람다를 통한 메시지 공급자(message supplier)를 받아요. 이는 특히 단언에 대한 상세 오류 메시지를 생성하는 Power-assert 컴파일러 플러그인에서 성능을 개선해줘요.

새 오버로드는 다음 단언 함수에서 사용할 수 있어요:

함수 설명
assertTrue() / assertFalse() 값이 true인지 false인지 확인해요.
assertEquals() / assertNotEquals() 값이 같은지 아닌지 확인해요.
assertSame() / assertNotSame() 값이 같은 인스턴스를 가리키는지 확인해요.
assertIs() / assertIsNot() 값이 지정된 타입인지 확인해요. assertIs()는 그 타입으로 스마트 캐스트해요.
assertNull() 값이 null인지 확인해요.
assertContains() 요소(키, 문자, 부분 문자열, 정규식)가 컬렉션·배열·시퀀스·범위·맵에 있는지 확인해요.
assertContentEquals() 컬렉션·시퀀스·배열이 같은 순서로 같은 요소를 포함하는지 확인해요.

새 API를 사용하려면 @OptIn(ExperimentalKotlinTestApi::class) 애노테이션으로 명시적으로 opt-in해야 해요:

import kotlin.test.ExperimentalKotlinTestApi
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@OptIn(ExperimentalKotlinTestApi::class)
fun testValues(actual: Int, expected: Int, items: List<String>) {
    // 메시지는 단언이 실패할 때만 만들어져요
    assertTrue(actual > 0) { "Expected a positive value but got $actual" }

    // 단언이 실패하지 않는 한 리스트 형식화를 피해요
    assertEquals(expected, actual) { "Unexpected value for items: ${items.joinToString()}" }
}

자세한 내용은 이 기능의 KEEP을 참고하세요.

Kotlin/Native

Kotlin 2.4.20은 Kotlin Multiplatform 프로젝트의 SwiftPM 의존성을 위한 Package.swift 파일 자동 생성, sealed class와 교차 언어 상속 지원을 포함한 새 Swift export 기능, 그리고 개선된 증분 컴파일을 제공해요.

SwiftPM 의존성을 위한 생성된 Package.swift

SwiftPM 패키지에 의존하는 XCFramework를 export할 때, 결과 SwiftPM 패키지를 배포해야 올바르게 resolve돼요. 이를 돕기 위해 assembleSharedXCFramework Gradle 태스크가 이제 XCFramework와 함께 배포할 Package.swift 파일을 생성해요.

자세한 내용은 SwiftPM export 페이지를 참고하세요.

새 Swift export 기능

Sealed classes

Kotlin 2.4.20은 Swift export에 sealed class와 sealed interface 지원을 추가했어요.

이전에는 sealed 타입에 대한 모든 switch 문에 default 케이스를 작성해야 했어요. 이제 Kotlin에서 정의한 sealed 계층은 Swift enum으로 매핑되어, Xcode에서 완전한 자동 완성이 지원되는 exhaustive한 switch 문을 사용할 수 있어요.

Swift export는 각 sealed 타입에 sealedType() 메서드를 생성해요. 이 메서드는 sealed 계층의 직접 하위 클래스와 일치하는 케이스를 가진 Swift enum을 반환해요. 이 호출을 중첩해서 계층의 더 깊은 수준에 맞출 수 있어요.

예를 들어, Kotlin에서 class 계층을 가진 sealed interface를 선언해볼게요:

// Kotlin
sealed interface Shape

class Circle : Shape {
    override fun toString(): String = "Circle"
}

class Rectangle : Shape {
    override fun toString(): String = "Rectangle"
}

fun createCircle(): Shape = Circle()

Swift 쪽에서는 default 케이스 없이 exhaustive한 switch를 사용할 수 있어요:

// Swift
let shape = createCircle()

let name = switch shape.sealedType() {
    case let .circle(type): "It's a \(type.value)"
    case let .rectangle(type): "It's a \(type.value)"
}
// name == "It's a Circle"

switch가 exhaustive하므로 새 하위 클래스가 sealed 계층에 추가되면 컴파일러가 경고를 줘서, default 케이스에 의존하는 대신 즉시 처리할 수 있어요.

Swift export에서의 교차 언어 상속

Kotlin 2.4.20은 Swift export에 교차 언어 상속(cross-language inheritance) 지원을 도입했어요.

이 기능의 일반적인 사용 사례는 reverse import 패턴이에요. Kotlin에서 계약을 정의하고 Swift 쪽에서 플랫폼별 구현을 제공하는 방식이죠. 이는 Kotlin으로 직접 import할 수 없는 순수 Swift 라이브러리를 사용해야 할 때 특히 유용해요.

이 패턴을 구현하려면 Swift 구현이 상속할 Kotlin 슈퍼클래스와 Kotlin 인터페이스를 선언하세요. 그런 다음 Swift에서 이 인터페이스를 구현하고, 그 인터페이스를 받는 Kotlin 함수에 Swift 객체를 전달하면 돼요. 예를 들어 CryptoKit 라이브러리에서 사용하는 방법을 볼게요:

  • Kotlin 쪽에서 인터페이스, 그 인터페이스를 받는 함수, open 기본 클래스를 선언해요:
// Kotlin
interface CryptoProvider {
    fun hashMD5(input: String): String
}

fun processHash(provider: CryptoProvider, input: String): String = provider.hashMD5(input)

open class SwiftBase
  • Swift 쪽에서 export된 SwiftBase 클래스를 상속하고, 순수 Swift 라이브러리로 인터페이스를 구현한 뒤 객체를 Kotlin으로 다시 전달해요:
// Swift
import CryptoKit

final class IosCryptoProvider: SwiftBase, CryptoProvider {
    func hashMD5(input: String) -> String {
        guard let data = input.data(using: .utf8) else { return "failed" }
        return Insecure.MD5.hash(data: data).description
    }
}

let provider = IosCryptoProvider()

// Kotlin 함수를 호출하면 다시 Swift의 hashMD5()를 호출해요
print(processHash(provider: provider, input: "Hello, world!"))

Kotlin이 Swift 객체를 받으면 일반 인터페이스의 구현처럼 취급하면서 Swift 코드를 직접 호출해요.

Swift export에 대한 더 자세한 내용은 문서를 참고하세요.

klib 아티팩트의 증분 컴파일 개선

Kotlin 2.4.20은 klib 아티팩트의 증분 컴파일을 안정화 개선했어요. 이제 Beta 단계예요.

이 최적화는 Kotlin 1.9.20에서 처음 도입됐고, 디버그 빌드의 컴파일 시간을 크게 줄여주는 것으로 확인됐어요. 그 이후 여러 버그를 수정하고 성능을 개선했어요.

증분 컴파일을 사용해 보려면 gradle.properties 파일에 다음 옵션을 추가하세요:

kotlin.incremental.native=true

우리는 적극적으로 피드백을 수집하고 있으며, 다음 Kotlin 릴리스에서 모든 프로젝트에 증분 컴파일을 기본으로 활성화할 계획이에요. 문제가 발생하면 이슈 트래커에 보고해 주세요.

Kotlin/Wasm

Kotlin 2.4.20은 Kotlin/Wasm이 @JsFun 선언에서 최상위 require() 호출을 처리하는 방식을 변경하고, companion object 초기화 순서를 JVM 동작과 정렬하고, 함수형 인터페이스의 바이너리 크기를 줄이고, 새 컴파일 모드를 도입하며, Kotlin Gradle 플러그인의 wasmWasi 대상에 Wasmtime 런타임 지원을 추가했어요.

@JsFun 선언에서 최상위 require() 호출의 변경

Kotlin/Wasm은 이제 @JsFun 선언이 최상위 require() 함수를 사용하면 오류를 보고해요.

이전에는 컴파일러가 import-object.mjs 파일에 require 변수를 생성해서 @JsFun 선언이 require()를 호출할 수 있게 했어요.

이 동작은 의도치 않게 컴파일러 구현 세부 사항을 노출했어요. 여기서 벗어나는 전환을 지원하기 위해 Kotlin/Wasm은 생성된 require 선언을 제거하고, 컴파일러가 이제 그런 호출에 오류를 보고해요. 예를 들어:

// 오류를 보고해요
@JsFun("(mod) => require(mod)")
external fun loadModule(mod: String): JsAny

이 변경에 대비하려면 @JsFun 선언의 최상위 require() 호출을 @JsModule 애노테이션으로 대체하세요:

@JsModule("module")
external val module: Module

external interface Module {
    // 기대하는 모듈 멤버를 정의해요
}

동적 모듈 로딩에는 대신 import() 식을 사용하세요. webpack이 동적 import를 분석하지 않도록 /* webpackIgnore: true */ 매직 코멘트를 추가하세요:

@JsFun("""
    ((module) => () => module)(
        await import(/* webpackIgnore: true */ "module")
    )
""")
private external fun loadModuleDynamically(): JsAny?

import() 식을 조건부로 사용할 수도 있어요. 예를 들어 Node.js에서 실행할 때만 모듈을 로드할 수 있죠:

@JsFun("""
    ((module) => () => module)(
        ((typeof process !== "undefined") && (process.release.name === "node"))
            ? await import(/* webpackIgnore: true */ "module")
            : null
    )
""")
private external fun loadNodeModule(): JsAny?

프로젝트가 최상위 require() 함수를 요구하는 의존성에 의존한다면, globalThis의 속성으로 추가하는 방식으로 우회할 수 있어요:

@JsFun("""
    ((module) => {
        globalThis.require = module.default.createRequire(import.meta.url)
        return () => {}
    })(await import("node:module"))
""")
external fun defineRequire()

문제가 발생하면 이슈 트래커에서 피드백을 공유해 주세요.

companion object 초기화 순서 개선

Kotlin/Wasm은 이제 슈퍼클래스의 companion object를 하위 클래스의 companion object보다 먼저 초기화해 JVM 동작과 일치시켜요. 이전에는 초기화 순서가 뒤집힐 수 있어 플랫폼 간 동작이 일관되지 않았어요.

이 업데이트는 크로스 플랫폼 일관성을 개선하고 클래스 초기화 동작의 플랫폼별 차이를 줄여줘요. 또한 중간 클래스가 companion object를 선언하지 않는 경우를 포함해 더 깊은 상속 계층에서 companion object 초기화를 올바르게 처리할 수 있게 해줘요.

Kotlin Gradle 플러그인의 Wasmtime 지원

Kotlin 2.4.20은 Kotlin Gradle 플러그인의 wasmWasi 대상 런타임으로 Wasmtime 지원을 도입했어요.

이전에는 wasmWasi 대상이 Node.js 런타임만 지원해서 WASI 애플리케이션을 실행하려면 JavaScript bootstrap이 필요했어요. Wasmtime 지원 덕분에 이제 독립형 WebAssembly 런타임에서 Kotlin/Wasm 애플리케이션을 실행할 수 있어요.

wasmWasi 대상 런타임으로 Wasmtime을 사용하려면 Gradle 빌드 파일에 wasmtime()을 추가하세요:

kotlin {
    wasmWasi {
        wasmtime()
    }
}

YouTrack에서 피드백을 주시면 감사하겠어요.

새 컴파일 모드

Kotlin 2.4.20은 새 멀티 모듈 모드를 포함해 Kotlin/Wasm 컴파일 모드를 선택할 수 있게 지원해요. 이전에는 컴파일러가 monolith 컴파일 모드를 사용해서 프로젝트와 그 의존성을 함께 컴파일하고 단일 바이너리를 생성했어요. 이 방식은 컴파일러가 dead code elimination을 수행해 가장 작은 출력을 만들 수 있게 해줬죠.

이제 다음 컴파일 모드 중 하나를 선택할 수 있어요:

컴파일 모드 컴파일 출력 최적화 동작
monolith (기본값) 프로젝트와 그 의존성을 함께 컴파일해요. 단일 바이너리 도달 불가능한 선언을 제거하고 의존성을 포함한 전체 프로그램에 최적화를 적용해요.
multimodule-open-world 각 모듈을 독립적으로 컴파일하고 변경된 모듈만 다시 컴파일해요. 모듈마다 별도의 독립 바이너리 크로스 모듈 최적화를 적용하지 않아 바이너리가 더 커져요.
multimodule-closed-world 모든 모듈을 한 번의 호출로 처리하고 변경된 모듈만 다시 컴파일해요. 서로 의존하는 별도의 바이너리 도달 불가능한 선언을 제거하지만 각 Wasm 바이너리를 독립적으로 최적화해요.

컴파일 모드를 선택하려면 gradle.properties 파일에 kotlin.wasm.compilationMode 속성을 추가하세요:

kotlin.wasm.compilationMode=multimodule-open-world

또한 개발 빌드에는 closed-world 멀티 모듈 컴파일을, 프로덕션 빌드에는 monolith 컴파일을 사용하도록 Kotlin/Wasm을 구성할 수도 있어요. 이렇게 하면 개발 중 다시 컴파일 시간이 줄고 프로덕션 빌드에서는 가장 작은 출력을 만들 수 있어요.

이 구성을 사용하려면 gradle.properties 파일에 다음 속성을 추가하세요:

kotlin.wasm.compilationMode=multimodule-closed-world-only-in-dev

YouTrack에서 피드백을 주시면 감사하겠어요.

람다와 함수형 인터페이스의 바이너리 크기 감소

Kotlin 2.4.20은 Kotlin/Wasm이 람다와 함수형 인터페이스를 컴파일하는 방식을 변경해요. 별도의 익명 클래스를 생성하는 대신 컴파일러가 이제 함수를 생성하고 공유 기본 클래스를 사용해요.

KotlinConf 애플리케이션으로 진행한 테스트는 이 변경이 Wasm 바이너리 크기를 약 5–10% 줄여준다는 것을 보여줘요.

이 변경은 더 많은 동적 호출을 도입하므로 런타임 성능에 영향을 줄 수 있어요. 문제가 발생하면 이슈 트래커에 보고해 주세요.

Kotlin/JS

Kotlin 2.4.20은 data class의 exportability를 개선하고, 브라우저 테스트를 위한 새 실험적 DSL을 도입하며, suspending 람다를 JavaScript async 함수로 export하는 지원을 추가했어요.

export된 data class의 합성 함수 exportability 일관성

Kotlin 2.4.20은 @JsExport.Ignore 애노테이션이 data class 속성에 제대로 적용되지 못하게 하던 문제를 수정했어요.

이전에는 data class에 @JsExport 애노테이션을 표시하면, 자동 생성되는 copy()componentN() 함수 때문에 컴파일러가 여전히 data class의 exportability에 대한 경고를 보고했어요. 생성자와 속성을 명시적으로 @JsExport.Ignore로 표시한 경우에도 마찬가지였죠.

예를 들어, JavaScript로 export되는 Session data class가 export하지 않으려는 내부 DatabaseConnection 타입을 참조한다고 해볼게요:

// Kotlin
// JavaScript로 export되지 않는 내부 타입
class DatabaseConnection

@JsExport
data class Session @JsExport.Ignore constructor(
    val userId: String,
    @JsExport.Ignore val connection: DatabaseConnection,
)

이제 문제가 수정되어 컴파일러가 @JsExport.Ignore 애노테이션을 고려하므로, Session의 합성 copy()componentN() 함수가 더 이상 export되지 않는 타입 DatabaseConnection에 대한 경고를 일으키지 않아요. 이는 @ConsistentCopyVisibility와 @ExposedCopyVisibility 애노테이션이 도입한 가시성 규칙과 일치해요.

브라우저 테스트를 위한 새 DSL

Kotlin 2.4.20은 브라우저 환경에서 Kotlin/JS 테스트를 실행하기 위한 새 실험적 DSL을 도입했어요.

현재 Kotlin Gradle 플러그인은 여러 브라우저에서 JavaScript 테스트를 실행하기 위해 Karma를 브라우저 런처로 사용해요. Karma 프로젝트는 2년 동안 deprecated 상태였어요. 그래서 우리는 브라우저 테스트를 지원할 다른 방법을 탐구하게 됐죠.

새 DSL은 내부적으로 여러 도구를 관리하는 Karma를 대체하기 위한 것으로, 다음을 포함해요:

  • Playwright를 브라우저 드라이버이자 Chromium, Firefox, WebKit(Safari) 브라우저 엔진을 지원하는 배포 관리자로 사용해요.
  • Mocha를 테스트 러너로 사용해요.
  • webpack을 번들러로 사용해요. (다음 릴리스에서 Vite로 교체 예정이에요.)

브라우저 테스트용 새 DSL을 사용해 보려면 Kotlin/JS 대상의 browser {} 안에 opt-in test {} 블록을 추가하세요:

import org.jetbrains.kotlin.gradle.ExperimentalJsTestDsl
import kotlin.time.Duration.Companion.seconds

kotlin {
    js {
        browser {
            // 새 test {} 블록을 추가하고 구성해요
            @OptIn(ExperimentalJsTestDsl::class)
            test {
                // 모든 러너의 기본 timeout 구성
                timeout = 2.seconds
                // Gradle provider를 사용한 headless 모드 구성
                headless = providers
                    .environmentVariable("IS_IN_CI")
                    .map { it.toBoolean() }
                    .orElse(false)
                // Chromium 테스트 러너 활성화 및 구성
                chromium {
                    // 공통 timeout 옵션 재정의
                    timeout = 5.seconds
                    // 추가 실행 인자 추가
                    launchArgs.add("--no-sandbox")
                }
                // Firefox 테스트 러너 활성화
                firefox()
                // WebKit 테스트 러너 활성화
                webkit()
                // 추가 WebKit 테스트 러너 활성화 및 구성
                webkit("noheadless") {
                    // 커스텀 옵션 설정
                    headless = false
                }
            }
        }
    }
}

브라우저 테스트용 새 DSL은 활발히 개발 중이에요. YouTrack에서 피드백을 주시면 감사하겠어요.

자세한 내용은 Kotlin/JS에서 테스트 실행을 참고하세요.

suspending 람다를 async 함수로 export 지원

Kotlin 2.4.20부터 suspending 람다 식을 JavaScript async 함수로 export할 수 있어요.

이전에는 suspending 람다를 포함하는 선언을 Kotlin/JS 라이브러리에서 export할 방법이 없었어요. 이제 Kotlin 컴파일러가 Kotlin의 suspend 함수와 JavaScript 네이티브 async/await 모델 사이의 브리징을 자동으로 처리해줘요. 이는 Kotlin/TypeScript 혼합 코드베이스에서 유용하죠.

이 기능을 활성화하려면 build.gradle.kts 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    js {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions {
                    freeCompilerArgs.add("-Xsuspend-lambda-exporting")
                }
            }
        }
    }
}

그런 다음 관련 선언을 @JsExport로 표시하세요:

// Kotlin
@JsExport
class TaskRunner {
    suspend fun runTask(task: suspend () -> String): String {
        return task()
    }
}

TypeScript 쪽에서는 suspending 람다가 일반 async 함수로 나타나요:

// TypeScript
import { TaskRunner } from "..."

const runner = new TaskRunner();
const result = await runner.runTask(async () => "done");
console.log(result); // "done"

@JsExport 애노테이션에 대한 자세한 내용은 문서를 참고하세요.

Gradle

Kotlin 2.4.20은 Gradle 7.6.3부터 9.7.0까지 완전히 호환돼요. 최신 Gradle 릴리스까지의 버전도 사용할 수 있어요. 다만 그 경우 deprecation 경고가 발생할 수 있고, 일부 새 Gradle 기능이 작동하지 않을 수도 있다는 점 참고하세요.

Kotlin 2.4.20은 또한 Problems API와의 통합도 개선했어요.

Problems API 보고 개선

Kotlin 2.2.0은 Kotlin Gradle Plugin(KGP)이 Gradle의 Problems API와 통합된 첫 릴리스였어요. Kotlin 2.4.0은 Kotlin/JVM에 대한 컴파일러 메시지를 Problems API로 작성하는 지원을 추가했죠.

Kotlin 2.4.20은 컴파일러가 Problems API로 전달하는 정보에 컴파일러 진단 ID를 추가해요. 또한 이 ID를 기준으로 진단을 그룹화해서 컴파일 문제의 출처를 더 쉽게 식별할 수 있게 해줘요.

Gradle 8.6부터 KGP는 이 통합을 기본으로 활성화해요. API가 아직 계속 발전 중이므로 최신 개선을 누리려면 가장 최근 Gradle 버전을 사용하세요.

Build tools API

Kotlin 2.4.20은 build tools API에 Kotlin/JS, Kotlin/Wasm, Kotlin metadata에 대한 실험적 지원을 추가했어요.

Kotlin/JS, Kotlin/Wasm, Kotlin metadata 지원

Kotlin 2.2.0에서 build tools API(BTA)는 Kotlin/JVM용으로 제공됐어요. Kotlin 2.4.20은 Kotlin/JS, Kotlin/Wasm, Kotlin metadata라는 새 대상을 지원해 BTA 안정화를 향한 다음 단계를 밟았어요.

이로써 Kotlin Gradle 플러그인이 컴파일러와 더 일관되게 상호작용하게 돼요. 어떤 경우에는 더 빠르고 안정적인 컴파일의 혜택도 받을 수 있어요.

BTA는 빌드 시스템과 Kotlin 컴파일러 생태계 사이의 추상화 계층 역할을 하는 범용 API예요. 사용 가능한 빌드 도구에서 Kotlin 기능과 Kotlin 컴파일러 호환성을 지원하는 데 도움을 줘요.

Kotlin 2.4.20에서 BTA는 새 대상에 대해 opt-in으로 제공돼요. 사용해 보려면 gradle.properties 파일에 해당 속성들을 추가하세요:

kotlin.wasm.runViaBuildToolsApi=true
kotlin.js.runViaBuildToolsApi=true
kotlin.metadata.runViaBuildToolsApi=true

Kotlin 2.5.0부터는 Kotlin/JS, Kotlin/Wasm, Kotlin metadata에서 BTA를 기본으로 활성화할 계획이에요.

BTA 제안에 관심이 있거나 피드백을 공유하고 싶다면 이 KEEP을 참고하세요.

Kotlin 컴파일러

Kotlin 2.4.20은 변경된 Kotlin 러너 명령 kotlinr에 대한 업데이트를 포함하고, 실험적 Kotlin 컴파일러 native image를 도입해요.

Kotlin 러너 명령을 kotlin에서 kotlinr로 변경

kotlinr 명령이 Kotlin Toolchainkotlin 명령과의 이름 충돌을 피하기 위해 Kotlin 러너 명령으로 kotlin을 대체해요. Kotlin 러너는 kotlin 명령을 사용할 때 경고하고 대신 kotlinr을 권장하기도 해요.

Native image

Kotlin 2.4.20은 Kotlin 컴파일러 native image의 첫 Experimental 릴리스를 선보여요. native image는 표준 kotlinc 명령줄 도구의 드롭인 대체품으로, 더 빠른 시작 시간과 더 높은 성능을 제공해요.

native image를 사용해 보려면 GitHub Releases에서 빌드를 다운로드하세요.

native image는 -Xplugin 또는 -Xcompiler-plugin CLI 옵션으로 사용할 수 있는 다음 컴파일러 플러그인도 함께 번들해요:

Kotlin 컴파일러 native image에 대한 자세한 내용은 README를 참고하세요.

Breaking changes와 deprecations

이 섹션은 중요한 breaking changes와 deprecations를 강조해요. 전체 개요는 Compatibility guide를 참고하세요.

  • Apple이 32비트 watchOS 대상을 지원 중단하므로, watchosArm32 Kotlin/Native 대상이 이제 deprecated됐어요. Xcode 27 호환성을 위해 Kotlin 2.5.0에서 제거될 예정이에요.
  • Kotlin 2.4.20부터 Kotlin/Native 컴파일러는 public 인라인 함수 안이나 다른 파일에서 호출되는 internal 인라인 함수 안에서 AtomicFU 원자 연산을 금지해요.
  • Kotlin 2.4.20은 webpack의 npm 의존성을 5.108.1로 업데이트했어요. 이는 프로젝트에 두 가지 방식으로 영향을 줄 수 있어요:
    • webpack은 내장 minimizer 의존성을 terser-webpack-plugin에서 더 포괄적인 minimizer-webpack-plugin으로 옮겼어요. Terser는 여전히 기본 JavaScript minimizer지만, 프로젝트가 terser-webpack-plugin을 직접 구성하거나 그것에 의존한다면 구성을 업데이트해야 할 수 있어요.
    • webpack은 JavaScript 파일의 모듈 타입을 결정할 때 더 이상 import.meta를 무시하지 않아요. import.meta가 있으면 webpack은 파일을 ES 모듈로 취급하므로, CommonJS 구문도 사용하는 파일이 깨질 수 있어요. Kotlin/JS의 경우 useEsModules() Gradle DSL로 대상이 ES 모듈을 사용하도록 구성할 수 있어요. Kotlin/Wasm은 대부분의 경우 추가 구성 없이 작동해야 해요. Kotlin/Wasm에서 import.meta 오류가 발생하면 프로젝트 소스나 직접·전이 의존성이 import.meta를 사용하는지 확인하세요. 필요에 따라 직접 코드를 업데이트하고, 의존성이 문제를 일으킨다면 가능한 경우 호환 버전으로 업데이트하거나 라이브러리 메인테이너에게 문제를 보고하세요.
  • Kotlin 2.4.20부터 Kotlin/Wasm은 생성된 JavaScript wasmExports API를 deprecated 처리해요. 컴파일러는 wasmExports.memory를 제외한 모든 export에 대한 접근을 금지하며, wasmExports.memory는 경고와 함께 임시로 계속 사용할 수 있어요. 모듈의 WebAssembly.Memory 객체에 접근하려면 kotlin.wasm.unsafe.wasmMemory 속성을 사용하세요.

문서 업데이트

지난 릴리스 이후 Kotlin 생태계 문서를 위해 새 페이지와 튜토리얼을 만들고 기존 항목을 개편했어요:

2026년 9월 14일

라이브러리와 API Kotlin 2.4.20-RC3의 새로운 기능

더 알아보기