LiveScript 살펴보기 – 01 : 기본 문법에 대해

LiveScript(이하LS)는 자바스크립트로 컴파일되는 스크립트 언어로 함수형 언어의 스타일과 기능을 많이 도입하여 간결하고 불필요한 보일러 플레이트를 최대한 배제한 코드를 작성할 수 있는 언어이다. 커피스크립트의 간접적인 방언이며 (창시자가 역시 커피스크립트를 만든 사람이다.) 객체 지향 및 절차 지향적인 코드를 작성함에 있어서도 많은 잇점을 누릴 수 있는 언어이다.

기본 문법과 리터럴

참고: LiveScript는 Javascript가 처음 나왔을 당시의 이름이었다.

간단한 JS 코드를 하나 작성해보자. 콘솔에 인사 문구를 출력하는 아주 간단한 함수를 작성한다.

var hello = function(){
    console.log('hello, world');  
}  
hello();

이 코드의 LS 표현은 다음과 같다.

hello = !-> console.log 'hello, world!'
hello!

LS의 기본적인 문법은 함수형 언어들에서 많이 빌려왔는데, 그러면서 괄호와 컴마의 사용을 최소화 할 수 있는 형태로 수렴되도록 디자인하였다. 따라서 원라이너(one-liner)로 작성하는 코드가 JS처럼 억지스럽지 않고 가능한 자연스럽게 읽힐 수 있으며, 전적으로 코드량이 작고 깔끔해지는 효과가 있다.

이 글에서는 LS의 기본적인 구문들에 대한 문법을 살펴볼 예정이다.

기본 문법

  1. 심볼(이름) 중간에는 -를 쓸 수 있고, 이 표기는 컴파일 시에 camel-case로 변환된다. 예를 들어 background-color = 'green' 이라는 코드는 var backgroundColor = 'green';으로 컴파일된다.
  2. 라인의 끝에는 세미콜론을 붙이지 않아도 된다.
  3. 대부분의 경우 블럭은 들여쓰기를 이용해서 구분하며, 중괄호를 쓰지 않는다.
  4. 블럭은 인라인 구문과 절묘하게 결합될 수 있다. 블럭을 시작하는 키워드를 기준으로 같은 컬럼에 맞추거다 더 들여쓰는 것이 좋다. (사실 원래 라인보다 더 들여쓰면 되는데, 저렇게 정리하는게 이쁨)

이렇게 써 놓으면 괄호도, 세미콜론도, 콤마도 거의 없이 어떻게 자바스크립트랑 1:1 매칭이 되는 코드를 작성할 수 있을것인가에 대해서 궁금증이 일기 시작한다. 그렇다면 가장 기본적인 값, 객체, 함수들을 어떻게 작성하는지 리터럴 문법부터 살펴보도록 하자.

변수 및 대입

기본적인 대입 구문은 변수 = 값으로 등호 1개를 사용한다.  변수는 미리 선언하지 않는다. 자신의 스코프에서 정의된 적 없는 변수가 좌변에 사용되면 이는 자동으로 현재 스코프의 새로운 변수로 선언된다.

다만 이렇게 하면 상위 스코프의 변수를 수정할 수 없게 되는 문제가 있는데, 이는 상위 스코프에서는 자기가 알지 못하는 시점에 값이 바뀌는 부작용이 있는 패턴이다. 따라서 사용하기를 권하지 않으며, 대신에 편의를 위해서 := 라는 연산자를 도입하여 상위 스코프 변수를 변경할 수 있게 한다. (애초에 함수형 스타일을 지향하고 있고, 함수형(혹은 순수언어)에서는 중간에 상태가 바뀌는 것을 허용하지 않는다. 즉 모든 값은 상수이다.)

x = 10
# var x = 10

# 이 구문은 익명 함수를 정의하고 평가한다.
do ->
  x = 5 # 함수 내에서 새로이 정의된 지역변수이다. 따라서 원래 10인 전역변수를 변경하지 않는다.

# (function() {
#    var x;
#    return x = 5;
# })();

x # => 10

## 블럭내에서 := 를 쓰면 외부 스코프의 변수를 변경한다.
do ->
  x := 2
# x #=> 2

표현식

사실 LS의 거의 모든 것은 표현식이다. 따라서 아래의 코드는 대입문 우변에 뜬금없이 if 절이 들어간것 같지만, 사실은 조건에 따라 분기하는 표현식을 사용한 셈이다.

x = if 2 + 2 == 4
    then 10
    else 0
## var x = (2 + 2 == 4) ? 10 : 0;

참고로 블럭내의 여러 줄의 표현식이 연이어 오는 경우, 해당 블럭은 최종적으로 맨 마지막 표현식의 값으로 평가된다. 1

var vs const

앞서 변수는 따로 선언할 필요가 없다고 했지만, var 키워드를 이용해서 선언만 하는 것도 가능하다.  여기에 또 하나의 선언 키워드가 있는데, 바로 const이다. 짐작은 했겠지만, 이 값은 상수로 취급되며 재 할당시에 에러가 난다.2

const x = 10
x = 0 # Error!

여기서 const는 변수 자체가 재할당이 불가능한 상수라는 뜻이다. 객체는 항상 mutable하며, const로 정의되었더라도 내부의 프로퍼티는 늘 수정할 수 있다.

숫자 및 대수 리터럴

숫자 표기와 관련해서는 자바스크립트와 몇 가지 차이가 있다.

  1. .4는 유효한 숫자가 아니다.  반드시 0.4와 같이 써야 한다.
  2. 숫자중간에 _를 쓸 수 있다.  (긴 자리수 숫자에 유용하다)
  3. n~를 써서 2~36진수까지를 표현할 수 있다. 6~12는 10진수 8이다.
  4. 뒤에 단위가 붙은건 자동으로 무시된다. 180cm로 쓰는 것은 문제 없는 숫자 리터럴이다.

불리언

true/false외에 yes/no, on/off의 별칭을 지원한다.

void, null, undefined

LS의 undefined는 JS의 그것과는 좀 다른 개념이다. 정의되지 않은 빈 값은 void를 사용한다.

문자열 리터럴

겹따옴표, 홑따옴표를 같이 쓸 수 있다. 여기에 \word 와 같은 식으로 백슬래시를 문자열의 시작 기호로 쓰는 방법이 있다. 이 경우에는 빈칸, 컴마, 괄호 등 부호 문자 앞까지를 문자열의 덩어리로 인식한다.

\word ## 'word'

func \word, \word ## func('word', 'word');

[\word] ## ['word']
{prop: \word}

겹따옴표는 내삽(interpolation)을 지원한다. #{ 표현식}을 써서 문자열 내에 특정한 표현식의 결과를 삽입할 수 있다.  간단히 변수의 값만 삽입하는 경우에는 중괄호도 생략할 수 있다.

"The anser is #{2 + 2}"

홑따옴표를 1개 혹은 3개 사용하면 멀티라인 문자열을 작성할 수 있다. 홑따옴표 1개인 경우에는 여러 줄에 나눠쓴 텍스트라도 1줄로 인식하며, 3개 사용한 경우에는 줄바꿈 문자를 포함한다.

정규식

정규식 리터럴은 JS와 동일하다.  대신에 //를 사용하면 여러 줄에 정규식 패턴을 나눠서 쓸 수 있다. (따라서 LS의 인라인 코멘트는 //이 아닌 #를 쓴다.

/moo/gi ## /moo/gi;

//
  | [!=]==?
  | @@
  | <\[(?:[\s\S]*\>)?
//g
### /|[!=]==?|@@|<\[(?:[\s\S]*?\]>)?/g;

객체 리터럴

JS와 유사한데, 사실 유사한 문법은 키:값 쌍으로 프로퍼티를 표현한다는 것 외에는 없을 수도 있다. 기본적으로 중괄호 내에 컴마로 구분한 키값 쌍을 쓴다.

그러면서 중괄호는 당연히 생략이 가능하며, 키:값 쌍을 여러 줄에 하나씩 풀어서 쓰면 (들여써야 한다) 컴마도 생략할 수 있다.

 

## js와 동일한 객체리터럴 문법을 사용할 수 있다. 
obj = {prop: 1, thing: 'moo'}

## 중괄호를 생략할 수 있다. 
oneline = color: \blue, heat: 4

## 키:값 쌍을 하나씩 여러 줄에 쓸 수 있다. 
person = 
  age: 23           ## 키:값은 들여쓰며, 각 라인간에 컴마는 불필요하다.
  eye-color: \green ## 각 항목은 같은 폭만큼 들여써야 한다.
  height: 180cm 

객체 프로퍼티에 대해서 키 이름은 동적으로 적용된다. 문자열의 내삽을 이용하거나, 괄호로 감싸서 “키패스의 값” 자체를 키로 쓸 수 있다.

obj = 
  "#variable": 2245
  (person.eye-color): false  ## 'brown': false

키:값 쌍이아니라, 선언된 변수를 이용하면 변수명:값 쌍으로 해석해준다.  불리언 타입인 경우에는 별도의 선언도 필요없이 +, -를 이용할 수 있다.

```livescript
 x = 1
 y = 2
 obj = {x, y} # {x:x, y:y}

options = {+debug, -live}
 # {debug: true, live: false}
 ```

this@로 쓸 수 있는데, 속성을 액세스하는 경우 바로 뒤에오는 .을 생략할 수 있다.

this ## this를 그대로 쓸 수 있다. 
@    ## this
@.location
@location ## 으로 줄여 쓸 수 있다. 

그외에도 객체 리터럴의 확장이라든지 여러 추가 문법들이 있는데, 차차 알아보기로 하고 기본적인 객체 리터럴은 여기까지 살펴보자.

리스트 리터럴

함수형 언어를 지향하기 때문에 리스트에 대한 리터럴 지원이 좋은 편이다. 역시 컴마를 생략할 수 있고, 객체 리터럴과 유사하게 여러 줄에 각 원소를 쓸 수도 있다.

 

[1, person.age, 'French Fries']

## 호출가능한 원소들이 아니라면 컴마는 생략될 수 있다. LS에서는 함수명과 변수명의 구분이 없으므로
## 할당된 이름을 쓴다면 컴마를 써야 한다. 

[1 2 3 true void \word 'hello world']

## 객체 리터럴과 유사하게 각 원소를 여러 줄에 (컴마로 구분 없이) 쓰는 것도 가능하다.
## 단, 들여쓴다.

my-list = 
  32 + 1
  person.height
  'beautiful'

one-itemed = # 그런데 원소가 하나 밖에 없는 리스트라면?
  1
  ... ## 끝에 '...'을 붙여서 이거 지금 리스트라고 명시한다.

여러 줄로 리스트를 나열할 때의 규칙은 먼저 하위 원소는 상위 원소보다 들여써야 하며, ‘같은’ 레벨의 원소는 같은 양만큼 들여써야 한다는 것이다. 그리고 * 를 앞에 붙여서 리스트 내 리스트 구조를 쉽게 만들 수 있다.

문자열 리스트를 만들 때는 &lt;[ ... ]&gt; 문법이 요긴하다. 이 구문은 공백으로 구분된 모든 문자열을 “그대로” 공백으로만 분리하여 문자열의 리스트를 만든다. 또 참고로 내삽된 문자열은 앞에 %를 붙여서 각 파트별로 쪼개진 리스트로도 만들 수 있다. (사실 무슨 의미가 있는지는 모르겠음)

<[ way out hello world ]>  ## 문자열 리스트 리터럴
## ["way", "out", "hello", "world"]

a = 2
b = 3
c = %"the #a and #b"
## ["the", 2, "and", 3]

트리

트리는 리스트의 리스트, 객체 리터럴의 리스트 등의 중첩된 구조를 표현하는 방법이다. 중첩 구조에서는 하위 리스트의 시작을 알려주기 위해서 * 를 사용한다.

  • 각 원소의 처음은 최소한 들여써져야 한다.
  • 동일 레벨의 원소는 같은 들여쓰기를 가져야 한다.
  • 중간에 *로 시작하면 다시 해당 위치에서 동일레벨의 리스트를 새로 시작한다.
tree =
  * 1
    2    ## 별이 아니라 첫요소의 위치 기준으로 깊이를 판단한다.
    * 3  ## nested된 요소
      4
    5
    * 6
      7 
## [1, 2, [3, 4], 5, [6, 7]]

참고로 트리 계층으로 리스트의 리스트 구조를 만드는 문법은 객체 리터럴에서도 쓰이며, 혼합될 수 있다.

obj-list =
  * name: \tessa
    age: 24
  * name: \kendall
    age: 19

조건제시법

조건 제시법 형태로도 리스트를 만들 수 있다.

  1. 다른 리스트로부터
  2. 범위 값으로 부터
  3. 객체로 부터

for x in ... 방법이 리스트를 이용한 조건 제시법에 사용될 수 있다. 파이썬의 그것과 완전 동일하다. in LIST 대신에 from START to|til END [by STEP]을 쓸 수도 있다. 곧 범위리터럴 쪽에서 언급할 것이다.

파이썬의 사전과는 달리 of를 이용해야 객체 프로퍼티의 키를 얻을 수 있다. 이를 이용해서 객체의 프로퍼티를 순회하면서 리스트를 만들 수 있다.

d = [1 2 3 4]
e = [x * 2 for x in d]

f = [n * 2 - 1 for n from 0 to 20 by 2]

## 혹은 아래와 같이 예쁘게 써도 된다. 
f = [n * 2 - 1 for n 
     from 0
     to 20
     by 2]

obj = 
  person: \jane
  age: 22
  height: 161cm

## 리스트 축약 처럼 of를 써서 객체 속성 순회를 할 수 있다. 
girl = ["#x : #{obj[x]}" for x of obj]

범위

to 는 끝나는 지점을 포함하며, til은 끝나는 지점을 포함하지 않는다. 범위 리터럴은 0 to 10 과 같은 식으로 두 숫자 사이에 to/til을 넣는데, 이 때 기본적으로 증감분은 1이다. (시작값이 더 크다면 -1) 시작값은 기본적으로 0이므로 생략할 수 있다. 참고로 알파벳 문자도 범위를 만들 수 있다.

[1 to 5] ## 1,2,3,4,5
[1 til 5] ## 1,2,3,4
[til 5] ## 0,1,2,3,4
[\A to \D] ## 'A', 'B', 'C', 'D'

기타

잘 쓰이지는 않는데, :label 문법이 있다. 중첩된 루프 등에서 특정 레벨로 break를 즉시 할 수 있는 좋은 기능이다.

정리

지금까지 LS에서 기본적인 표현식을 사용하는데 있어서 기본적인 리터럴을 살펴보았다. LS는 기본적으로 JS로 1:1로 매핑되는 구조로 디자인되었기 때문에 많은 많은 부분에 있어서 JS와 비슷하거나, 좀 더 편의를 위해서 확장된, 그리고 더욱 간결하게 쓸 수 있는 표현들을 지원한다. 이러한 문법에서 느껴지는 어떤 간결하면서도 각 표현이 연결되어 부드럽고, 또 억지스럽지 않게 수학 공식처럼 코드가 이어질 수 있는 부분이 LS가 지향하는 느낌에 가깝지 않을까 한다.

여러 값의 표현은 JS를 근간으로 하고 있지만, 다음번에 알아볼 연산자 관련한 부분은 JS보다는 하스켈이나 스칼라 같은 함수형 언어로부터 큰 영향을 받고 있기 때문에, JS를 생각하고 있다면 꽤 어려울 수 있다. 그러한 관점에서본다면 현대의 대부분의 언어들은 값 리터럴들은 대부분 기본적으로는 비슷하기 때문에, LS는 JS와 다른 별개의 언어로 보는 것이 도움이 될지도 모르겠다.

참고자료:

예제

생일축하 노래의 가사를 출력하는 코드를 짜 보자. 아, 한줄이다.

[ "Happy Birthday #{if x is 2 then 'Dear my friend' else 'to you'}" for x from 0 to 3] * '\n' |> console.log

생일 축하 노래는 조금 쉽나? 그럼 이건 어떤가?

[[ "#x * #y = #{x * y}" for y from 2 to 9] * '\n' for x from 2 to 9 ] * ('\n\n' + '=' * 20 + '\n\n') |> console.log

  1. 어? 이거 어디서 본 스토리 같은데? Haskell의 do 문법이 이렇잖아. 
  2. 물론 자바스크립트에서 상수는 없지만, 이는 LS 컴파일러가 판단하여 처리한다.