ES2015와 React

현대적인 자바스크립트 앱을 작성하고 이해하기 위해서 새로운 ES2015 언어 명세들을 알아봅니다.

작성자

이재호

이재호

sairion

ES2015와 React

React는 Virtual DOM을 통해 과거 브라우저와 호환성을 유지하는 반면, 트랜스파일러를 이용한 최신 ECMAScript 명세의 사용을 적극적으로 권장하고 있기도 합니다. React에 관련된 자료들을 처음 접하는 분들은 다소 답답함을 느끼는 부분일 수도 있습니다. 수많은 라이브러리들이 최신 언어 명세를 이용해서 작성되고 있는 추세이고, 이를 사용하면 프로젝트의 코드 가독성도 크게 향상되기 때문에 IE8 이하의 구 브라우저를 지원해야 하는 상황이 아니라면 최신 언어 명세의 사용을 권장합니다.

이 글의 목적은 React 앱의 코드를 읽고 쓰는 데에 도움을 주기 위한 것입니다. ECMAScript 2015와 언어 명세 제안서들에 대해서는 이 글에서 모든 내용을 커버하기는 힘들기도 하고 인터넷에서 많은 자료들을 구할 수 있는 관계로 React에 관련된 새로운 부분만 간략하게 설명하도록 하겠습니다.

ES2015? ES6? ES7?

이전 글들에서는 ES6, ES7, ES2015 같은 수많은 용어들이 사용되었는데요, 다시 정리를 해 보겠습니다.

일반적으로 ES2015라고 하면 2015년 6월에 Allen Wirfs-Brock를 에디터로 하여 공식적으로 발표된 ECMAScript의 표준 ECMA-262의 6번째 에디션을 의미합니다. ES6이라고 칭해졌던 것들은 기본적으로 ES2015와 같으며, 매년 에디션을 내자는 움직임이 있어 ES6에서 연도를 반영하는 형태의 ES2015로 변경되었습니다. 이에 따라 ES7이라 불려지던 것은 대부분 ES2016이 되겠지만, 모든 현재의 proposal이 표준이 된다는 법은 없으므로, 그냥 "ES 제안서" 정도로 표현하면 될 것 같습니다. 물론 아직 인터넷에는 수많은 용어들이 딱히 명확한 구분 없이 사용되고 있는 상황입니다.

그 외 ECMAScript 표준이 어떻게 만들어지는지에 대해서는 TC39 미팅에 참석하셨던 분의 글을 참고해 봅시다. 알아둬야 할 점이라면, ECMAScript 표준 제안 단계에는 stage가 있다는 것입니다. Babel에서는 언어 명세의 stage에 맞춰 기능을 사용할 수 있게 설정할 수 있는 옵션을 제공하며, stage 0 제안 단계의 언어 명세에 대해서는 semver를 고려하지 않는다고 밝혔으므로 (즉 API가 한 순간 바뀔 수 있습니다), 사용한다면 shrinkwrap 기능 등을 이용하여 업데이트를 보수적으로 해야 할 필요가 있습니다.

구현체 별 언어 명세 지원 테이블은 http://kangax.github.io/compat-table/es6/를 참고하도록 합시다.

그러면 이제 중요한 언어 명세들에 대해 알아보겠습니다.

Block-level assignment: let/const

let은 block 스코핑을 사용할 수 있는 최신의 기능이며, V8을 비롯한 대부분의 엔진에서 아직 제대로 지원하고 있지 않습니다. 현재 글을 쓰는 시점에서는 V8에서 strict mode일 시에만 작동합니다.

'use strict';
let foo = 1;
{
    let foo = 2;
    console.log(foo); // => 2
}
console.log(foo); // => 1

위와 같이 중간에 별도의 바인딩을 설정하는 컨트롤 플로우를 만들 수도 있으며,

for (let i = 0; i < 10; i++) {
    setTimeout(() => { console.log(i); }); // 9, 9, ... 9
}
for (let i = 0; i < 10; i++) {
    setTimeout(() => { console.log(i); }); // 0, 1, ... 9
}

for loop 안 에서는 let 바인딩을 매 룹마다 새로 생성해서, 반복문에서 잘못된 바인딩을 참조하는 흔한 실수를 방지할 수 있습니다.

더 자세한 설명은 2ality blog의 글을 참고합시다.

Destructuring Assignment

destructuring assignment는 오브젝트의 구조와 같이 기술해 바인딩을 만들 수 있는 방법입니다.

var [a, b] = [1, 2]; // a => 1, b => 2
var {a, b} = {a: 1, b: 2}; // a => 1, b => 2

// 함수에서
function foo([a, b]) { console.log(`${a} and ${b}`); }
foo(['a', 'b']); // => 'a and b'

함수에서 destructuring assignment를 이용하면, 패턴 매칭이나 키워드 인자처럼 사용할 수 있습니다.

function bar({ first, second }) {
    return `${first} and ${second}`;
}
// nullable with default argument
function baz({ first, second } = { first: '?', second: '?'}) {
    return `${first} and ${second}`;
}

bar({ first: '1', second: '2' }) // '1 and 2';
bar(); // '? and ?'
baz(); // throws error

인자가 nullable이라면, 위의 baz와 같이 default 인자를 지정해 둬야 합니다.

더 자세한 설명은 (MDN)을 참고합시다.

Template String and Tagged Template

자바스크립트에서는 원래 멀티라인 스트링을 쓸 수 없어, 멀티라인을 표현하려면 string concatenation을 해야만 했습니다. javascript var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + '\n'; lorem += 'Maecenas sed velit at risus finibus lobortis semper vitae tortor.' + '\n'; lorem += 'Integer iaculis in sem sed venenatis. Praesent vulputate ante' + '\n'; ...

ES2015 이후의 스펙에서는 멀티라인 스트링을 표현하기 위해 Template string을 사용할 수 있습니다.

var lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas sed velit at risus finibus lobortis semper vitae tortor.
Integer iaculis in sem sed venenatis. Praesent vulputate ante`;

템플릿 스트링은 이름에서 드러나듯 string interpolation을 가능하게 해 줍니다.

Relay에서 GraphQL DSL을 표현하기 위해 tagged template string을 사용하는 경우가 대표적인 사용례라고 볼 수 있습니다. Tagged template string은 backtick앞에 문자열을 조작하는 함수를 사용하는 경우입니다.

더 자세한 설명은 MDN을 참고하도록 합시다.

ES2015 (Fat) Arrow Function

커피스크립트, C# 등을 사용해보신 분들께는 이미 익숙할 Arrow Function이 있습니다. ES2015의 Arrow Function은 기존의 함수와는 작동이 많은 부분 차이가 나는데, 가장 유용한 부분이라면 Arrow Function은 자신의 this를 가지지 않고, 바깥 스코프의 this를 참조하는 것을 들 수 있습니다. 예를 들면 이벤트 핸들러에서 fn.bind(this) Babel에서는 우리에게 이미 익숙한 self 또는 that 이름의 변수명으로 바깥 스코프에서 참조하는 형태로 트랜스파일됩니다.

유의해야 할 점은, arguments가 없다는 점, 그리고 Arrow Function은 new의 타겟이 될 수 없다는 점입니다. (Babel의 트랜스파일 결과물은 function 을 반환하므로 스펙과 차이가 있습니다)

ES2015 Class

객체지향 언어에서 주로 사용하는 클래스와 거의 같지만, 함수를 이용한 prototype 기반 상속의 문법 입니다. 자바스크립트에는 OOP 클래스가 필요없다는 주장도 간간히 나오지만, 상속을 통해 다형성을 표현해야 하는 로직일 경우 유용하며 가독성을 상승시킵니다. 단 내부에서는 프로토타입 기반 상속이 이루어지므로, 프로토타입이 변경될 가능성이 있는 베이스 클래스에 대한 상속을 피하는 것이 좋겠습니다. 'composition over inheritance'의 교훈은 자바스크립트에서도 다를 것이 없습니다만, 파이썬과 같이 언어 차원에서 제공하는 믹스인 같은 것은 없으므로 데코레이터나 별도의 프로토타입을 확장하는 로직을 작성해야 합니다.

유의해야 할 점은, 멤버 변수는 constructor() 생성자 함수 안에서 선언해야 한다는 점입니다. 즉 멤버 변수 foo를 선언하고 싶으면 this.foo = value 와 같이 선언해줘야 합니다. 이 때문에 ES7 Class Properties와 같은 제안이 나와 있습니다.

ECMAScript 클래스는 단계적으로 발전해 나가기로 한 바, 현재의 클래스의 기능은 다른 OOP 기반 언어에 비해서는 상당히 제한적입니다. 클래스에서 변수를 선언할 수 없다든지, private/protected 등의 접근 제한자가 없다든지, 인터페이스가 없다든지 등, 상속 외에는 거의 기능이 없지만 그래도 유용합니다.

React에서는 0.13.x부터 ES2015 클래스를 적극적으로 도입하기 시작하여, 많은 라이브러리들이 클래스로 짜여져 있습니다. 단, React.Component를 상속하는 클래스를 또 상속하는 것은 문제가 될 수 있으며, React 팀에서 의도한 사용 방식이 아니기 때문에 삼가합시다.

Module

ES2015 Module은 CommonJS module과 기능은 거의 유사합니다만, 두 가지의 차이가 있습니다.

  1. default export기능이 있다는 점
  2. 모듈 탑 레벨 스코프에서만 importexport 선언을 해야 한다는 점

Default export는 default export var Foo = React.createClass({ ... }); 와 같이 선언하면, import Foo from './components/foo'와 같이 기본 임포트가 가능합니다. 트랜스파일할 경우, module.exportsdefault프로퍼티로 접근할 수 있습니다.

두번째는, CommonJS의 require()처럼 함수 안에서 import를 한다거나 하는 것은 불가능하며, 모듈의 탑 레벨에 노출되어 있어야 하며 그렇지 않을 경우 사용할 수 없습니다.

React에서는 React가 default export이기는 하나, React.Components 등을 named import로 가져오는 것도 가능합니다.

더 자세한 것은 문법 명세에 대해 잘 정리되어 있는 2ality 블로그 글을 참고하시기 바랍니다.

Object Literal

var foo = 1;
var bar = 2;

var baz = {foo, bar}; // === {foo: 1, bar: 2}

기존의 오브젝트 리터럴처럼 키/값을 모두 쓰지 않고 키만 써 줘도, 스코프에 있는 바인딩을 참조합니다.

Rest/Spread Statement

function fun(a, b, ...rest) { ... }
fun(1, 2, 3, 4) // => rest: [3, 4]

Spread 오퍼레이터는 남은 인자들을 지정된 변수에 할당합니다.

(제안 단계) 데코레이터

@depreactedClass
class foo {
    hellllo () {
        return 'hellllo';
    }
}

function depreactedClass (fn) {
    console.log('Class ' + fn.name + ' is deprecated. Do not use!');
}

데코레이터는 파이썬의 데코레이터와 흡사히며, 함수를 받아 함수를 반환하는 표현식입니다. 클래스와 함수, 메서드에 대해서 데코레이터를 사용할 수 있습니다. 자세한 내용은 스펙을 참조합시다.

(제안 단계) async-await

function sleep(ms) {
    console.log('sleep()');
    return new Promise(r => setTimeout(r, ms));
}

async () => {
    console.log('a');
    await sleep(1000);
    console.log('b');
}(); // a / sleep / b

async-await은 Promise를 이용한 비동기 처리에서 더 나아가, 비동기 함수를 동기적인 방식으로 작성할 수 있게 해주는 문법입니다. 위의 예를 보면 비동기 함수임에도 불구하고 동기적으로 불려지는 것을 볼 수 있습니다. Babel과 같은 트랜스파일러를 사용하면 사용할 수 있긴 하나, 제너레이터 등 여러가지의 최신 기능을 동시에 활용하므로 모든 기능을 트랜스파일하면 문제가 발생했을 시 디버깅이 어려울 수 있는 단점이 있습니다.

더 읽을거리

더 많은 제안 단계 언어 명세: tc39/ecma262 ECMAScript 2015 언어 명세에 대한 요약: lukehoban/es6features

Tags : javascript react es2015 es6 es7 

comments powered by Disqus