SSR 서버사이드렌더링 구현하려고 했더니 바벨을 마주 안할 수가 없네요..ㅎ ㅎ %^~ㄴㅇㄹ~@#$~!! 예상 외의 스터디가 되었습니다.
바벨 -> 웹팩 -> SSR 순으로 공부를 하면 좋을 것 같다는 생각이 드네요. 이전에 스터디에서 바벨에 대해서 듣기만 했는데, 역시 제 스스로의 것으로 만드는 건 그냥 발표를 듣는거랑 직접해보는 거랑은 차이가 있는 것 같습니다..
● 바벨이란 무엇인가?
바벨은 브라우저에서 ES6이상의 문법들을 사용하기 위해서 이전 버전의 자바스크립트 코드로 변환해주는 도구입니다. (트랜스파일러)
브라우저마다 ES6의 문법의 호환성이 각각 달라서 크로스브라우징 이슈가 생겨나곤 했는데요, 이를 해결하기 위해 나타난 것이 바벨입니다.
● 트랜스파일러 VS 컴파일러
우리는 컴퓨터는 0과 1만을 이해할 수 있다는 사실을 알고 있습니다. 그런데 컴퓨터는 어떻게 우리가 적은 코드들을 이해하고 작동하는걸까요? 이는 컴파일러 때문입니다. 우리가 적는 코드들은 사람이 이해하기 쉽도록 추상화가 많이 되어있다는 의미를 담아 고급 프로그래밍 언어라고 합니다. 하지만 우리가 적은 코드를 컴퓨터가 이해하기 쉽도록 만들어주어야하는데요, 우리가 작성한 코드를 컴퓨터가 이해할 수 있도록 하기 위해서는 컴퓨터가 이해하는 기계어와 1 : 1로 대응하는 저급 프로그래밍 언어(어셈블리어라고 부릅니다.)로 변역하는 과정이 필요합니다. 즉, 고급프로그래밍언어를 어셈블리어로 변환시켜주는 과정이 필요한 것이죠.
우리는 프로그래밍의 영역에서 이러한 언어 간의 번역(변환)을 담당하는 번역가를 컴파일러라고 부릅니다.
그렇다면 트랜스파일러는 무엇일까요?
위에서 언급했던, 고급 프로그래밍 언어를 저급 프로그래밍 언어로 변환해 주는 컴파일러가 영어를 완전히 다른 언어인 한국어로 번역해 주는 번역가라고 한다면, 트랜스파일러는 옛 우리말을 현대 한국어로 번역해 주는 번역가라고 할 수 있겠습니다.
바벨이라는 툴이 트랜스파일러인 이유는, 최신 자바스크립트 문법으로 작성된 코드를 구버전 브라우저도 이해할 수 있는 수준의 오래된 자바스크립트 코드로 변환해 주는 소프트웨어이기 때문입니다. 즉, 바벨은 트랜스파일러로써 유사한 두 언어 사이에서의 변환 기능을 제공해줍니다.
보통 자바스크립트 최신 문법이라고 하면 ES6(ECMAScript 2015)를 기준으로 잡습니다. ES6 표준에서 자바스크립트의 주요한 신규 기능들이 워낙 많이 추가돼서, 이 기능들을 제공하지 않는 브라우저를 구버전 브라우저로 간주하기 때문인데요. 만약 바벨이라는 빌드 툴이 없었다면, 구버전 브라우저 (대표적으로 인터넷 익스플로러)에서도 서비스를 정상적으로 제공하기 위해서는 ES6 이전의 자바스크립트 문법만으로 코드를 작성해야 했을 것입니다. 이는 당연히 생산성을 매우 떨어뜨리는 일입니다. 당장 var 키워드를 사용하거나 비동기 코드를 프로미스 없이 짤 생각을 하니 정신이 혼미해지네요.
이때 구원자로 등장하는 게 바로 바벨입니다. 바벨은 ES6 문법을 활용해 작성한 자바스크립트 코드를 이전 버전의 자바스크립트 코드로 변환해 줌으로써, 우리가 만든 서비스가 구버전 브라우저에서도 의도한 대로 동작하도록 만들어줍니다.
● 바벨의 빌드 단계
바벨은 총 3단계의 빌드 과정을 거칩니다.
- 파싱 parsing
- 변환 transforming
- 출력 printing
코드를 읽고 추상 구문 트리(AST)로 변환하는 단계를 "파싱"이라고 합니다. 이것은 빌드 작업을 처리하기에 적합한 자료구조인데 컴파일러 이론에 사용되는 개념입니다. 추상 구문 트리를 변경하는 것이 "변환" 단계입니다. 실제로 코드를 변경하는 작업을 한다. 변경된 결과물을 "출력" 하는 것을 마지막으로 바벨은 작업을 완료합니다.
요즘 자바스크립트 프로젝트를 하다보면, devDependencies에 정말 많은 의존성이 있음을 알 수 있습니다. 자바스크립트 트랜스파일링, 코드 최소화, CSS pre-processor, eslint, prettier 등등등. 이러한 기능들은 실제 프로덕션 코드로 올라가는 것은 아니지만, 개발 과정에서 중요한 것들을 담당합니다. 그리고 이러한 툴들은 AST processing 을 기반으로 작동합니다. 여기서 AST란 무엇일까요?
컴퓨터 과학에서 추상 구문 트리(abstract syntax tree, AST), 또는 간단히 구문 트리(syntax tree)는 프로그래밍 언어로 작성된 소스 코드의 추상 구문 구조의 트리이다. 이 트리의 각 노드는 소스 코드에서 발생되는 구조를 나타낸다. 구문이 추상적이라는 의미는 실제 구문에서 나타나는 모든 세세한 정보를 나타내지는 않는다는 것을 의미한다. 예를 들어, 그룹핑을 위한 괄호는 암시적으로 트리 구조를 가지며, 분리된 노드로 표현되지는 않는다. 마찬가지로, if-condition-then 표현식과 같은 구문 구조는 3개의 가지에 1개의 노드가 달린 구조로 표기된다.
정말 뭔 소린지 모르겠죠?
예시를 보면 이해하기가 쉽습니다.
function square(n) {
return n * n
}
이런 코드를 AST로 변환한다면,
{
"type": "Program",
"start": 0,
"end": 36,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [0, 36],
"errors": [],
"comments": [],
"sourceType": "module",
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 36,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [0, 36],
"id": {
"type": "Identifier",
"start": 9,
"end": 15,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 15
},
"identifierName": "square"
},
"range": [9, 15],
"name": "square",
"_babelType": "Identifier"
},
"generator": false,
"async": false,
"expression": false,
"params": [
{
"type": "Identifier",
"start": 16,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 17
},
"identifierName": "n"
},
"range": [16, 17],
"name": "n",
"_babelType": "Identifier"
}
],
"body": {
"type": "BlockStatement",
"start": 18,
"end": 36,
"loc": {
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 3,
"column": 1
}
},
"range": [18, 36],
"body": [
{
"type": "ReturnStatement",
"start": 22,
"end": 34,
"loc": {
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 14
}
},
"range": [22, 34],
"argument": {
"type": "BinaryExpression",
"start": 29,
"end": 34,
"loc": {
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 14
}
},
"range": [29, 34],
"left": {
"type": "Identifier",
"start": 29,
"end": 30,
"loc": {
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 10
},
"identifierName": "n"
},
"range": [29, 30],
"name": "n",
"_babelType": "Identifier"
},
"operator": "*",
"right": {
"type": "Identifier",
"start": 33,
"end": 34,
"loc": {
"start": {
"line": 2,
"column": 13
},
"end": {
"line": 2,
"column": 14
},
"identifierName": "n"
},
"range": [33, 34],
"name": "n",
"_babelType": "Identifier"
},
"_babelType": "BinaryExpression"
},
"_babelType": "ReturnStatement"
}
],
"_babelType": "BlockStatement"
},
"_babelType": "FunctionDeclaration"
}
]
}
ㅎ_ㅎ 그만알아보도록하자. 아무튼 AST는 코드들을 트리구조의 데이터로 만드는 것인데, 바벨을 사용하면 알아서 이런 파싱작업을하고 다음으로 다시 코드들로 변환을 해준뒤, 출력해주는 작업을 해주는 것입니다.
패키지/플러그인 | 패키지 이름 | 설명 |
Babel | @babel/cli | 터미널로 바벨을 사용할 수 있게 한다. |
Babel | @bable/core | 바벨의 핵심적인 기능 (파싱과 출력을 담당) |
Babel 프리셋 | @babel/preset-env | 프리셋은 다양한 플러그인을 모아둔 것이다. (변환을 담당) |
바벨은 파싱과 출력만 담당하고 변환 작업은 다른 얘가 처리를 해주는데, 이것을 "플러그인" 이라고 부릅니다. 바벨에는 다양한 플러그인이 있습니다. 하지만 각각 필요한 플러그인을 찾아서 일일이 적용하는 것은 번거롭다. 따라서 등장한 것이 프리셋입니다.
따라서 쉽게 그림으로 비유를 들자면
이런 느낌인 것 같습니다. 프리셋에 따라서 출력되는 코드가 달라지는 것이죠.
Babel이 제공하는 공식 Babel 프리셋은 다음과 같습니다.
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
preset-env는 ECMAScript2015+를 변환할 때 사용합니다. 바벨 7 이전 버전에는 연도별로 각 프리셋을 제공했지만(babel-reset-es2015, babel-reset-es2016, babel-reset-es2017, babel-reset-latest) 지금은 env 하나로 합쳐졌습니다.
preset-flow, preset-react, preset-typescript는 flow, 리액트, 타입스크립트를 변환하기 위한 프리셋입니다.
플러그인은 또 뭔가 싶을 수 있는데, 플러그인 같은 경우에는 하나씩 설정해줄 수 있는거라고 생각하면 됩니다. 예를 들어서 화살표 함수를 쓴걸 이전 문법과 호환을 해주고 싶다면, 화살표 함수를 지원해주는 플러그인을 설치해주면됩니다.
하지만 우리는 매번 모든 문법마다 지원해주려고 이렇게 플러그인을 깔아줘야할까요? 넘 귀찮죠? 그래서 지원하는 것이 프리셋입니다. 즉, 플러그인들을 모아준게 프리셋이라고 봐야겠죠?
● polyfill 폴리필
폴리필이란 충전솜을 이야기합니다. 부족한 솜을 채운다라는 의미가 있는데요, 바벨에서도 폴리필이란 바벨의 부족한 어떠한 부분을 채워주는 것이라고 생각해볼 수 있겠습니다.
그럼 어떠한 부족한 점을 채워주는 것일까요?
A polyfill is a piece of code(usually JavaScript on the Web) used to provides
modern functionality on older browsers that do not natively support it.
즉, 구현 브라우저에서 지원하지 않는 최신 기능을 가져와 사용하는 코드 뭉치
바벨은 프리셋과, 플러그인을 통해서 트랜스파일링을 해주는데요, 그렇다고 모든 기능들을 사용할 수 있는 것이 아닙니다. 예를 들어 프로미스같은 빌트인 객체들은 최신문법이기 때문에 바벨이 트랜스파일링 해줄 수 없습니다. 왜냐하면 이전 ECMA Script 문법에는 아예없던 존재였고 새로운 문법이기 때문에, 최신문법을 이전문법으로 트랜스파일링해준다고해도 없던걸 만들어 줄 순 없기 때문이죠.
이때 필요한 것이 폴리필입니다. 즉 트랜스파일링하는 과정 속에서 이전 버전에 없던 것을 만들어주어야하기때문에 솜뭉치처럼 채워넣어주는 것입니다. 부족한 코드들을 채워주는 것이겠죠?
대충 이론적인 부분은 이렇습니다. 실제로 실습을 해봐야겠죠?
● 바벨 실습해보기
❍ 환경 설정하기
저는 먼저 아무 디렉토리를 하나 만들고 npm init -y 을 해주었습니다.
다음 바벨 코어와, 바벨 cli 을 설치해줍니다.
npm install -D @babel/core @babel/cli
디렉토리 안에 app.js 파일을 만들고, 아래 코드를 적어 저장합니다.
const alert = msg => window.alert(msg)
터미널에 아래와 같은 명령어를 적으면 바벨을 터미널 환경에서 실행시킬 수 있습니다.
npx babel app.js
const alert = msg => window.alert(msg);
하지만 아직 변환된게 아무것도 없죠? 왜냐면 프리셋이나 플러그인을 설치하지 않았기 때문입니다.
❍ 플러그인 설치하기
아래와 같이 block-scoping이라는 플러그인을 설치해줍니다.
npm install -D @babel/plugin-transform-block-scoping
다음으로 이 플러그인을 실행해주면?
npx babel app.js --plugins @babel/plugin-transform-block-scoping
var alert = msg => window.alert(msg);
var이라는 블록스코프로 변수명이 변경된걸 확인 할 수 있습니다.
하지만 플러그인들을 이렇게 다운로드받고 실행할 때마다 저 플러그인을 터미널에 적어주어야하는데 넘 귀찮죠?
그전에 커맨드라인 명령어가 점점 길어지기 때문에 설정 파일로 분리하는 것이 좋습니다. 웹팩 webpack.config.js를 기본 설정파일로 사용하듯 바벨도 babel.config.js를 사용합니다.
babel.config.js
module.exports = {
plugins : [
"@babel/plugin-transform-block-scoping",
]
}
이런식으로 plugins 를 배열로 열고, 배열안에 플러그인들을 넣어사용할 수 있습니다.
그러고 바로 npx babel app.js 하면 알아서 잘적용이됨.. 죤쉰기하죠?
❍ 프리셋 설치하기
npm install -D @babel/preset-env
먼저 env 프리셋을 설치해줍니다.
babel.config.js
module.exports = {
presets : ["@babel/preset-env"],
}
presets에 깔아준 프리셋을 적어준다.
그리고 빌드하면,
npx babel app.js
"use strict";
var alert = function alert(msg) {
return window.alert(msg);
};
와 신기하졈?
❍ env 설정
코드가 크롬 최신 버전(2019년 12월 기준)만 지원한다고 해보겠습니다. 그렇다면 인터넷 익스플로러를 위한 코드는 필요가 없죠? 따라서 target옵션에 브라우저명을 지정만하면 env 프리셋은 이에 맞는 플러그인을 찾아 최적의 코드를 출력해냅니다.
babel.config.js
module.exports = {
presets : [
[ "@babel/preset-env",
{
targets : {
chrome : "78"
}
}
]
]
}
그러면 아래와같이
npx babel app.js
"use strict";
const alert = msg => window.alert(msg);
❍ 폴리필 설정
먼저 이전 자바스크립트에서 지원하지 않는 promise를 활용해서 app.js
app.js
const a = new Promise();
바벨로 처리하면 어떤 결과가 나올까?
npx babel app.js
"use strict";
const a = new Promise();
바벨로 처리를 해도 똑같은걸 볼 수 있습니다. 왜냐면? 이전 문법에선 없는 문법이니까 교체해줄수있는 그런게 없어서겠죠.
플러그인이 프로미스를 ECMAScript5 버전으로 변환할 것으로 기대했는데 예상과 다릅니다. 바벨은 ECMAScript2015+를 ECMAScript5 버전으로 변환할 수 있는 것만 빌드하기때문이죠. 그렇지 못한 것들은 "폴리필"이라고부르는 코드조각을 추가해서 해결합니다.
가령 ECMAScript2015의 블록 스코핑은 ECMASCript5의 함수 스코핑으로 대체할 수 있습니다. 화살표 함수도 일반 함수로 대체할 수 있습니다. 이런 것들은 바벨이 변환해서 ECMAScript5 버전으로 결과물을 만듭니다.
하지만 프라미스는 ECMAScript5 버전으로 대체할 수 없습니다. 다만 ECMAScript5 버전으로 구현할 수는 있습니다.
따라서 아래와 같이 폴리필을 설정해주어야합니다.
babel.config.js
module.exports = {
presets : [
[ "@babel/preset-env",
{
useBuiltIns : "usage", // 폴리필 사용 방식 지정
corejs :{ // 폴리필 버전 지정
version : 2,
}
}
]
]
}
useBuiltIns는 어떤 방식으로 폴리필을 사용할지 설정하는 옵션입니다. "usage" , "entry", false 세 가지 값을 사용하는데 기본값이 false 이므로 폴리필이 동작하지 않았던 것입니다. 반면 usage나 entry를 설정하면 폴리필 패키지 중 core-js를 모듈로 가져옵니다(이전에 사용하던 babel/polyfile은 바벨 7.4.0부터 사용하지 않음).
useBuiltIns는 세 가지 가능한 값을 허용합니다: "항목", "사용" 및 false. useBuiltIns를 "entry"로 설정하면 코드에서 실제로 사용되는지 여부에 관계없이 대상 환경에서 지원되지 않는 모든 기능에 대한 모든 폴리필이 포함됩니다.
반면, useBuiltIns가 "usage"으로 설정된 경우, Babel은 코드에서 실제로 사용되는 기능에 대한 폴리필만 포함합니다. 즉, 바벨은 코드를 분석하여 사용된 기능에 따라 어떤 폴리필이 필요한지 결정합니다.
"entry"과 "usage"의 주요 차이점은 "entry"은 사용되지 않더라도 항상 모든 폴리필을 포함하지만 "usage"은 코드에서 실제로 사용되는 기능에 따라 필요한 폴리필만 포함한다는 것입니다.
일반적으로 "usage"은 필요한 폴리필만 포함함으로써 번들 크기를 줄이고 로드 시간을 단축할 수 있으므로 일반적으로 선호되는 옵션입니다. 그러나 특정 폴리필을 사용할 수 있는 타사 라이브러리를 사용하는 경우에는 'entry'이 필요할 수도 있습니다.
corejs 모듈의 버전도 명시하는데 현재(2023년 04월 기준) 기본값은 3입니다.
그러고 나서 다시 빌드해주면?
npx babel app.js
"use strict";
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
var a = new Promise();
core-js 패키지로부터 프로미스 모듈을 가져오는 임포트 구문이 상단에 추가되었습니다.
● 웹팩으로 통합하기
이제 바벨을 통해서 트랜파일링이 잘되는 것을 확인했습니다. 그렇다면 이렇게 트랜스파일링된 코드들로 수정하고 싶다면 어떻게 해야할까요? 이건 babel-loader(바벨로더)가 해줍니다.
// webpack.config.js:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader", // 바벨 로더를 추가한다
},
],
},
}
위의 예시는 웹팩을 깔고, 웹팩 설정을 할 때 넣어주는 코드입니다.
test는 .js로 끝나는 파일을 바벨로더가 처리하겠다 라는 의미입니다. (정규식활용)
exclude (제외하겠다) 라는 말입니다. 사용하는 써드파티 라이브러리가 많을수록 바벨 로더가 느리게 동작할 수 있는데 node_modules 폴더를 로더가 처리하지 않도록 예외 처리했습니다.
폴리필 사용 설정을 했다면 core-js도 설치해야합니다. 웹팩은 바벨 로더가 만든 아래 코드를 만나면 core-js를 찾을 것이기 때문입니다.
따라서 core-js를 깔아줍니다.
npm i core-js@3
그리고 웹팩으로 빌드하면, 완성입니다 :)
npm run build
즉, babel-loader로 웹팩과 함께 사용하면 훨씬 단순하고 자동화된 프론트엔드 개발환경을 갖출 수 있습니다.