요즘 잘나가는 프론트엔드 개발환경 만들기(2018): ES6

본 시리즈의 이전 아티클에서는 웹팩4에 대해 다루면서 바로 쓸 수 있는 환경을 만들어봤다. 이번편에는 그 환경 그대로 이용해 ES6 개발 환경을 추가한다. ES6는 이제 충분히 써도 될만한 시기라고 생각한다. “나는 프런트 개발자가 아니기도 하고 브라우저가 모두 지원하지도 않기에 ES6는 아직 못쓴다” 라고 생각하고 있다면 이 글을 통해 생각을 바꿀 수 있길 바란다. 어차피 쓰는 거 ES6뿐 아니라 ES8까지 사용하는 것을 권장한다. IE11 이하 버전들은 ES6나 이후에 대한 고려가 거의 없지만 엣지(MS Edge)를 포함한 모든 모던 브라우저들은 네이티브로 ES8(ECMAScript2017)의 대부분의 스펙을 지원한다.(실용적인 사용에 있어서는 100%라고 봐도 된다) 모던 브라우저들은 이렇게 열심히 스펙을 따라가 주고 있다. 프로젝트의 지원 대상 브라우저가 IE11 이하 버전을 포함하고 있다면 그때는 우리의 무기 바벨을 사용할 때이다. 바벨은 ES6+ 버전을 ES5로 바꿔주는 트랜스파일러다. 프로젝트의 코드베이스는 ES6로 가독성 있고 간결하게 유지하고 실제로 브라우저에서 사용하는 코드들은 바벨로 트랜스파일 된 ES5 코드를 사용하는 것이다. 필요하면 트렌스파일 과정에서 여러 가지 최적화까지 적용할 수도 있다. 소스맵 덕에 브라우저상에서의 디버깅도 전혀 문제없다. 이미 어마어마하게 많은 수의 프로젝트들이 바벨을 이용해 아무 문제없이 ES6코드로 개발되고 있다. 물론 트랜스파일이란 과정이 잠재적인 오류를 발생할 수도 있겠지만 그런 경우는 매우 희박하다 아니 아마 그런 일을 겪을 가능성은 거의 없다고 말하는게 좋겠다. 이런 걱정으로 ES6 도입을 망설이고 있다면 지금 바로 라잇 나우 시작하길 바란다.

바벨

앞서 소개한 바와 같이 바벨은 특정 버전의 ECMAScript 코드를 하위 버전의 ECMAScript로 변환해주는 트랜스파일러다. 커피스크립트를 사용해봤던 사람이라면 트랜스파일러에 익숙할 것이다. 다만 바벨은 커피스크립트가 아닌 ECMAScript 표준을 따른다. 그 외 특별한 기능은 없기에 설정이나 사용법은 간단한 편이다. 하나씩 해보자

npm install --save-dev babel-cli

일단은 babel-cli 인스톨을 한다. 이전 아티클을 따라서 해봤다면 만들던 프로젝트에서 이어서 진행하는 것을 권장한다. babel-cli 설치로 일단 프로젝트 내에서 바벨을 실행시킬 최소한의 환경은 구축한 셈이다.

    // es6test.js
    const myconst = 123;
    let mylet = 456;

위와 같이 파일을 하나 만들고 터미널에서 아래와 같이 실행하면

npx babel ./es6test.js

바벨이 원본 코드를 ES5로 변환한 결과를 터미널에 뿌려준다. 아래와 같이 나온다.

img

변환 된다고 했는데 전혀 변환이 되지 않고 원본 코드 그대로 뿌려준다. 나한테 왜 이럴까? babel-cli 는 트랜스파일을 진행해주는 코어 기능만 있고 실제 코드를 변환할때는 기능별로 확장된 플러그인들이 필요하다. 쉽게 말해 애로우 펑션을 트랜스파일하려면 애로우 펑션을 트랜스파일할 수 있는 플러그인(transform plugin)을 추가로 설치해야한다. 하지만 ES6 만 해도 추가된 기능이 적지 않은데 기능별로 플러그인들을 개별적으로 설치하려면 귀찮은 일이 아닐 수 없다. 이런 불편함은 프리셋을 설치해서 해결할 수 있다. 프리셋은 버전별로 필요 플러그인들을 모아놓은 셋트라고 생각하면 된다. 매해 나오는 ECMAScript 버전에 맞게 플러그인과 프리셋이 계속 추가 개발되고 있다.

npm install --save-dev babel-preset-env

babel-preset-env 은 그런 프리셋과 플러그인들을 모와 관리하고 있는 모듈이다. 예전에는 버전 별로 따로 프리셋 모듈이 존재해 사용할 버전의 프리셋을 개별 설치해야 했는데 이제 그런 프리셋들은 모두 디프리케이트되었다. babel-preset-env 를 설치하고 다시 바벨을 실행해도 ES5로 바뀌지 않은 원본 그대로 출력된다. 프리셋을 설치했다면 어떤 프리셋을 사용하는지 옵션으로 전달해야 한다. 프리셋을 전달하는 옵션은 --presets 다. 이번에는 화면에 뿌리지 말고 트랜스파일 된 파일로 저장하도록 --out-file 옵션도 사용한다.

npx babel ./es6test.js --out-file es5.js --presets=es2015

es5.js 파일을 열어보면 아래와 같이 ES5 코드로 변경되었음을 알 수 있다.

    // es5.js
    "use strict";
    
    var myconst = 123;
    var mylet = 456;

"use strict" 가 추가되었다. ES6의 모듈 코드는 스펙상 언제나 스트릭트모드로 동작한다. ES5로 변환되었기 때문에 "use strict" 가 추가된 것이다. 반대로 ES6 코드에서 "use strict" 를 사용하는 것은 옳은 사용이 아니다. 정적 분석 도구가 지적질할 수도 있다. 스트릭트모드의 추가 외에 const와 let 키워드가 var 키워드로 변경되었다. 사용된 ES6 코드는 const와 let뿐이고 간단하게 사용된 코드라 전체적으로 변화가 거의 없다. var로만 변경될 뿐이면 const와 let의 스펙이 충실히 구현되지 않는것 같다. 이번에는 const 변수의 값을 변경하는 코드를 추가하고 다시 동일한 옵션으로 바벨을 실행해보자.

    const myconst = 123;
    let mylet = 456;

    myconst = 555;

img

위와 같이 에러 메세지를 출력하면서 트랜스파일이 되지 않는다. 이렇게 바벨은 스펙에 어긋나게 사용되는 코드들을 트랜스파일 단계에서 발견할 수 있게 해준다. 또 다른 경우를 보자.

    // es6test.js
    const myconst = 123;
    let mylet = 456;
    
    if (myconst) {
      let mylet = 1;
      mylet += 1;
    }

let은 블록 스코프다 그래서 if문 블록의 mylet과 블록 밖의 mylet은 물리적으로 다른 변수고 서로 영향을 받지 않아야 한다. 즉 if 문을 빠져나오면 mylet의 값은 여전히 456여야 한다. var 키워드로 변수를 선언하던 ES5는 함수 스코프라 변수 mylet의 값은 2가 된다. 바벨은 이 코드를 어떻게 ES5로 바꿀까? 다시 바벨을 실행해보자.

    // es5.js
    "use strict";
    
    var myconst = 123;
    var mylet = 456;
    
    if (myconst) {
      var _mylet = 1;
      _mylet += 1;
    }

아예 변수명을 바꿔 버려서 블럭스코프와 동일한 효과를 낸다. 이런 식으로 대상 코드의 사용 내용에 따라 스펙대로 동일하게 동작할 수 있게 ES5로 바꿔준다. const와 let뿐 아니라 같은 기능이라도 사용하는 방법에 따라 변경되는 코드의 내용이 효율적으로 관리된다. 가끔 궁금해서 트랜스파일되어 ES5로 변경된 파일의 코드를 살펴보다보면 각종 꼼수와 구현에 감탄하게 된다.

babelrc

바벨은 .babelrc 라는 파일명으로 프로젝트의 바벨 관련 설정을 등록할 수 있고 package.json 에서 babel 이란 키로 설정을 추가할 수도 있지만 .babelrc 를 사용하는것을 선호한다. babel-cli 를 실행하면서 매번 커맨드라인으로 옵션을 줄 수 있지만 바벨에 사용되는 옵션은 보통 프로젝트 별로 정해져있기 때문에 관리와 환경 공유를 위해 설정 파일에 저장한다. 커맨드라인 옵션으로 사용할 수 있는 대부분의 내용은 설정 파일로 저장할 수 있다. 사실 웹팩과 연계하면 커맨드라인에서 바벨을 실행할 일도 없다. 앞서 다룬 커맨드라인 옵션은 간단하게 아래와 같이 설정할 수 있다.

    {
      "presets": ["es2015"]
    }

트랜스파일할 대상과 결과 파일을 저장하는 옵션 --out-file 은 설정 파일에 저장할 수 없다. 사용할 수 있는 바벨 옵션에 대한 설명은 API페이지의 Options파트 에서 확인할 수 있는데 버전7의 준비때문인지 old라는 서브도메인 붙은 링크만 유효하고 현재 공식페이지의 링크는 깨져있다.(여기) 어차피 사용할 ECMAScript 스펙에 대한 설정 정도만 .babelrc 에 저장하고 나머지는 웹팩에 맡기기 때문에 특별히 설정을 복잡하게 할 일도 없다.

    {
      "presets": ["env"]
    }

프리셋을 "env" 로 설정하면 babel-preset-latest 라고 불리는 현재 지원 가능한 가장 최신 버전의 프리셋을 사용하고 추가로 프로젝트의 지원 브라우저를 기반으로 폴리필과 필요 트랜스폼 플러그인들을 관리할 수 있는 옵션들을 사용할 수 있다. 바벨을 통한 최신 ECMAScript는 항상 최신 버전을 사용하게 되는 경우가 많아 명시적으로 스펙을 설정하기 보다 항상 최신 버전의 스펙을 사용하면서 대상 브라우저에 따라 트랜스파일된 코드가 돌아가기 위한 런타임 코드를 최적화할 수 있는 옵션들로 발전했다.

    {
      "presets": [
        ["env", {
          "targets": {
            "chrome": 52
          }
        }]
      ]
    }

위와 같이 타겟에 옵션으로 지원 브라우저의 버전을 정의할 수 있다. 모던 브라우저 같은 경우는 특정 버전을 기준으로 지원하는 정책보다는 최신 버전을 지원하거나 최신 버전에서 몇 버전 전까지를 지원하는 정책이 더 유용하다. 스트링 쿼리 형태로 지원 브라우저를 정의할 수 있는 browserslist 를 바벨이 사용하고 있기 때문에 “최근 2개의 버전까지”와 같은 형태로 대상 브라우저를 정의할 수 있다.

최근 2가지의 버전만 지원하면서 IE의 경우 10버전 이하는 제외하는 설정은 아래와 같다.

    {
      "presets": [
        ["env", {
          "targets": {
            "browsers": ["last 2 versions", "not ie <= 10"]
          }
        }]
      ]
    }

배열 형태로 여러 가지 조건을 조합해서 설정할 수 있다. 재미있는 건 글로벌한 브라우저 사용 통계를 기반으로도 설정할 수가 있는 옵션이 있다. 예를 들면 “전 세계 사용량이 5% 이상인 브라우저만 지원” 이렇게 설정을 할 수 있는데 이건 너무 불명확해서 개인적으로는 추천하고 싶지가 않다. 내 프로젝트가 어떤 버전의 브라우저를 지원하는지 명확하지 않게 된다. browserslist 에서 사용 가능한 쿼리 목록은 깃헙페이지 에서 확인할 수 있다. 하지만 대부분 위의 예제로 사용한 내용만으로 충분할것이다.

그리고 아직 정식 버전에는 포함되어 있지 않은 새로운 스펙들은 plugins 배열로 추가할 수 있다.

    {
      "presets": [
        ["env", {
           "targets": {
            "browsers": ["last 2 versions", "not ie <= 9"]
          }
        }]
      ],
      "plugins": ["transform-object-rest-spread"]
    }

object-rest-spreadRest/Spread Properties 라는 이름으로 Stage-4 단계에 있는 스펙으로 ES2018에 추가될 예정이다. 설정만해서는 당연히 동작하지 않고 플러그인을 추가 설치 해줘야 한다.

npm i babel-plugin-transform-object-rest-spread --save-dev

이렇게 아직 정식 버전에 포함되지 않은 개발 중인 스펙은 플로그인 형태로 개별 추가해 해당 기능을 사용할 수 있지만 Stage-3 이상의 스펙들만 사용하는 것을 추천한다.

사용할 수 있는 플러그인 목록들은 여기에서 확인할 수 있다. 이미 정식 버전에 포함된 스펙들은 프리셋으로 모아서 적용 가능하기 때문에 따로 따로 플러그인으로 사용할 일은 없다. 아직 정식 버전에 포함되지 않은 스펙의 플러그인들은 Experimental 파트에 정리되어 있다.

웹팩 연동

.babelrc 의 설정이 제대로 잡혀있다면 웹팩과의 연동은 어렵지 않다. 우선 웹팩에서 바벨을 연동할 수 있게 babel-loader 를 설치 한다.

npm i babel-loader --save-dev

그리고 module.rules 옵션에 로더를 추가 한다.

    module.exports = (env, options) => {
      const config = {
        entry: {
          app: ['./src/index.js']
        },
        //... 중략 ...
        module: {
          rules: [
            {
              test: /\.js$/,
              exclude: /node_modules/,
              use: {
                loader: 'babel-loader'
              }
            }
          ]
        }

간단하게 로더 설정에 대해 설명하면 test 는 해당 로더가 적용될 파일을 정의하고 exclude는 제외할 파일들을 정규식으로 정의한다. exclude/node_modules/ 로 정의했는데 이렇게 하면 /node_modules/ 디렉터리의 하위 내용은 모두 포함되지 않는다. rules 옵션은 특정 조건의 파일을 기준으로 적용되어야 할 로더들을 정의하기 때문에 rules 에 로더 하나씩 정의할 수도 있고 특정 파일들에 여러 개의 로더가 적용되는경우 중복해서 정의하지 않고 use 옵션을 배열로 넘긴다.

    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            { loader: 'style-loader' },
            { loader: 'css-loader'}
          ]
        }
      ]
    }

그리고 웹팩에 꼭 추가해야 할 설정이 babel-polyfill 을 추가하는 작업이다. 바벨 폴리필은 필요 ECMAScript 버전에 포함된 빌트인 객체나 메서드들을 추가해준다. 즉 특정 버전의 런타임 환경을 만들어 준다. 예를 들면 Promise 가 없는 브라우저 환경에 Promise 를 만들어준다. 바벨로 트랜스 파일 된 코드를 사용하고자 한다면 번들링된 파일을 로드하기 전에 브라우저에 먼저 로드되어야 한다. 사실 babel-polyfill 은 웹팩과는 상관없이 바벨을 위해 필요한 부분이지만 웹팩을 이용해 번들링해서 사용한다. 웹팩에서 디펜던시 코드들을 관리하는 부분은 프로젝트별로 상황에 맞게 다양하게 적용될 수 있는데 이전 아티클에서 소개했던 내용과 같이 디펜던스 코드는 따로 분리해서 번들링 하는 방식으로 처리하는 것이 일반적이다. 바벨 폴리필도 그 분리되는 파일에 포함시킨다. 사실 서비스 코드에서 바벨 폴리필을 직접 로드해주면 이전 아티클의 최종 웹팩 설정 내용에서 바뀔 부분이 없지만 바벨 폴리필은 서비스 코드에서 직접 임포트해서 사용할 이유가 없기 때문에 서비스 코드에서는 숨기고 웹팩 엔트리에 포함 시킨다.

    module.exports = (env, options) => {
      const config = {
        entry: {
          app: ['babel-polyfill', './src/index.js']
        },
        // .... 중략 ....
        optimization: {
          splitChunks: {
            cacheGroups: {
              commons: {
                test: /[\\/]node_modules[\\/]/,
                name: 'vendors',
                chunks: 'all'
              }
            }
          }
        }
      }
    }

위와 같이 설정하면 폴피필은 다른 프로젝트 디펜던스들과 함께 vendors.bunde.js 파일에 번들링된다. 이전 아티클의 설정 코드에서는 entry.app 배열에 'babel-polyfill' 을 추가한것이 전부다.

마무리

자 이제 ES6++로 개발할 수 있는 환경을 만들었다. ES6++의 사용법이 아직 익숙치 않다면 여기에서 코드들을 위주만 살펴봐도 어느 정도 사용법을 파악할 수 있고 상당히 유용한 기능들이 많다는 것을 알 수 있다. 자바스크립트의 단점을 개선하고자 나온 기능들이기도 하고 대부분의 기능이 어렵지 않은 내용들이라 프로젝트에 쉽고 유용하게 적용할 수 있다.