초보 웹 개발자를 위한 자바스크립트 빌드 툴과 SWC
안녕하세요. 카카오엔터테인먼트에서 카카오페이지 프론트엔드 개발을 하고 있는 빌입니다. 저희 팀은 최근에 기존 카카오페이지 프로젝트에 모노레포 기법을 도입했습니다. 모노레포를 처음 도입해 보는 것이다 보니 작업 도중 시행착오가 많았고, 기존 프로젝트 코드가 새롭게 구축한 모노레포 환경에서도 정상적으로 동작하는지를 확인하기 위해 수십 번의 테스트를 거쳐야 했습니다. 모노레포 구축에 대한 경험담을 풀어보는 것도 흥미롭겠지만, 저는 그 대신 모노레포 테스트 과정 중 시간을 가장 많이 앗아갔던 빌드(Build)에 초점을 맞춰보고자 합니다. 카카오페이지 웹사이트는 Next.js 프레임워크를 기반으로 개발되었습니다. Next.js는 프로젝트를 빌드하기 위해 많은 빌드 툴들을 사용하는데요. 그중 대표적인 빌드 툴들을 뽑아보자면 프론트엔트 개발을 해보셨다면 한 번쯤은 들어보셨을 바벨(Babel)과 Terser이 있습니다. (Webpack 도 대표적인 빌드 툴이지만 이 글에서 메인으로 다루지는 않습니다) 프로젝트의 규모가 커질수록 전체 프로젝트 코드가 바벨과 Terser와 같은 빌드 툴들을 거쳐서, 실제 프로덕션 코드로 변환되기까지 걸리는 시간이 기하급수적으로 늘어납니다. 실제로 제가 테스트를 위해 카카오페이지 프로젝트 코드를 한 번 빌드 할 때마다 5분~10분 정도의 시간이 걸렸습니다. 테스트 한 번 할 때마다 빌드 때문에 계속 막히니 답답해 미칠 지경이었죠. 카카오페이지의 프로젝트 빌드 속도를 빠르게 할 수 있는 방법을 찾던 중, Next.js 팀에서 작년 10월에 발표하고 Next.js 12 버전부터 정식으로 도입된 SWC 라는 툴에 관심을 가지게 되었습니다. Next.js 의 공식 웹사이트에서는 SWC로 구축된 새로운 컴파일러를 이용해 Next.js 프로젝트를 빌드하면, 기존과 비교했을 때 빌드 타임이 최대 5배까지 빨라질 수 있다고 소개하고 있습니다. 빌드 속도가 무려 5배라니, 개발자라면 그냥 넘어갈 수 없는 문구입니다. Next.js 팀이 발표한 벤치마킹 결과 - 출처 : Next.js 공식 블로그 이 글에서는 바벨과 Terser라는 빌드 툴에 대해 간단하게 알아보고, Next.js에서 도입한 SWC가 과연 무엇인지에 더불어 SWC가 왜 기존 빌드 툴들보다 더 빠를 수밖에 없는지에 대한 이유에 대해 알아보고자 합니다. 단순히 글로만 소개하는 것이 아니라 간단하게 따라 해볼 수 있는 예시를 준비했으므로, 이 글을 읽으시는 분들이 이 글에서 소개하는 빌드 툴들에 좀 더 익숙해질 수 있는 기회가 되셨으면 좋겠습니다. 이 글은 바벨이나 Terser, 그리고 SWC에 대해 아예 들어보신 적이 없거나, 직접 이 툴들을 다뤄보거나 알아볼 기회가 없었던 초보 웹 개발자분들을 기준으로 작성되었습니다. 따라서, 특정 도메인의 지식이 필요한 설명은 최대한 배제하고 개념만 파악하실 수 있는 정도로 단순하게 설명하는 데 초점을 맞췄습니다. 1. 바벨 (Babel) 바벨은 자바스크립트 개발자가 최신 자바스크립트 문법을 사용하는 데 없어서는 안 될 아주 중요한 빌드 툴입니다. 바벨의 역할을 좀 더 자세히 살펴보면, 바벨은 자바스크립트 빌드 툴 중 트랜스파일러(Transpiler) 라는 영역에 속한다는 것을 알 수 있습니다. 여기서 잠깐! 트랜스파일러가 대체 뭘까요? 1-1. 컴파일러 (Compiler) 트랜스파일러에 대해 이해하기 위해서는 우선 컴파일러가 무엇인지부터 알아야 합니다. 우리는 컴퓨터가 0과 1만을 이해할 수 있다는 사실을 알고 있습니다(이렇게 0과 1로만 작성된 언어를 기계어라고 합니다). 하지만 컴퓨터에서 동작할 어떤 프로그램을 구현할 때, 프로그래머는 0과 1로 프로그램을 작성하는 것이 아니라 자바스크립트나 C++와 같은 프로그래밍 언어를 사용해 프로그램을 작성합니다. 어떻게 이런 일이 가능한 걸까요? 대부분의 프로그래밍 언어는 영어와 같이 사람이 실제로 사용하는 언어를 기준으로, 사람이 이해하기 쉬운 단어와 문법을 통해 프로그램을 작성할 수 있도록 설계되어 있습니다. 이런 언어들을 사람이 이해하기 쉽도록 추상화가 많이(고수준으로) 되어있다는 의미를 담아 고급 프로그래밍 언어라고 부릅니다. 이러한 자바스크립트와 같은 고급 프로그래밍 언어로 짜인 코드를 컴퓨터에 바로 집어넣는다고 해서 컴퓨터가 우리의 코드를 이해하고 실행할 수 있는 것은 아닙니다. 컴퓨터에 우리가 작성한 코드를 이해시키기 위해서는, 컴퓨터가 이해하는 기계어와 1대 1로 대응되는 저급 프로그래밍 언어(어셈블리어라고 부릅니다)로 번역하는 과정이 필요합니다. 마치 영어를 모르는 사람을 위해 영어를 한국어로 번역해주는 것처럼 말이죠. 우리는 프로그래밍의 영역에서 이러한 언어 간의 번역(변환)을 담당하는 번역가를 컴파일러라고 부릅니다. 위에서는 고급 프로그래밍 언어를 저급 프로그래밍 언어로 변환해주는 예시만 들었지만, 실제로는 한 언어로 작성된 코드를 동일한 기능을 제공하는 다른 언어의 코드로 변환해주는 역할만 제공한다면 그 소프트웨어는 컴파일러라고 할 수 있습니다. 1-2. 트랜스파일러 (Transpiler) 트랜스파일러는 위에서 설명한 컴파일러의 하위분류입니다. 언어를 변환해 주는 기능을 제공하는 소프트웨어인 것은 일반적인 컴파일러와 동일하지만, 트랜스파일러는 완전히 다른 두 언어 사이를 변환해 주는 것이 아니라 유사한 두 언어 사이에서 변환해주는 한정된 역할을 제공해 주는 소프트웨어라는 점이 다릅니다. 위에서 언급했던, 고급 프로그래밍 언어를 저급 프로그래밍 언어로 변환해 주는 컴파일러가 영어를 완전히 다른 언어인 한국어로 번역해 주는 번역가라고 한다면, 트랜스파일러는 옛 우리말을 현대 한국어로 번역해 주는 번역가라고 할 수 있겠습니다. 바벨이라는 툴이 트랜스파일러인 이유는, 최신 자바스크립트 문법으로 작성된 코드를 구버전 브라우저도 이해할 수 있는 수준의 오래된 자바스크립트 코드로 변환해 주는 소프트웨어이기 때문입니다. 즉, 바벨은 트랜스파일러로써 유사한 두 언어 사이에서의 변환 기능을 제공해줍니다. 보통 자바스크립트 최신 문법이라고 하면 ES6(ECMAScript 2015)를 기준으로 잡습니다. ES6 표준에서 자바스크립트의 주요한 신규 기능들이 워낙 많이 추가돼서, 이 기능들을 제공하지 않는 브라우저를 구버전 브라우저로 간주하기 때문인데요. 만약 바벨이라는 빌드 툴이 없었다면, 구버전 브라우저 (대표적으로 인터넷 익스플로러)에서도 서비스를 정상적으로 제공하기 위해서는 ES6 이전의 자바스크립트 문법만으로 코드를 작성해야 했을 것입니다. 이는 당연히 생산성을 매우 떨어뜨리는 일입니다. 당장 var 키워드를 사용하거나 비동기 코드를 프로미스 없이 짤 생각을 하니 정신이 혼미해지네요. 이때 구원자로 등장하는 게 바로 바벨입니다. 바벨은 ES6 문법을 활용해 작성한 자바스크립트 코드를 이전 버전의 자바스크립트 코드로 변환해 줌으로써, 우리가 만든 서비스가 구버전 브라우저에서도 의도한 대로 동작하도록 만들어줍니다. 바벨이 단순히 ES6 문법만을 변환해 주는 것은 아닙니다. 현재 바벨은 ECMAScript 2020까지의 기능들을 별도의 세팅 없이도 자체적으로 지원해 주고 있으며, 별도의 플러그인을 사용한다면 ECMAScript 2021의 기능들도 모두 지원하도록 세팅할 수 있습니다. 심지어는 아직 정식으로 ECMAScript 표준에 포함될 것이라고 결정된 기능들 이외에도, 단순히 제안만 되었고 아직 검토 중인 기능들 (이를 Proposals라고 합니다)도 플러그인을 통해 지원하도록 설정해 줄 수 있습니다. ES2021에서 도입된 기능 중에서 간단한 기능을 하나 예로 들어보겠습니다. 숫자 구분자(numeric separator)라는 기능이 있습니다. 우리가 일상생활에서 매우 긴 숫자를 표현할 때, 1,000,000,000과 같이 쉼표 구분자를 사용해 숫자의 각 단위를 쉽게 알아볼 수 있도록 표현하는 경우를 종종 볼 수 있습니다. 자바스크립트에서는 숫자를 표현할 때 이런 구분자를 활용할 수 있는 방법이 없었지만, ES2021에서 쉼표 대신 아래와 같은 언더 바(_)를 숫자 구분자로 활용할 수 있는 기능이 추가되었습니다. const longNumber = 1_000_000_000 console.log(longNumber) // 1000000000 이 숫자 구분자를 구버전 브라우저에서 사용하면, 언더 바를 인식하지 못해 에러가 발생할 것입니다. 따라서, 브라우저 버전과 상관없이 숫자 구분자를 사용하기 위해서는 @babel/plugin-proposal-numeric-separator 바벨 플러그인을 프로젝트에 적용해야 합니다. 바벨에서는 사용자가 일일이 플러그인들을 설치하지 않아도 최신 자바스크립트 문법들을 사용할 수 있도록, 관련 플러그인들의 집합인 프리셋을 공식적으로 지원합니다. (@babel/preset-env) 위에서 설명한 숫자 구분자도 @babel/preset-env에서 기본적으로 지원합니다. 1-3. 바벨 실습 바벨을 별도로 설치하지 않아도, 바벨이 우리의 코드를 어떤 식으로 변환해 주는지 테스트를 해볼 수 있는 공식 웹사이트가 존재합니다. 제가 위에서 언급했던 ES6의 문법들을 사용한 매우 간단한 자바스크립트 코드를, 구버전 브라우저인 IE10 환경에서도 문제없이 동작하도록 변환하는 예제를 보겠습니다. 아래 웹 사이트에 접속해 봅시다. https://babeljs.io/repl 웹 사이트에 접속하면 가장 먼저 코드를 작성할 수 있는 공간이 보입니다. 코드를 작성하기 전에 좌측 메뉴(화면 크기에 따라서는 상단 메뉴일 수도 있습니다)를 열어 TARGETS 영역의 내용을 다음과 같이 변경해서, 바벨이 우리의 코드를 IE10에서도 지원 가능하게 변환해 주도록 설정해봅시다. 그 후, 코드를 작성할 수 있는 에디터 영역에 아래와 같이 ES6 문법으로 작성된 자바스크립트 코드를 넣어봅시다. const HelloWorld = (( name, content )) => ( let fullText = ""
if (name) (
fullText += name: $(name)
)
if (content) (
fullText += content: $(content)
)
return fullText )
HelloWorld(( name: "Bill", content: "Hello World!" )) 에디터 영역에 코드를 작성하면 별도의 조작 없이, 바벨이 실시간으로 아래와 같은 코드로 변환해주는 것을 확인할 수 있습니다. var HelloWorld = function HelloWorld(_ref) ( var name = _ref.name, content = _ref.content var fullText = ""
if (name) ( fullText += "name: ".concat(name) )
if (content) ( fullText += "content: ".concat(content) )
return fullText )
HelloWorld((
name: "Bill",
content: "Hello World!",
)) 변환 결과를 보면, 바벨이 우리의 코드를 IE10에서도 동작하도록 다음과 같이 변환해 줬음을 확인하실 수 있습니다. let 및 const 키워드를 var로 변경 화살표 함수는 일반 함수로 변경 템플릿 리터럴을 concat 함수로 대체 구조 분해 할당 문법을 사용한 함수 속성을 _ref로 대체 2. Terser Terser라는 빌드 툴에 대해 처음 들어보는 분이 있으실 거로 생각합니다. 사실, 우리가 Terser라는 툴을 직접 다룰 일은 많지 않습니다. 대부분의 프레임워크에서는 이미 Terser에 대한 세팅이 기본적으로 되어있으며, 더욱이 Webpack의 경우 v4 이후 버전부터 별도의 설정 없이도 프로덕션 모드에서 자동으로 Terser 툴을 사용하도록 세팅되어 있기 때문입니다. Terser 공식 웹사이트에서는 Terser라는 툴을 다음과 같이 소개하고 있습니다. ES6+를 위한 자바스크립트 parser, mangler 그리고 compressor 파서(parser)가 무엇인지 설명하려면 글이 너무 길어질 것 같아서 생략하고, 이 글에서는 mangler와 compressor가 무엇인지에 대해서만 초점을 맞춰보겠습니다. 2-1. mangler 사전에 mangle이라는 단어의 뜻을 검색해 보면, 심하게 훼손하다 혹은 엉망으로 만들다라는 뜻을 가지고 있다는 것을 알 수 있습니다. 따라서, Terser가 mangler라는 뜻은 우리가 작성한 자바스크립트 코드를 훼손하거나 엉망으로 만들어주는 툴임을 유추해 볼 수 있습니다. 이름만 봐서는 왜 필요한 툴인지를 모르겠네요. Terser가 mangler로써 하는 역할은 우리가 코드에서 사용한 변수 / 함수 / 속성들의 이름을 매우 단순화된 이름으로 변경해주는 것입니다. 우리가 어떤 언어로 프로그래밍을 하는지와 무관하게, 변수나 함수의 이름을 정할 때에는 항상 의미 있는 네이밍을 사용하라는 격언이 언제나 등장합니다. 좋은 네이밍은 그 변수나 함수가 어떤 역할을 하는지 코드 전체를 보지 않더라도 대략적으로 이해할 수 있게 해주기 때문에, 코드의 가독성을 매우 향상시킬 수 있습니다. 하지만, 컴퓨터 입장에서 이 네이밍은 전혀 중요하지 않습니다. 우리가 특정 메뉴가 보여져야 하는지의 여부를 관리하는 Boolean 타입 변수의 이름을 isMenuVisible이라고 짓든지 아니면 단순히 b라고 짓든지 와 상관 없이, 컴퓨터는 그저 하나의 똑같은 불리언 변수로 취급합니다. 우리가 코드의 가독성을 위해 붙여준 이름들은 결과적으로 코드의 길이를 길게 만들기 때문에, 소스 파일의 크기가 커질 수밖에 없습니다. 프로젝트에서 사용하는 소스코드 파일의 크기는 작으면 작을수록 좋습니다. 사용자가 어떤 웹 페이지에 접근하면, 브라우저는 웹 페이지의 다양한 기능들을 제공하는데 필요한 소스 파일들을 서버로부터 다운로드합니다. 이때 소스 파일의 크기가 크면 클수록 다운로드에 걸리는 시간이 길어지기 때문에, 기능 제공을 위해 걸리는 시간이 더 오래 걸리게 되고 결국 나쁜 사용자 경험을 줄 수밖에 없죠. mangler는 우리의 소스코드에 존재하는 네이밍들을 의미 없는 문자로 바꿔버립니다. isMenuVisible 이라는 변수명을 단순히 a로 바꿔버리면, 13글자를 한 글자로 줄일 수 있습니다. 이러한 최적화 과정을 통해 실제 프로덕션 환경에서 사용될 우리의 코드 사이즈를 획기적으로 줄일 수 있습니다. 물론 우리의 코드는 엉망이 되겠지만 말이죠(mangled). 실제로 Terser가 mangler로써 코드를 어떻게 바꿔주는지 직접 확인해 봅시다. 위에서 바벨 테스트를 위해 사용했던 HelloWorld 코드를 복사한 후 hello.js라는 이름의 파일로 컴퓨터에 저장하고, 다음 커맨드를 통해 로컬 CLI 환경에 Terser를 설치해 봅시다. npm install terser -g 커맨드 라인에서 다음 명령어를 실행하면, hello.js 파일을 Terser의 mangler를 통해 최적화한 결과인 hello-mangled.js 파일이 새로 생성됩니다. terser --mangle --toplevel hello.js -o hello-mangled.js 생성된 hello-mangled.js 파일을 보면, 우리가 hello.js 코드에서 사용했던 HelloWorld나 fullText와 같은 네이밍이 n, t, e와 같은 의미 없는 문자로 변환된 것을 확인하실 수 있습니다. Terser는 기본적으로 우리가 작성한 코드에서 모든 공백 문자를 제거해 주기 때문에 (이 또한 코드 최적화를 위해서입니다), 코드가 한 줄로 변환돼서 코드가 어떻게 변경됐는지 보기 어려울 수도 있습니다. 만약 공백문자가 제거되지 않았다고 가정한다면, 다음과 같이 변경되었을 것입니다. const n = (( name: n, content: e )) => (
let t = ""
if (n) (
t += name: $(n)
)
if (e) (
t += content: $(e)
)
return t
)
n((
name: "Bill",
content: "Hello World!",
)) 공백 문자가 제거되지 않았다고 가정하고 원본 코드와 변환된 코드가 담긴 파일 크기를 비교해 보면, 변환된 코드의 크기가 대략 30~40% 정도 줄어든 것을 확인할 수 있습니다. 굉장하네요! 2-2. compressor compress는 압축하다라는 뜻을 가지고 있습니다. 따라서, Terser가 compressor라는 뜻은 우리가 작성한 자바스크립트 코드를 압축해 주는 툴임을 유추해 볼 수 있습니다. Terser가 compressor로써 하는 역할은 우리의 자바스크립트 코드를 분석한 후, 더 짧은 코드를 통해 동일한 기능을 구현할 방법이 있는지 확인하고 그 방향으로 코드를 변환해 주는 것입니다. compressor는 글로 설명하는 것보다 직접 변환 결과를 보는 게 더 쉽게 이해할 수 있습니다. 다음 명령어를 통해서 Terser가 어떻게 우리의 코드를 압축하는지 확인해봅시다. terser --compress --toplevel hello.js -o hello-compressed.js 변환 결과가 담긴 hello-compressed.js 파일을 보면 다음과 같습니다. (공백 문자는 제거되지 않았다고 가정하겠습니다) ((( name: name, content: content )) => (
let fullText = ""
name && (fullText += name: $(name)), content && (fullText += content: $(content))
))((
name: "Bill",
content: "Hello World!",
)) 이번에는 Terser의 mangler 기능을 사용하지 않았기 때문에, 우리가 정해준 변수의 이름들은 그대로 유지되었습니다. 하지만, 문법 구조상으로 다음과 같은 변경 점들이 발생했습니다. HelloWorld라는 함수명이 사라지고, 함수가 정의와 동시에 실행되도록 변경되었습니다. HelloWorld 함수는 hello.js 내부에서만 사용되며 재사용되지 않고 한 번만 호출되기 때문에, 함수 선언과 동시에 실행되어도 무방합니다. HelloWorld 함수가 다른 곳에서 재사용된다면 변환 후에도 원본 코드와 같이 정의와 실행이 분리되어 있겠죠. if 조건문이 && 연산자로 변경되었습니다. fullText에 내용을 채워 넣는 코드들이 쉼표(,)를 통해 한 줄의 코드로 변경되었습니다. 실제로 HelloWorld 함수의 리턴 값을 사용하는 곳이 없으므로 (리턴 값을 콘솔로 찍는 등으로 활용하고 있지 않습니다), return 문이 제거되었습니다. 위 코드는 저희가 처음 작성했던 hello.js와 문법 구조가 달라졌으나 결과적으로 같은 기능을 제공하기 때문에, 코드가 압축되어 최적화되었다고 할 수 있습니다. 원본 코드와 변환된 코드의 파일 크기를 비교해보면, 이번에도 대략 30~40% 정도 줄어든 것을 확인할 수 있습니다. 2-3. Terser as Minifier 위에서 설명한 코드를 mangle 하는 과정이나 compress 하는 과정과 같이, 우리가 작성한 코드를 동일한 기능을 제공하는 경량화된 코드로 변환해 주는 일련의 작업을 minify 혹은 minification(코드 경량화) 이라고 부릅니다. 그리고 코드 경량화 작업을 해주는 툴을 우리는 minifier라고 부릅니다. 따라서, Terser는 우리가 작성한 자바스크립트 코드를 프로덕션에서 더욱 경량화된 상태로 제공될 수 있도록 도와주는 minifier 빌드 툴이라고 할 수 있습니다. 참고로, hello.js가 mangle 과정과 compress 과정을 모두 거치고 공백까지 모두 제거된다면, 아래 결과와 같이 기존 코드의 절반 이하 크기로 경량화됩니다. 3. SWC (Speedy Web Compiler) SWC는 자바스크립트 프로젝트의 컴파일과 번들링 모두에 사용될 수 있는, Rust라는 언어로 제작된 빌드 툴입니다. SWC는 Speedy Web Compiler의 약자로, 말 그대로 매우 빠른 웹 컴파일러의 기능을 제공하는 툴입니다. 하지만 이름과는 달리 컴파일러의 기능만을 제공하는 것은 아닙니다. 웹팩과 같은 자바스크립트 번들러의 기능을 제공할 spack도 개발 중이며 얼마든지 기능 확장이 가능하도록 설계되어 있기 때문에, 단순한
