ES6 In Depth는 ECMAScript 표준의 6번째 에디션(줄여서 ES6)을 맞아 JavaScript에 새로 추가된 요소들을 살펴보는 시리즈입니다.
화살표는 처음부터 JavaScript의 한 부분이었습니다. 첫 JavaScript 튜토리얼은 인라인 스크립트를 HTML 주석으로 감싸라고 조언했습니다. 이는 JS를 지원하지 않는 구식 브라우저가 의도하지 않게 JS 코드를 화면에 표시하는 것을 방지하려는 목적이었습니다. 이 조언에 따라 작성한 코드는 다음과 같은 모습이 될 것입니다.
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
구식 브라우저들이 처리하게 되는 것은 지원하지 않는 태그 2개와 주석입니다. 오직 최신 브라우저들만이 JS 코드를 처리하게 됩니다.
이렇게 이상한 꼼수를 지원하기 위해, JavaScript 엔진은 <!--
화살표 기호를 한줄짜리 주석문의 시작으로 처리합니다. 농담이 아닙니다. 정말 이것이 랭귀지의 일부였습니다. 그리고 이것은 지금도 지원되고 있습니다. 인라인 <script>
의 첫 줄에서뿐만 아니라 JS 코드의 모든 곳에서 적용됩니다. 심지어 Node도 이것을 지원합니다.
공교롭게도, ES6에 이르러서야 이런 스타일의 주석문이 처음으로 표준화되었습니다. 하지만 오늘 우리가 이야기하려는 화살표는 이것이 아닙니다.
또다른 화살표 기호인 -->
도 한 줄짜리 주석문을 나타냅니다. 원래는 -->
기호 앞에 있는 HTML 문자열이 주석이지만, 이상하게도 JS는 -->
다음에 있는 나머지 라인 구문을 주석으로 취급합니다.
점점 더 이상해집니다. 이 화살표는 오직 라인 맨 앞에 있을 때만 주석문으로 처리됩니다. -->
기호가 JS 코드 중간에 있을 때는 “goes to” 연산자로 처리됩니다!
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
이 코드는 정말 동작합니다. n 값이 0 이 될 때까지 루프가 실행됩니다. 이것 또한 ES6에 새로 도입된 요소가 아닙니다. 대신 친숙한 기호들에 약간의 오해가 겹쳐진 것입니다. 무엇이 오해인지 이해할 수 있나요? 언제나처럼, 퀴즈의 답은 Stack Overflow에서 찾을 수 있습니다.
그리고 작거나 같음 <=
기호가 있습니다. 아마 당신은 JS 코드에서 더 많은 화살표 기호들을 발견할 수 있을 것입니다. 숨은 그림 찾기는 여기서 멈춥시다. 이제 존재하지 않는 화살표 기호를 정리해봅시다.
<!-- |
한 줄짜리 주석문 |
--> |
“goes to” 연산자 |
<= |
작거나 같음 기호 |
=> |
??? |
=>
기호는 무슨 용도죠? 오늘 그 얘기를 해봅시다.
먼저, 함수에 대해 조금 더 얘기해봅시다.
어디에나 있는 함수 표현식
JavaScript의 재미있는 점은 함수가 필요하다면 언제나, 실행 코드의 중간에서라도 함수를 써넣을 수 있다는 점입니다.
예를 들어, 브라우저에게 사용자가 어떤 버튼을 클릭하면 무엇을 해야 하는지 지시한다고 가정해봅시다.
$("#confetti-btn").click(
jQuery의 .click()
메소드는 함수 하나를 인자로 취합니다. 문제 없습니다. 당신은 여기에 바로 함수를 써넣을 수 있습니다.
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
이렇게 코드를 작성하는 것이 지금의 우리에게는 아주 자연스러운 일입니다. JavaScript가 대중화 되기 전까지는 이런 방식의 프로그래밍이 낯설었다는 사실이 이상해 보일 지경입니다. 다른 많은 랭귀지들은 이런 방식을 지원하지 않습니다. 물론 Lisp은 1958년에 람다 함수(lambda function)라고 불리는 함수 표현식을 지원했습니다. 하지만 C++, Python, C#, Java 등은 긴시간 동안 함수 표현식을 지원하지 않았습니다.
지금은 아닙니다. 이제 4개 랭귀지 모두 람다(lambda)를 지원합니다. 새로 발표되는 랭귀지들도 대부분 람다를 지원합니다. 모든 것이 JavaScript 덕분입니다. 초창기 JavaScript 프로그래머들이 두려움 없이 람다를 적극적으로 활용하여 라이브러리를 구축한 덕분에 이 개념이 널리 퍼지게 되었습니다.
그렇다고 할 때, 모든 랭귀지들 중에서 JavaScript의 람다 문법이 가장 장황하다는 사실은 약간 슬픈 일입니다.
// 6개 랭귀지의 아주 간단한 함수 표현식.
function (a) { return a > 0; } // JS
[](int a) { return a > 0; } // C++
(lambda (a) (> a 0)) ;; Lisp
lambda a: a > 0 # Python
a => a > 0 // C#
a -> a > 0 // Java
화살통 속의 새 화살
ES6는 함수 표현식을 위해 새로운 문법을 도입했습니다.
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());
인자가 하나 뿐인 간단한 함수를 표현할 때, 새로운 문법에 의하면 그냥 Identifier => Expression
라고 쓰면 됩니다. 이제 function
문과 return
문, 그리고 괄호, 중괄호, 세미콜론 등을 타이핑할 필요 없습니다.
(개인적으로 이것을 아주 반갑게 생각합니다. function
을 타이핑할 필요가 없다는 사실이 제게는 무척 중요합니다. 왜냐하면 저는 항상 functoin
이라고 잘못 타이핑하고 이를 수정하는 습관이 있기 때문입니다.)
여러 개의 인자가 있는 함수 (또는 인자가 없는 함수, 또는 레스트(rest) 파라메터나 디폴트 파라메터를 사용하는 함수, 또는 디스트럭처링(destructuring) 인자가 있는 함수)를 표현할 때는 인자 리스트를 괄호로 묶어줍니다.
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
무척 보기 좋군요.
화살표 함수는 Underscore.js와 Immutable 같은 라이브러리들이 제공하는 함수형 도구들과 어울려 아름답게 동작합니다. 사실, Immutable 문서에 있는 모든 예제들은 ES6로 작성된 것입니다. 그래서 많은 예제들이 벌써 화살표 함수를 쓰고 있습니다.
함수형 개념이 덜 강조되는 상황에서는 어떨까요? 화살표 함수는 한 줄짜리 함수 표현식 대신 코드 블럭을 가질 수도 있습니다. 앞서 예제를 떠올려 보세요.
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
이것이 ES6로는 이렇게 표현됩니다.
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});
이정도는 사소한 개선일 것입니다. Promise를 사용하는 코드에는 조금 더 큰 영향을 줍니다. }).then(function (result) {
라인들을 쌓아올릴 수 있기 때문입니다.
블럭 바디를 갖는 화살표 함수의 경우 어떤 값을 자동으로 리턴하지 않는다는 사실에 주의합시다. 어떤 값을 리턴하려면 return
구문을 사용해야 합니다.
화살표 함수를 이용해서 단순 객체를 리턴할 때 지켜야 하는 규칙이 하나 있습니다. 해당 객체를 항상 괄호로 묶어주어야 한다는 것입니다.
// 각 강아지(puppy)가 가지고 놀 비어있는 객체 만들기
var chewToys = puppies.map(puppy => {}); // BUG!
var chewToys = puppies.map(puppy => ({})); // ok
혼란스럽게도 비어있는 객체 {}
와 비어있는 블록 {}
은 보기에 똑같습니다. ES6의 규칙에 따르면 화살표 바로 다음에 오는 {
기호는 언제나 블록의 시작으로 취급합니다. 객체의 시작으로 취급하는 일은 절대 없습니다. 그래서 puppy => {}
코드는 아무것도 하지 않고 undefined
를 리턴하는 화살표 함수로 처리됩니다.
더욱 혼란스러운 것은 {key: value}
같은 객체 리터럴이 라벨을 포함한 블록과 보기에 똑같다는 점입니다. 적어도 JavaScript 엔진에게는 똑같아 보입니다. 다행인 것은 모호한 기호가 {
하나뿐이라는 점입니다. 그래서 우리는 객체 리터럴을 괄호로 묶는다는 주의사항만 기억하면 됩니다.
this
는 어떻게 되나요?
통상적인 function
함수와 화살표 함수 사이에는 약간의 차이점이 있습니다. 화살표 함수는 자기 고유의 this
값을 갖지 않습니다. 화살표 함수 안의 this
값은 언제나 화살표 함수를 감싸는 외부 스코프(scope)에서 계승 받습니다.
이것의 실제 의미를 알아보기 전에, 하던 얘기를 조금 더 해봅시다.
JavaScript에서 this
는 어떻게 결정되나요? this
는 어디에서 유래하나요? 결코 간단하게 답할 수 없는 문제입니다. 만약 이 문제가 간단하다고 느껴진다면, 그건 당신이 이 문제를 오랫동안 다뤄왔기 때문입니다!
이 문제가 그토록 자주 제기되는 이유는 function
함수의 this
값이 자동으로 주어지기 때문입니다. 우리가 원하든 원하지 않든 말이죠. 다음과 같은 꼼수를 사용해본 적이 있나요?
{
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
여기서, 내부 함수에 우리가 정말 쓰고 싶은 코드는 this.add(piece)
뿐입니다. 불행히 내부 함수는 외부 함수의 this
값을 계승하지 않습니다. 내부 함수의 this
값은 window
또는 undefined
가 될 것입니다. 임시 변수 self
는 외부 함수의 this
값을 내부 함수에서 참조하기 위해 사용한 것입니다. (이 꼼수를 대신하는 또다른 방법은 내부 함수의 .bind(this)
메소드를 사용하는 것입니다. 어떤 방법도 그다지 아름답지 못합니다.)
ES6의 경우, 다음과 같은 규칙을 따른다면 this
와 관련된 대부분의 꼼수를 쓰지 않아도 됩니다.
object.method()
문법을 통해 호출하는 메소드는 화살표 함수가 아닌 일반 함수를 쓰세요. 그런 함수들은 호출자(caller)로부터 의미 있는this
값을 전달 받습니다.- 그밖의 모든 경우에는 화살표 함수를 쓰세요.
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
ES6에서, addAll
메소드는 호출자로부터 this
를 전달받습니다. 내부 함수는 화살표 함수이기 때문에, this
값을 자신을 감싸는 외부 스코프로부터 계승 받습니다.
보너스로 ES6는 객체 리터럴 안에 메소드를 간략하게 정의하는 표기법도 제공합니다! 그래서 위의 코드를 좀 더 간결하게 만들 수 있습니다.
// ES6 with method syntax
{
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
메소드와 화살표 중간에 더이상 functoin
이라고 써넣지 않아도 됩니다. 정말 좋은 생각입니다.
사소하지만 화살표 함수와 일반 함수 사이에 차이점이 하나 더 있습니다. 화살표 함수에는 arguments
객체가 전달되지 않습니다. 물론 ES6를 쓰는 당신이라면 arguments
객체보다 레스트(rest) 파라메터나 디폴트 파라메터를 더 좋아할 것입니다.
컴퓨터 과학의 심장을 뚫는 화살
우리는 지금까지 화살표 함수의 실용적인 사용법에 대해 이야기했습니다. 제가 제안하고 싶은 화살표 함수의 용도가 하나 더 있습니다. ES6의 화살표 함수를 컴퓨터 과학의 깊숙한 이야기를 들춰내기 위한 교육자료로 활용하는 것입니다. 그것이 실용적일 수도 실용적이 아닐 수도 있지만, 결정은 당신 몫입니다.
1936년, Alonzo Church와 Alan Turing은 각자 독자적인 방법으로 강력한 컴퓨터 연산 모델을 개발했습니다. Turing은 자기가 개발한 모델을 a-machine이라고 명명했지만, 곧 사람들은 튜링머신이라고 부르기 시작했습니다. 이와 대조적으로 Church는 함수(function)에 대해 연구했습니다. 그의 모델은 λ(람다)-calculus라고 불렸습니다. (λ는 그리스 문자 람다의 소문자 표기입니다.) 이것이 Lisp가 함수를 LAMBDA
라는 키워드로 표기하는 이유입니다. 그리고 오늘날 우리가 함수 표현식을 “람다(lambda)”라고 부르는 이유이기도 합니다.
그런데 λ-calculus란 무엇인가요? “컴퓨터 연산 모델”은 무엇을 의미하나요?
짧게 설명하기 어려운 문제입니다. 하지만 시도해보겠습니다. λ-calculus는 초창기 프로그래밍 랭귀지들 중 하나입니다. 원래 프로그래밍 랭귀지로 설계된 것은 아니었습니다. 해결하고자 하는 연산을 표현하기 위한 아주 단순하고, 꼭 필요한 것만 있는, 순수한 수학적 아이디어였습니다. 프로그램 내장 컴퓨터(stored-program computer)는 그후 10~20년 정도 지나서야 출현했습니다. Church는 컴퓨터 연산의 일반 개념을 증명하기 위해 이 모델을 만들었습니다.
그는 시스템에 필요한 것이 오직 함수(function)뿐이라는 사실을 발견했습니다.
이 선언이 얼마나 특별한 것인지 생각해봅시다. 객체가 없어도, 배열이 없어도, 넘버가 없어도, if
구문이나, while
루프, 세미콜론, 할당 구문, 논리 연산자, 또는 이벤트 루프가 없어도, 함수만 있으면 JavaScript로 가능한 모든 연산을 새로 만들 수 있습니다.
여기 Church의 λ(람다) 표기법을 이용해서 수학자가 작성하는 일종의 “프로그램” 예제가 있습니다.
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
이와 동등한 JavaScript 함수 표현식은 다음과 같습니다.
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
이제 JavaScript는 실제로 동작하는 λ-calculus를 갖게 되었습니다. JavaScript 속의 λ-calculus 입니다.
Alonzo Church와 후배 연구자들이 λ-calculus에 관해 무엇을 더 연구했는지, 그리고 λ-calculus가 어떻게 거의 모든 메이저 프로그래밍 랭귀지들에 스며들었는지는 이 글의 범위를 넘어섭니다. 하지만 만약 컴퓨터 과학의 기초에 관심이 있다면, 또는 함수만 갖고 있는 랭귀지로 루프와 재귀호출을 어떻게 구현하는지 알고 싶다면, 어느 비 오는 날 오후를 할애해서 Church numerals와 fixed-point combinators를 읽어 보세요. 그리고 그 내용을 Firefox 콘솔이나 Scratchpad에서 실험해보세요. ES6의 화살표 함수 덕분에, 이제 JavaScript는 λ-calculus를 위한 최고의 랭귀지라고 주장할 수 있게 됐습니다.
언제 화살을 쓸 수 있나요?
ES6의 화살표 함수는 제가 지난 2013년 Firefox에 구현했습니다. 그리고 Jan de Mooij가 처리 속도를 더 빠르게 만들었고, Tooru Fujisawa와 ziyunfei가 버그를 수정했습니다.
화살표 함수는 마이크로소프트의 Edge 브라우저 프리뷰 버전에도 구현되어 있습니다. 지금 당장 웹에서 화살표 함수를 쓰고 싶다면 Babel, Traceur, 그리고 TypeScript를 이용하면 됩니다.
다음 글에서 다룰 주제는 ES6의 가장 생소한 기능입니다. 우리는 typeof x
가 완전히 새로운 값을 리턴하는 것을 보게될 것입니다. 우리는 언제 문자열이 아닌 이름이 존재할 수 있는지 묻게 될 것입니다. 우리는 평등함에 대해 의문을 갖게 될 것입니다. 아마 이상할 것입니다. 그러니 ES6의 심볼(symbol)에 대해 깊이 살펴볼 다음 글도 함께해 주세요.
이 글은 Jason Orendorff가 쓴 ES6 In Depth: Arrow functions의 한국어 번역본입니다.
작성자: ingeeKim
"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.
13 댓글