코틀린 JS로 배우는 코틀린 기초 - 코틀린으로 계산기 만들기

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

이 글은 코드스피츠 82 코틀린 JS로 배우는 코틀린 기초 2강에 대한 요약 자료입니다.
https://www.youtube.com/watch?v=hkwiI4ebJpA

코틀린의 데이터 타입

코틀린은 모든 데이터 타입을 클래스 객체형으로만 처리한다. 리터럴로 사용하던, 형을 지정하던 상관없이 일괄적으로 객체로 처리한다. 그리고 이를 컴파일러가 코드의 컨텍스트를 읽고 박싱/언박싱 빈도 등을 종합적으로 분석하여 기본형으로 처리할지/ 객체형으로 박싱할지를 판단한다. 코틀린의 철학은 컴파일러가 판단해줄테니 프로그래머는 편하게 사용해라 라고 할 수 있다.

Number 클래스를 상속받는 숫자형들은 Number의 메소드를 전부 사용할 수 있으며, 코틀린이 제공하는 타입들은 플랫폼 중립적이다. 정수형들에 대해서는 Unsingned타입을 제공한다는 점도 주목하자.

플랫폼 중립적이라는 의미는, 코틀린이 제공하는 타입들을 이를 지원하지 않는 다른 플랫폼에서도 그대로 사용할 수 있도록 해준다는 것이다. 코틀린의 좋은 점이기도 하다.

Char는 4바이트로 utf-16을 채용하여 서로게이트 쌍 문제가 일어난다. CharSequence는 Char 배열 클래스이며, 이를 상속받은 타입이 String이다. CharSequence를 상속받은 몇 가지 타입이 더 있지만 코틀린의 기본패키지에 속하지는 않는다.

이러한 타입들을 사용하면 코틀린은 멀티플랫폼에서도 이를 컴파일했을 때 실행가능하도록 보장해준다. 따라서 JS나 JVM, 그 외 다른 플랫폼에 있는 타입을 사용하는 것보다는 코틀린의 타입을 사용하는 것이 안전하다.

타입과 상관없이 모든 타입들은 다음과 같은 타입 변환 메소드를 가진다.

또한 컴파일러가 만약 타입변환이 불필요하다고 판단하면 알아서 이러한 변환 메서드를 제거해주기도 한다. 아무 부담없이 변환 메서드를 사용해도 된다.

앞서 알아본 타입들에 대한 Array 타입이다.

자바와는 달리 타입에 맞는 어레이가 아예 기본적으로 제공되므로 제네릭을 쓸 수 없다. 다만 이러한 배열타입들은 플랫폼마다 완전히 호환되지는 않으므로 코틀린에서 이를 플랫폼에 호환시키기 위한 형변환 메서드를 따로 구비하고 있다.

Array의 기본 속성과 고차함수들의 목록으로 실제 사용가능한 고차함수는 이보다 훨씬 많다. 또한 이러한 고차함수들을 모두 플랫폼 네이티브 문법으로 변환해준다.

Enum

Enum이란 무엇인가? 컴파일 타임에 확정되는 싱글톤 인스턴스 이다. 싱글톤 객체가 클래스의 인스턴스가 1개 만 있는 것을 보장한다면 Enum 은 특정 클래스를 원하는 인스턴스 개수만큼을 확정적으로 만드는 것이다. 컴파일 타임에 확정되기 때문에, enum에 명시된 인스턴스 외에 더 이상 인스턴스를 만들 수가 없다.

Any

Any는 모든 객체의 최상위 부모로 마치 자바스크립트와 Object와도 같다. 엄밀히 말하자면 코틀린의 최상위 부모는 Any<Nullable> 이라는 Nullable 타입이다. Any 그 바로 다음으로 최상위의 부모로써, 어떤 것을 상속받을 지 모를 때 Any를 상속받는다.

Nothing

Nothing은 예외의 반환값이다. 예외에 반환값이 있다니 의아할 수 있지만, 코틀린에서는 반복문을 제외하면 모든 문 statement 가 전부 식 expression 이다. if, if else, when(타 언어의 스위치) 등이 전부 값으로 환원된다. if 블록의 마지막에 있는 값을 변수의 값으로 치환하는 식이다. 마찬가지로, throw또한 함수이기 때문에 값을 반환해야 하는 식 expression이다. 문과 식에 대해서는 문은 컴파일러가 해석은 하지만 메모리에 남지 않는 제어코드, 식은 컴파일러가 해석함과 동시에 값으로 취급하여 메모리에 참조를 가지는 것으로 구분한다. 어떠한 문장을 변수에 담을 수 있다면 그건 식 expression 이다.

Nothing이라는 값은 변수에 이 값이 들어왔을 때 그 이하의 모든 코드 실행을 멈추게 된다. 우리는 Nothing을 직접 변수에 할당할 수는 없으며, Throw 함수의 리턴값으로만 할당할 수 있다.

Result

Result 타입은 스위프트에도 존재하는 타입으로 예외와 실패를 포함할 수 있는 타입니다. 어떠한 함수가 리턴하는 값이 성공하는 경우와 실패하는 경우가 둘 다 존재한다고 할 때 이를 포함하는 Result를 리턴할 수 있다. 이는 함수형에서 보통 either 라는 타입으로 사용하는 것으로, 코틀린에서는 이를 언어차원에서 내장하고 있다.

Unit

코틀린에서는 void 반환값을 사용하는 대신 Unit 이라는 타입으로 반환한다. 만약 코틀린이 호환될 플랫폼이 void를 지원하지 않는다면(놀랍게도 이런 언어가 있다) 코틀린 컴파일러는 반환 코드를 감싸서 결과가 무시되도록 하며, 그 외의 언어에서는 void 로 이를 변환한다.

Comparable

숫자와 같이 비교가 가능한 객체들에 대해 비교하고 순서를 결정할 수 있도록 제공하는 인터페이스가 Comparable 이다.

Comparator

실제 비교를 할 수 있는 compare 메서드를 제공해주는 인터페이스이다.

Function

코틀린에서 사용되는 모든 람다의 객체이다. 부모로써의 역할만을 하는 빈 타입으로 아무 기능도 내장하지 않는다.

Pair, Triple

파이썬처럼 2개 혹은 3개의 값을 묶은 객체의 타입으로, 리턴 시에 구조체를 만들지 않아도 이러한 타입을 사용하여 복수의 값을 리턴할 수 있다.

Throwable

자바의 Exception에 해당하는 객체로 모든 예외의 최상위 부모이다. Error와 Exception으로 나눌 수 있다. 자바에서는 Exception을 구현할 때 catch의 사용을 강제할 수 있었지만, 코틀린에서는 모든 Exception을 catch 없이 그냥 무시할 수 있다. 이는 코틀린의 Throwable이 앞서 살펴본 Nothing 을 리턴하기 때문이다.

val 과 var

val 은 value의 약자로 변하지 않는 상수를 가리킴

var은 variable의 약자로 할당 후에도 변경가능한 변수

코틀린은 선언과 동시에 할당해야 한다. 초기화하지 않고 선언만 할 수는 없다. 또한 하나의 선언에는 하나의 변수만 선언할 수 있다.

또한 자바나 c와 같이 타입을 먼저 선언하고 변수이름을 선언하는 방식과 반대로, 형을 변수이름 뒤에

변수이름 : 타입= 값 과 같은 형태로 사용한다. 이는 현대언어의 특징이다.

문자열 리터럴

코틀린에서 문자열 리터럴은 세가지가 있다.

  1. '' : char literal

자바와 동일하게 작은따옴표로 선언한 리터럴은 char 타입이 된다.

  1. "":string literal

큰따옴표로 선언한 리터럴은 String 타입이다.

  1. """..."""

큰 따옴표 세 개로 선언된 문자열 리터럴 내부에서는 escaping을 무시하고 new line을 포함하여 문자열을 만들어준다. 정규식 또는 html 태그를 만들 때 편리하다.

코틀린 계산기

그러면 코틀린으로 계산기를 만들어보자. 계산기를 본격적으로 만들기 전에, 우선 코틀린의 람다에 대해서 알아볼 것이다.

코틀린의 람다

코틀린의 람다는 다음과 같은 형태를 가진다.

{
	a1:Type, a1:Type -> Type
	body
}

함수호출 뒤에 중괄호가 나오는 것이 의아할 것이다. 이는 코틀린의 람다의 형태로써 , 다른 언어에서는 함수의 인자가 나오고 그와 같이 람다 표현식이 나오는것이 일반적이지만 코틀린에서는 함수 호출 다음에 중괄호가 나오고, 중괄호 안에 인자와 반환타입을 선언하고 람다 표현식이 나온다. 다음과 같이 사용한다. 코틀린은 람다 화살표에 막대기 1개인 -> thin arrow를 사용한다. 그 뒤 개행문자가 오면 개행문자 뒤의 부분을 함수 본체 body 로 해석한다.

다음과 같이 쓸 수 있다.

val sum  = {a1:Int, b1:Int->Int
	a+b
}
println(sum(5+3)) //8

그런데 이때, 람다 body에 마지막 returnValue를 이용해서 람다 화살표 뒤의 타입을 생략할 수 있다. 즉, 컴파일러는 블록의 마지막에 오는 값을 반환값으로 판단하고 이를 통해 타입을 자동으로 캐스팅해준다.

따라서 위의 sum함수는 보다 간략하게 표현할 수 있다. 타입을 명시하지 않고 바로 함수 body를 기술하면, body의 마지막 반환값을 타입으로 캐스팅한다. 타입 선언이 생략되면 body를 함수 시그니쳐(header)와 구분하지 않고 같이 기술할 수 있다.

val sum = {a1:Int, b1:Int->a+b}

만약 인자가 하나인 경우에는, 컴파일러가 it이라는 블록 지역변수로 인자를 접근할 수 있게 해준다. it은 생략이 가능하므로 아예 반환값만을 사용할 수 있다.

val double = {it*2} //컴파일 에러
println(double(2)) //4

그런데 이 때, 컴파일러가 생성한 it이라는 인자는 타입을 캐스팅해주지 않고 있기 때문에 컴파일러가 타입을 추측할 수 없어서 컴파일 에러가 나게 된다. 따라서 타입을 변수쪽에서 캐스팅 해주어야 한다.

val double:(Int)->Int={it*2}
println(double(2)) //2

컴파일러가 타입을 추론할 수 있게끔 변수쪽이든, 람다의 헤더부문이든 한 군데는 반드시 타입을 선언해야 한다. 결과적으로 보자면 변수의 타입 또한 람다가 될 수 있다.

따라서 다음과 같은 고차함수를 선언할 수 있다.

fun map(a:Int, b:(Int)->Int):Int{
	return b(a)
}
map(5,double) //10

혹은 이미 함수 시그니쳐에서 타입캐스팅이 되어있으므로 바로 람다를 인자로 넣을 수도 있다.

map(5,{it*2}) //10

코틀린은 다른 모던 언어와 마찬가지로 trailing block argument 라는 것을 구현하여, 람다 인자가 마지막으로 오는 경우 이를 함수 인자 바깥에 쓸 수 있다. 혹은, 인자가 람다하나 밖에 없는 경우 함수인자를 아예 생략할 수도 있다.

fun map2(mapper:(Int)->Int):Int{
	return b(5)
}
map(5){it*2} //10
map2{it*2} //10

그럼 이제 계산기를 구현해보자.

계산기를 구현하기 위해서는 우리는 우선 숫자와 +, -, * , / 사칙연산이 아닌 문자를 전부 trim할 것이다.

앞서 배워본 문자열 리터럴을 사용해서 정규식 타입 리터럴을 선언할 수 있다.

val cleanup:Regex = """[^.\d-+*\/]""".toRegex()

그러나 이 때, toRegex 메서드를 통해서 컴파일러는 이 val 상수의 타입을 유추할 수 있다. 따라서 형을 반드시 할당하지 않아도 컴파일러가 자동으로 타입을 캐스팅해주므로 생략할 수 있다.

val cleanup = """[^.\d-+*\/]""".toRegex()

현대 언어에서 타입을 뒤에 사용하는 이유는 컴파일러가 자동으로 캐스팅하여 이를 생략가능하게 하기 위함이다. 타입을 뒤에 쓸 때는 이를 생략한 것과 생략하지 않은 것이 차이가 별로 없어 자연스럽기 때문이다.

그 다음은 *, / 연산자의 피연산자를 묶어주기 위해서 정규식을 통해 그룹화를 할 것이다.

val mulDiv = """((?:\+-)?[.\d]+)([*\/])((?:\+-)?[.\d]+)""".trimMargin().toRegex()

매우 복잡한 정규식이기 때문에 부분적으로 나누어서 살펴보자. 잘 살펴보면 *, / 를 캡쳐하는 정규식 앞뒤의 정규식은 반복된다는 것을 알 수 있다.

((?:\+-)?[.\d]+) 이렇게 괄호 안에 감싸진 정규식은 캡쳐 그룹 이라는 것으로 괄호 안의 부분을 전체 문자열로부터 분리하여 그룹을 만들고, 이를 캡쳐한 결과를 만든다. 만약 (?:...) 처럼 앞에 ?: 를 붙이면 그룹은 만들지만 캡쳐는 하지 않는다.

따라서 (?:\+-) 라는 그룹은 +- 라는 패턴의 그룹이지만 결과는 캡쳐되지 않는다. 또한 (?:\+-)? 이렇게 그룹뒤에 바로 물음표가 오게 되면 앞서 만든 정규식 그룹이 존재할수도, 존재하지 않을 수도 있다는 뜻이다. [.\d]+는 . 또는 d(숫자 그룹)이 한개 이상 나온다는 뜻이다. 따라서 위의 정규식은 +-가 올 수도 있고, 오지 않을 수도 있고 그 뒤에 2.667나 17 등 숫자나 점이 한 개 이상인 숫자 가 오는 것을 표현한다.

+- 가 왜 나오는지 궁금할 수도 있을텐데, 우리의 계산기에서는 로직을 단순화하기 위해 숫자 앞에 오는 - 부호의 경우 뺄셈과 구분하지 않고 같이 처리하기 위해서 앞에 모두 + 를 붙여줄 것이다.

따라서 위의 정규식은 5+4, 5/4, +-5*+-4 등을 표현한다.

그러면 이러한 정규식으로 입력 문자열을 처리해주는 함수를 만들어보자.

fun ex(v:String):Double{
    val r0 = v.replace(cleanup,"").replace("-", "+-").replace(mulDiv){/*람다 본체*/}
} 

세 개의 replace 함수에서 앞의 두개는 인자를 그대로 받았지만 마지막 replace는 하나의 인자와 하나의 람다를 받는 것을 알 수 있다. 앞서 알아본 trailing block argument 에 따라 마지막 람다를 함수 인자 바깥으로 빼낸 것을 알 수 있다.

실제 CharSequence 타입의 replace 메서드를 살펴보자.

fun CharSequence.replace(
	regex:Regex,
	transform:(MatchResult)->CharSequence
):String

정규식을 인자로 받아 이에 대한 MatchResult를 다른 CharSequence 문자열로 반환하는 람다 를 두번째 인자로 받는다는 것을 알 수 있다. 첫번째와 두번째 replace함수는 람다가 생략된 것이 아니라 인자로 전달한 CharSequence가 람다의 리턴값인 CharSequence로써 해석된 것이다. 따라서 우리는 it 지역변수를 사용해서 MatchResult를 문자열로 반환하는 람다를 전달할 수 있다.

따라서 우리는, mulDiv 정규식으로 캡쳐한 +-가 붙어있거나 붙어있지 않은 정수나 실수를 피연산자로 하는 곱셈, 또는 나눗셈 식 그룹을 it 인자로 받을 것이다. 우리의 목표는 전달받은 (+-2*+-5) or (0.4/+-0.2) 형태의 식을 우선 split한 뒤 실제로 계산하는 것이다.

fun ex(v:String):Double{
  val r0 = v.replace(cleanup,"").replace("-", "+-").replace(mulDiv){
		val (_,left,op,right) = it.groupValues
	}
} 

정규식 MatchResult의 groupValues 에는 다음과 같은 결과가 담긴다.

["전체매치결과","캡쳐그룹매치결과1",결과2,결과3....]["전체 매치결과" , "캡쳐그룹 매치결과1", 결과2, 결과3....]

앞서 코틀린에서는 한 줄에 하나의 변수를 선언할수 있다고 햇지만, 괄호를 사용함으로써 한 줄에 여러개의 변수를 선언할 수 이 때 할당하는 값은 반드시 Array나 List여야 한다. 자바스크립트의 분해할당과 비슷하다고 생각하면 된다. Kotlin Destructuring 이라고 부른다. Destructuring은 Destructuring을 위한 별도의 인터페이스를 상속받아 구현한 타입만이 가능하다. (Array, List 등) . 주의해야할 점은 Array의 경우 Destructuring을 완전히 지원하지는 않고 5번 인덱스까지만 지원한다는 점이다. 언어차원의 하드코딩된 매직넘버라서 당황스러운 점이기도 하다. _ 로 선언한 변수는 선언은 되지만 이를 본문에서 사용하지 않도록 언어 차원의 제약이 걸려있기 때문에, 몇번이고 중복해서 사용할 수 있다.( _, __, ___namespace 를 차지하지 않음)

그룹캡쳐 결과를 얻었다면 의도적으로 덧붙인 + 를 제거하고 실제 숫자 타입으로 형변환을 해주자.

val l = left.replace("+", "").toDouble()
val r = right.replace("+", "").toDouble()

그 뒤 실제 연산을 해주고, 이를 처음과 마찬가지로 - 부호를 처리하기 위해 앞에 +를 덧붙여 준다.

"${if(op=="*") l*r else l/r}".replace("-", "+-")

코틀린은 if / if else 등의 블록 또한 값을 반환한다. 연산자에 따라 연산을 문자열 리터럴로 바꾸고, +를 덧붙인다. 는코틀린에서템플릿리터럴의역할을하는데,StringLiteral또는3중따옴표에서동일하게사용할수있다.는 코틀린에서 템플릿 리터럴의 역할을 하는데, String Literal 또는 3중 따옴표에서 동일하게 사용할 수 있다. `변수명표현식과같은방식으로사용할수있는데,주의할점은중괄호없이{표현식}` 과 같은 방식으로 사용할 수 있는데, 주의할 점은 중괄호 없이 `변수명과 같이 사용할 경우 변수명까지만 식으로 해석하고[0]등을 통한 인덱스 접근 등은 식으로 계산하지 않는다는 점이다. 따라서 모든 표현식expression은 그냥${}` 로 사용하는 습관을 들이는 것이 좋다.

따라서 위의 템플릿 표현식은 if 블록이 반환하는 값을 문자열로 변환해준다. toString()메서드와 동일하다고 할 수 있다.

fun ex(v:String):Double{
  val r0 = v.replace(cleanup,"").replace("-", "+-").replace(mulDiv){
		val (_,left,op,right) = it.groupValues
		val l = left.replace("+", "").toDouble()
		val r = right.replace("+", "").toDouble()
		"${if(op=="*") l*r else l/r}".replace("-", "+-")
	}
} 

이로써 곱셈/나눗셈 결과를 모두 문자열로 치환한 뒤 - 부호를 처리한 문자열을 얻게 되었다.

따라서 여기까지 진행했다면 우리는 -2 * -3 + 0.4 / -0.2 라는 식의 결과를 6 +-2 라는 문자열을 얻게 되었을 것이다. 이를 + 로 split 한뒤 reduce 함수의 역할을 하는 fold를 사용해 sum 을 하자.

fun ex(v:String):Double{
	val r0= ...{}.split('+').fold(0.0){sum,v->
	sum+ if(v.isBlank())0.0 else v.toDouble()
	}
	return r0
}

fold는 기본값이 주어진 reduce함수로 기본값과 이에 대해 수행할 reducer 람다를 받는다. 기본값은 reducer 람다의 sum 인자로 들어간다. isBlank 메서드는 trim을 자동으로 수행한 뒤 문자열이 빈문자열인지를 확인하기 때문에 공백문자열을 모두 걸러낼 수 있다.

그러나 여기에서 우리가 살펴봐야 할점이 있다. 우리는 r0이라는 변수에 단일 수식을 할당하고 이를 바로 리턴하고 있다. 어떠한 함수가 단일 표현식만을 가지고 있다면 코틀린을 이를 single expression function 으로 인식하여 몸체와 중괄호를 제거할 수 있다. 또한, 단일 표현식의 경우 식의 값으로 부터 함수 반환 타입을 추론할 수 있기 때문에 변수 선언에서 반환 타입을 생략할 수 있다. 따라서 최종 코드는 다음과 같다.

fun ex(v:String)=v.replace(cleanup,"").replace("-", "+-").replace(mulDiv){
		val (_,left,op,right) = it.groupValues
		val l = left.replace("+", "").toDouble()
		val r = right.replace("+", "").toDouble()
		"${if(op=="*") l*r else l/r}".replace("-", "+-")
	}.split('+').fold(0.0){sum,v->
		sum+ if(v.isBlank())0.0 else v.toDouble()
	}

그럼 이제 실제 어플리케이션 구동을 위한 app 함수를 만들어보자

fun app(){
    document.querySelector("#base")?.innerHTML = """
        <input id="input">
        <div id="result">
    """.trimIndent()
}

자바스크립트의 DOM객체를 app함수에서 그대로 부르는 것을 볼 수 있다. 이렇게 할 수 있는 이유는 KotlinJS의 표준 라이브러리 stdlib에 브라우저의 스펙과 동일한 W3C 객체를 그대로 정적 객체로 맵핑해놨기 때문이다. 따라서 코틀린에서도 자바스크립트에서 부를 수 있는 브라우저 객체를 그대로 사용할 수 있다.

querySelector의 ?.Safe Call이라고 하며 널 객체에 대한 참조가 일어날 경우 null을 반환하도록 처리해준다.

fun app(){
	document.querySelector("#base")....=""""""
	document.querySelector("input")?.addEventListenenr("keyup", {
	})
}

이벤트 람다를 만들어주면, 이벤트가 it으로 들어올 것이다. 람다를 밖으로 뺄 수없는 이유는 마지막 인자로 isCapture의 기본값이 들어오는 인자가 생략되어 있기 때문에 마지막 인자가 아니기 때문이다. 이 때 it으로 들어오는 event는 범용 event이므로 타입캐스팅을 해야만 키보드이벤트의 속성을 사용할 수 있다.

if((it as KeyboardEvent).keyCode ==13){
	val input = it.target as HTMLInputElement
	val v = input.value
	document.querySelector("#result")?.innterHTML = "$v = ${ex(v)}"
}

그 후 it에서 target 객체를 얻어 타입캐스팅 한 뒤 그 value를 앞서 만든 계산기 함수에 넣어 결과값을 표시하면 된다.

괄호처리

그럼이제 괄호가 들어간 수식을 계산할 수 있도록 해보자. 괄호를 계산하려면 어떤 일을 해야할까?

바로

  1. 내부에 괄호가 없는 식부터 해소
  2. 더 이상 괄호가 없을 때까지 반복

그렇다면 괄호 정규식을 만들어보자

val paren = """\(([^()]*)\)""".toRegex()

()괄호는 정규식에서 특수한 의미를 가지므로 문자열 괄호를 나타내기 위해서는 앞에 이스케이프 문자 \ 를 붙여줘야 한다. 괄호내부의 ([^()]*)은 괄호가 아닌 어떤 문자가 0개 이상 있는 캡쳐그룹으로, *는 정규식에서 0개 이상을 말한다. 즉, 어떤 내용이든 괄호만 아니면 상관없으므로 빈괄호를 포함한 내부에 괄호가 없는 식이 이 정규식의 매치 결과가 될 것이다.

그렇다면 이 정규식을 사용해서 앞서 말한 2번. 괄호가 없을 때까지 연산을 반복하는 함수를 만들어보자.

fun calc(v:String):Double{
    var r = v
    while(paren.containsMatchIn(r)) r= r.replace(paren){"${ex(it.groupValues[1])}"}
    return ex(r)
}

코틀린에서는 인자값이 항상 Immutable이기 때문에 이를 임의로 변경할 수 없다. 따라서 var 변수에 할당하여 그 변수를 변경해야 한다.

괄호 정규식을 검사하여 매치가 더 이상 없을 때까지, 괄호를 괄호를 제외한 나머지 부분, 즉 paren에서 괄호를 제외한 캡쳐그룹인 groupValues[1]으로 replace한다. 괄호를 제외한 부분은 매번 연산한 결과로 바꿔준다. 모든 괄호가 해소되면 마지막으로 연산한 결과를 리턴한다.

이제 app함수는 이렇게 생겼을 것이다.

document.querySelector("#input")?.addEventListener("keyup", {
    if((it as KeyboardEvent).keyCode !=13)return@addEventListener
    val input = it.target as HTMLInputElement
    val v = input.value
    console.log(v)
    document.querySelector("#result")?.innerHTML = "$v=${calc(v)}"
    input.value=""
})

앞서서는 if 블록 내부에 다음 코드들을 작성했지만, 쉴드 패턴을 사용해서 keycode가 13이 아니면 먼저 함수를 종료하는 방식으로 뎁스를 줄였다. 이 때 리턴구문이 매우 특이하게 느껴찔 것이다. 이러한 구문은 코틀린만의 특징이다. 이렇게 사용하는 이유는, 어노테이션이 없이 람다 내부에서 return하게 되면 항상 람다가 아니라 람다를 감싸고 있는 함수 를 리턴하기 때문이다. 함수 내부에서 몇겹으로 람다가 중첩되어 있더라도, 감싸고 있는 함수가 리턴된다. 그러나, 만약에 감싸고 있는 함수가 없고 인자로 전달된 addEventListener 내부의 람다 같은 경우에는 감싸고 있는 함수가 없기때문에 리턴을 하면 컴파일 에러가 발생한다.

따라서 인자로 전달된 람다의 경우에는 기명 return 이라는 문법을 사용해서 중첩된 위치에서 특정한 람다 위치까지만 리턴하는 방법을 사용한다. 이는 마치 자바 반복문에서 라벨을 지정하여 원하는 위치로 탈출하는 것과도 같다고 생각하면 된다. 기명 return을 위해서는 람다 또한 이름을 가지는 기명 람다가 되어야 하는데, 형식은 다음과 같다. name@{ return @name . 혹은, 이렇게 직접 이름을 만들지 않아도 람다를 인자로 호출한 함수의 이름이 바로 해당 람다의 이름이 되는 자동이름 을 사용할 수도 있다. 따라서 여기서는 addEventListener가 해당 람다의 이름이 되므로, 자동이름을 사용하여 return@addEventListenr 의 형태로 기명 return을 해준 것이다.

profile
inudevlog.com으로 이전해용

0개의 댓글