웹팩(3/4); JS모듈화 역사 돌아보기(1)

Chullin
36 min readMay 4, 2019

--

.

안녕하세요. 개발자 chullin입니다. 4부작 중 3 편입니다. 1편은 프롤로그로서 웹팩을 한번 파보려는 개인적인 이유를 찾아보았고, 2편은 웹팩 사용법 및 SPA(single page application)을 개발할 때 사용하면 좋은 플러그인들을 꼽아보았습니다. 오늘 퍼블리시한 3편은 웹팩, 그리고 웹팩에서 조금 더 확장해 모듈번들러 및 JS에서 모듈화가 도입되게 된 계기를 작성해보고자 합니다. 거창하게 말하자면 JS 모듈화의 역사를 다뤄볼 예정입니다.

다만, 글이 길어지는 듯 싶어서, 모듈화의 기틀이 마련되기까지의 과정을 3–1부로, 그리고 browserify, webpack을 비롯한 이 시대 웹개발의 잇템 모듈 번들러가 등장하는 시기부터를 3-2부로 나눠 쓰고자 합니다.

오늘의 주제는 JS 모듈화의 역사입니다. (Photo by Patrick Tomasso on Unsplash)

이번 포스팅에서 다룰 꽤나 긴 글을 요약하자면, 다음과 같습니다.

.

1부 TL;DR

.

JS가 1996년에 탄생한 후, 근 10년동안은 모듈이라는 것이 등장조차 하지 않았습니다. IIFE 혹은 object interface 등의 모듈화 패턴 정도가 있었을 뿐이었습니다. 하지만, 2009년 CJS가 등장하면서 서버사이드 모듈화의 규칙이 마련되었고, 같은 해 nodeJS가 CJS를 활용해서 모듈화 시스템을 구현했습니다. 이러한 방식은 당시에 웹에서 직접 적용하기에는 부적합했습니다.

.

(1) CJS 모듈화 기법의 웹 부적합성 문제

CJS처럼 sync한 방식으로 로드할 때의 문제는 여러 번의 http request가 필요하다, 즉 request/response에 너무 많은 시간이 걸린다는 점이었습니다. 그 문제를 해결하고자 AMD 측에서는 async하게 모듈들을 로드할 수 있는 방법을 고안했습니다.

(2) AMD 모듈화 기법이 대세를 거스르는 문제

하지만 그 상황에서 또 발생했던 문제는 투트랙 개발이었습니다. 개발자들이 서버사이드용 코드, 브라우저용 코드를 따로 작성해나가고 있다는 점이 문제였습니다. CJS 스타일 기반의 nodeJS로 자바스크립트기반 서버를 개발하고, npm으로 패키지를 모듈로 장착하고 싶었던 개발자들에게 AMD는 계륵이었습니다.

(3) CJS 모듈화 기법을 쓰면서도, 퍼포먼스를 확보하려면?

nodeJS의 모듈화기법에서 벗어나지 않으면서도 request / response에 걸리는 시간을 획기적으로 줄일 수는 없었을까요.

입코딩을 해보자면, 여러 차례 http request 날리는 것이 문제라면 한 번 날리는 것으로 바꿔주면 됩니다. 대신, 한 번의 request를 통해서 모든 모듈들을 한꺼번에 로드하면 됩니다.

입코딩은 참 쉽지만, 어디서부터 시작해야하는 것인지 당황스러운데요. 방금 입코딩을 실제 코드로 구현한 집단은 2012년도에 등장합니다. CJS, AMD가 탄생했던 2009년으로부터 3년 후였고, nodeJS가 등장한지 약 2년 후였고, Node Package Manager가 등장한지 약 1년 후였습니다. 바로, Browserify입니다.

.

글묶음

.

1️⃣ 웹팩; 프롤로그 ⭕️

2️⃣ 웹팩; 기능 사용하기 ⭕️

3⃣-1웹팩; JS 모듈화 역사 돌아보기(1)⭕

3⃣-2웹팩; JS 모듈화 역사 돌아보기(2)X

4️⃣ 웹팩; 미래 점쳐보기 ❌

.

아무 것도 없었지만, 모든 것이 생기다.

.

webpack은 모듈 번들러입니다. 과거를 살펴볼까요? 모듈 번들러가 등장하기 전에는 requireJS 등의 모듈 로더가 있었고, 그 전에는 JS 코드의 모듈화를 자체를 어떻게 이룩할 것인지에 대한 논의가 있었습니다.

지금의 개발자들에게 있어서 모듈을 불러와 번들링하는 것은 웹팩, 파셀, 롤업 등의 서비스 덕택에 어렵지 않습니다. 하지만 번들링은 꿈이거니와, 모듈화 자체의 토양이 없었던 시기가 자바스크립트 진영에도 있었습니다. 대략 10여년 전만해도 말입니다. 모듈화를 기준으로 말하자면, 황량했던 시기였지요.

이번 글은 타임머신을 타고 과거로 돌아가 JS 모듈화와 관련된 기술적 스택을 하나씩 쌓아가면서 지금 2019년의 현재로 돌아오는 포맷을 취해보려고 합니다.

[목차](1) 모듈화의 표준이 없던 황량한 웹의 세계로 3번의 time travel
(2) <script> 태그로 도배하면 3 가지 문제가 있어요.
(3) 우리는 모듈화 기법을 만들 수 있을까요?
(4) CommonJS, 절반의 해답을 외치다.
(5) AMD, 나머지 절반을 채우다.

참고로, 이번 글은 역사를 돌아보는 것이 목적인만큼 코드 그 자체에 대한 언급은 그다지 많지 않을 수 있겠어요. 만약 독자분께서 개발자라면, 쉬어가는 겸 지하철 칸에서 읽어보시면 좋을 것이에요. 모듈화에 대한 역사를 굳이 써보려고 했던 것은, 우리가 왜 현재 webpack 같은 서비스를 쓰고 있는지 그 흐름을 파악해보려고 했었던 것이었어요.

.

모듈화의 표준이 없던 황량한 웹의 세계로 3번의 time travel.

.

2019년의 웹개발자들은 자바스크립트로 모듈을 불러오는 과정은 어렵지 않습니다. ES6 module 이라는 표준이 존재하기 때문입니다. 웹에 올릴 코드를 모듈화하기도 편하고, 서버사이드에서 코드를 모듈화하기도 편합니다. 하지만, 시간을 거슬러 올라가 보지요. 지금은 당연한 이 모듈화 시스템이 과거에는 그토록 요원하게 느껴지고 그토록 염원했던 바였던 적이 있습니다. 그 황량했던 과거로 타임머신을 타고 가보려구요. 정확히 3번의 타임랩스를 거쳐보기로 해요.

지금은 2019년 4월입니다. 첫번째 타임머신을 타고, 대략 4년 전으로 거슬러 올라가볼까요.

첫 번째 타임머신입니다! 2015년입니다. Photo by Djim Loic on Unsplash

바야흐로 2015년 6월, ECMAScript 6가 등장했습니다. 2015년에 ECMAScript가 만들어졌다고 해서 ECMA 2015라고도 불리고, 의견합치가 어려워서 거의 소득 없이 끝난 과거의 싸움판 회의를 잊고 모두가 수긍할만한 하모니를 이뤘다는 뜻에서 ECMA Harmony라는 이름도 가지고 있습니다. 어쨌든, 이 날 자바스크립트에 표준화된 모듈화 기법이 드디어 등장하게 되었습니다.

JS가 1996년에 등장했고, 약 20년 후 ECMAScript6이 등장합니다. ECMAScript Harmony라고 불리기도 하고, ECMAScript 2015로 불리기도 합니다.

ES6에서 표준화된 모듈 가져오기는 다음과 같습니다. main.js에서 foo.js파일에 있는 function_foo를 가져오는 코드입니다.

//main.jsimport foo from "foo"
foo();

그리고, foo.js에서는 다음과 같이 모듈을 내보냅니다.

//foo.jsfunction function_foo(){
console.log(“foo”)
}
export default {function_foo}

자바스크립트가 1996년에 등장하고, 거의 20년이 지나고서야 웹개발자들은 import/export 형식 모듈화의 표준을 갖게 되었던 것입니다. 덕분에, 자바나 파이썬처럼 모듈을 불러올 수 있었습니다. 오해하지 말아야 할 것은, 자바나 파이썬의 껍데기를 따라했다는 것이 아닙니다. 모듈화에 필요한 기본적인 요건을 갖춘 무언가가 자바스크립트 진영의 표준으로 등장했다는 점을 분명히 하고 싶습니다. 세 가지의 요건이 있어요.

첫째, 스코프가 독립될 수 있어야 합니다. 스코프 독립이 없다면, 전역 변수 영역이 더럽혀지는 문제가 발생할 것입니다. 전역변수영역이 더럽혀진다면, 마음껏 변수를 쓸 수가 없습니다. 변수에 값을 넣을 때마다 “아 내가 이 변수 써도 되나…?”하는 고민을 매번 해야하는 상황에 놓이게 되는 것입니다.

둘째, 단순히 파일 덩어리 하나 전부를 모듈로 가져오는 것이 아니라, 파일에서 함수나 객체를 가져올 수 있어야 합니다. 그래야 가독성 높은 코드 작성이 가능해지겠지요. 예를 들어서, script 태그로 모듈을 가져온다고 생각해볼게요. 이 방식으로는 파일을 가져올 수 있을 뿐입니다. 아래처럼요.

//main.html...
<script src="foo.js"></script>
...

이런 방식이 불편한 이유는모듈을 import할 때 해당 파일 내부에 어떤 모듈이 담겨있을지 유추할 수 있는 단서는 파일 이름뿐이라는 점입니다. 반면, nodeJS 에서 사용하는 것처럼 어떤 함수 혹은 어떤 변수를 import해서 가져올 수 있다고 생각해보아요.

//main.jsconst function_a = require("foo.js").function_a

좀 더 짧은 형식으로, 충분히 의미가 담긴 코드를 짤 수 있게 될 것입니다.

셋째, 의존성 관리가 수월해야 합니다. 특히, 모듈 A가 모듈 B를 import하고, 모듈 B가 모듈 C를 임포트하고… 엄청 많은 모듈이 사용되는 경우에 의존성 관리가 제대로 이루어지지 않는다면 scalable한 코드를 작성할 수 없습니다. scalable하지 못하다는 말은 곧 확장이 어렵다는 말과 같아요. 다시 말해, 개발자들은 실제 생산성을 낼 수 있는 코드를 작성하지 못하고 의존성 관리 뒷치다꺼리 하는 데에 시간을 다 허비할 것입니다.

방금 언급한 세가지 요건은 이번 글에 걸쳐 여러번 나올 것이니, 지금 이해가 다 안되어도 대충 그런게 있구나 하면서 넘어가셔도 좋습니다.

어쨋든 ESM(ECMA Script Module) 표준이 등장했다는 말은 곧 그 시기가 서로 다른 모듈화 방법론들이 우세를 점하려고 다툰 시기였음에 대한 방증입니다. 또한 그 시기에 수많은 개발자들이 모듈화 방법론을 표준화 하는 것에 대한 의지와 희망이 가득 담겨 있던 시기였음에 대한 방증이기도 합니다.

그렇다면, 이 ES6가 등장하기 전에는 어떤 방식으로 모듈을 가져오고 모듈을 사용했을까요? 그 서로 다른 방법들은 도대체 무엇이었을까요? 이러한 질문에 대한 답을 오늘 글에서 다룰 예정입니다. 소단원들 중에서 (4), (5)에서 다룰 것 같네요.

[목차](1) 모듈화의 표준이 없던 황량한 웹의 세계로 3번의 time travel
(2) <script> 태그로 도배하면 3 가지 문제가 있어요.
(3) 우리는 모듈화 기법을 만들 수 있을까요?
(4) CommonJS, 절반의 해답을 외치다.
(5) AMD, 나머지 절반을 채우다.

하지만, 그 작업은 오늘 다룬다는 약속만 해두고서, 저는 더 과거로 넘어갈 예정입니다. 더 멀리 더 과거로 가서 자바스크립트에서 모듈화를 어떻게 패턴화했던 것인지에서부터 시작해보려구요. 모듈화의 백가쟁명이 시작되던 그 시기로 말이지요.

이 시점에서 독자분들에 대한 요청사항이 하나 있습니다. 머리 속에서 ES6의 모듈화기법은 잠시 지워주시길 바랍니다. 다시말해, import, export 문법은 잠시 잊어주세요. 사극에서 왕이 휴대폰을 쓰면서 문제를 해결한다고 생각해보십시오. 데우스 엑스 마키나의 등장은 드라마를 재미없어집니다. 필자의 드라마에 더 젖어들기 바라는 마음에, 독자분들 스스로도 한번 사고의 제약을 걸어주시면 좋겠습니다. 과거의 시점에서 과거의 로직으로 생각해보자구요.

이제 2번째 타임머신을 탈 시간입니다. 2015년 6월이 ES6가 등장했다고 했지요? 이제 그로부터 6년 전으로 더 거슬러 올라가보지요.

두 번째 타임머신입니다! 2009년으로 넘어갑니다. Photo by Djim Loic on Unsplash

2009년 1월, 모질라 엔지니어 Kevin Dangoor를 중심으로 JS 모듈화 표준을 만들고자 하는 commonJS 그룹이 결성되었고, 3개월 뒤 commonJS API 0.1을 발표합니다. 이 시점이야말로, JS 모듈화의 0년이라고 할 수 있어요. 당시 모듈화 표준은 꿈에 불과했던, 황량한 웹의 세계였지요. 하지만, 이 시점 이후로 모듈화에 대한 가닥이 잡히기 시작했어요. commonJS 그룹에 대한 자세한 언급도 뒤로 미룰게요.

아직 타임머신 여행이 안끝났어요. 자, 저희는 어느새 2019년으로부터 두번의 타임머신을 타고서 10년 전으로 돌아왔네요. 이제 조금 더 과거로 가볼게요. 마지막 타임머신을 타기 전에, 머리 속에서 commonJS와 관련된 개념들도 지워주세요! 과거의 시점에서 과거의 로직을 생각해보기로 해요.

세 번째 타임머신입니다! 마지막 타임머신을 타고서 1996년으로 갑니다.Photo by Djim Loic on Unsplash

.

마지막 타임머신을 타고서, commonJS의 등장보다 14년 전으로 거슬러 올라가볼게요. 1996년 자바스크립트가 개발되었던 당시로 말입니다. 바로 이 시점 전후가 제가 서술하려는 역사의 시작 시점입니다.

1996년 JS가 넷스케이프 회사 내부에서 만들어졌을 당시만 해도 JS의 목적은 하드코어 소프트웨어 개발자들을 위한 언어가 아닌 이제 막 컴퓨터 공학의 세계에 눈뜬 디자이너들을 위한 것이었어요. 넷스케이프에서 만든 브라우저를 더욱 아름답게 꾸미기 위한 언어였지요.

마크 안드레센입니다. 두상이 인상적입니다. 링크

자바스크립트가 스크립트용 언어로 개발된 것이라는 말 있지요? 저는 JS언어가 등장했을 당시 말도 제대로 못하던 시절이라 이 말을 어떻게 이해해야 하는 것인지 아직도 확실히는 모르겠지만, 저는 HTML 파일의 script 태그 안에 들어가는 것이 목적인 것으로 이해했어요. HTML 파일 안에 들어가는 자그마한 코드조각으로 애니메이션도 만들고 여러가지 예쁜 효과들도 만들고자 JS가 탄생한 것으로 말이에요. 넷스케이프 사장 마크 안드레센이 말했다고 일컬어지는 다음의 문장 덕분에 이런 이해가 가능했어요.

… Mark Andreessen believed that HTML needed a ‘glue language’ that was easy to use by Web designers and part-time programmers to assemble components such as images and plugins. … — wikipedia

말 그대로, 웹디자이너를 위한 쉬운 언어라는 것이지요. 그때부터 아래와 같은 형식으로 썼을 것이에요. (이 주장에 확신은 없어요..)

<!doctype html>
<html>
<head>
...
<script src=‘./main.js’></script>
...
</head>
<body>
...</body>
</html>

JS가 하드코어 소프트웨어 개발자를 위한 언어가 아니라는 점, 다시 말해 운영체제도 빠싹하게 알고있고 C도 빠싹하게 알고있는 개발자들을 위한 언어가 아니라는 점은 다른 말로 하자면, scalable한 언어가 아니라는 것이에요. 이 언어로 모든 것을 처리할 수는 없고, 자잘자잘한 애니메이션 만드는 데에만 사용할 수 있는 그런 언어였던 것입니다.

하지만, 그렇다고 완전히 무시할 수 있는 언어도 아니었어요. 세기말의 닷컴 버블을 기억하시려나요? 그 당시만 해도 웹사이트를 가지면 무엇이라도 할 수 있고, 돈도 많이 벌 수 있다는 기대감이 팽배한 시기였어요. 그런 시기에 자바스크립트는 나름 유망한 언어였을 것입니다. 얼마나 많은 미국의 초보개발자 혹은 주니어 개발자 혹은 컴퓨터공학에 관심있는 디자이너들이 웹개발의 세계(다시 말해 자바스크립트의 세계)로 또 뛰어들었겠어요.

게다가 2002년에서 2005년 사이에 google에서 JS를 써서 AJAX를 개발했는데, 이는 자바스크립트의 위상을 드높이는 계기가 됩니다.AJAX를 처음 들어보신 분들을 위해 AJAX의 특징에 대해서 적어본 글을 링크로 걸어둘게요. [HTTP부터 WEBSOCKET까지]

AJAX이전에는 깨작깨작 애니메이션 넣었으려고 JS를 썼기 때문에 JS 자체에 대해서도 인식이 그다지 좋지 않았지만, google에서 JS만으로도 엄청나게 예쁜 물건을 만들 수 있음을 보여주었던 사례가 바로 AJAX를 활용한 사례였어요. 구글맵도 AJAX로 만든 것이었구요. 그리고 얼마 지나지 않아 존 레식이라는 개발자가 AJAX를 쓰기 쉽게 만든 jQuery를 만들었어요. JS를 쓰면 예쁘게 만들 수 있고 또 jQuery를 통해 쉽게 만들 수 있다면, 이제 더 많은 개발자들이 자바스크립트의 세계로 뛰어들지 않았을까요?

어쩌면, 바로 2005년에서 2009년 사이의 시점부터였을 것이에요. 많은 주니어 개발자들 그리고 시니어 개발자들이 JS에 대해서 좋은 인식을 갖고서 조금 더 공부해보고, scalable한 코드를 만드려는 생각을 한 시점 말이지요. 저는 왜 그렇게 생각하고 있냐하면, ajax, jquery가 등장했다 함은 이제 클라이언트 사이드에서도 어떤 중요한 작업을 수행할 수 있다는 말과 같거든요.

즉, 클라이언트 사이드에서 작업하는 내용들을 전문적으로 다룰 수 있는 인력수요가 증대되었다는 것과 같은 말로 제게는 들렸어요. 또 다른 말로 하자면, 웹사이트가 웹앱이 되었다는 말과 같은 말이구요. 그때부터는 클라이언트 사이드를 전문적으로 다룰 수 있고, 디자인도 좀 할 수 있는 그런 일자리가 생겼을 것이에요. 웹(앱)개발자 혹은 웹디자이너라는 명함을 걸고 구직을 하는 사람들과 그들을 구인하는 사람들이 붐비게 된 배경 시점이랄까요?

ajax와 jquery가 등장하기 전에는 어떤 웹사이트에 들어온 클라이언트가 해당 웹페이지에서 인터랙션을 시도한다고 할 때, 각각의 유저 요청에 대해서 새로운 html페이지를 띄워주는 식이었어요. 각 인터랙션에 해당하는 html 페이지를 response로 제공해주는 식으로 말이지요. 예를 들어, 메인 페이지에 들어온 유저가 무슨 버튼을 누르든, 그 버튼이 로그인 버튼이든 좋아요 버튼이든, 계속 새로운 html 페이지를 띄워주는 식 말이에요.

요런 식입니다. 사용자가 요청하면 서버에가 새로운 HTML 문서를 제공합니다. 새로운 HTML문서를 응답으로 받는다는 것은, 새로운 페이지로 이동한다는 것을 뜻합니다. 독자여러분들이 제 포스트로 이동하는 것도 새로운 html 페이지를 미디엄 서버로부터 응답받는 것입니다. (출처: 링크)

지금 생각하면 비효율적이고 그냥 “DOM만 바꿔주면 되는 거 아니야?” 라는 생각이 들지만, 당시에는 요청에 대한 응답을 주는 방법으로 존재하는 것은 새로운 html 페이지를 띄워주는 것 말고는 없었어요. 하지만, ajax와 jquery가 등장한 이후에는 클라이언트 쪽 브라우저에서도 어떤 역할을 담당할 수 있게 되었어요. 로그인 버튼을 누르거나, 좋아요 버튼을 누르는 과정은 이제 새로운 html을 만드는 것이 아니라, ajax를 활용해 개발해야 하는 상황이 마련된 것이에요. 개발자들은 웹사이트가 아니라 웹앱을 만들어내기 시작했지요.

요것이 AJAX 기술이 들어간 HTTP에 따른 통신. (출처: 링크)

AJAX와 jQuery의 등장으로 웹개발이 활성화되고, 많은 관심을 받자 javascript 언어를 조금 더 쓰기 쉽게 다듬으려는 노력들이 이어졌어요. 이제 자바스크립트는 더 이상 <script> 태그 안에 짜잘하게 넣는 코드가 아니라 jQuery도 임포트 해서 쓰고, ajax도 임포트해서 쓰고 의존성 관리도 해야하는 상황이 요청되었어요.

ajax와 jQuery만 있었겠어요? 아마 웹페이지를 예쁘게 만들 수 있고 유저의 인터렉션을 담당할 수 있는 다른 라이브러리나 프레임워크들도 꽤나 많았을 것이에요. 당시의 개발자들은 그것들을 다 임포트해서 쓰고 싶었을 것이에요. 저희 세대 개발자들도 그렇잖아요. 예쁘고, 잘나가고, 편리한 기능이 담긴 코드들은 가져다가 쓰고 싶어지는 것은 당시 개발자들도 같은 생각이었을 것이에요.

자, 이 상황에서 독자분들께서는 어떻게 하시겠어요? 지금까지(즉 2005년까지) javascript로 개발을 한 경험을 돌이켜봤을 때 <script> 태그 안에 코드를 넣는 것이 경험의 전부였다고 가정한다면, 저 같으면 html에 <script> 태그 오지게 박을 생각부터 했을 것이에요. 수많은 모듈들과 라이브러리들을 html의 <script> 태그에 구겨 넣는 방식 말이에요.

<!doctype html>
<html>
<head>

<script src=‘./main.js’></script>
<script src=‘./foo.js’></script>
<script src=‘./bar.js’></script>
<script src=‘./foooo.js’></script>
...
이렇게 스크립트 태그가 수 백개….?
</head>
<body>
...
</body>
</html>

하지만, 경험많고 똑똑한 웹개발자분들이라면 아시겠지요. 이러면 안된다는 것을 말이에요.

.

<script> 태그로 도배하면 3 가지 문제가 있어요.

.

문제는 다방면적으로 산재해있겠지만, 제가 언급할 수 있는 대표적인 문제는 스코프의 문제, 의존성 관리 문제, 그리고 로드시간의 문제 총 3개의 문제입니다.

첫째로, 브라우저 스코프가 문제입니다. 예를 들어 볼게요. 다음과 같이 서로 다른 두 개의 파일(foo.js, bar.js)을 각가 스크립트 태그에 넣고자 해요.

<!doctype html>
<html>
<head>
...
<script src=‘./foo.js’></script>
<script src=‘./bar.js’></script>
...
</head>
<body>
...</body>
</html>

이 상황에서 foo.js와 bar.js가 각각 다음과 같이 변수이름을 사용한다고 해볼게요. 예를 들어, foo.js에서도 track이라는 변수를 사용하고, bar.js도 track이라는 변수를 사용하는 식이지요.

//foo.jsvar track = 1

.

//bar.jsvar track = 2

이러면, 결국 가장 뒤에 쓰인 스크립트를 기준으로 변수이름이 적용되는 문제가 있어요. foo.js에서 사용한 변수 track은 씹히는 문제가 생기는 것이지요. track의 값은 언제나 2가 되겠지요.

둘째로, 의존성 관리도 문제였어요. 스크립트 태그를 두서없이 마음대로 붙여서는 안되었어요. 모듈의 의존성 순서를 전부 다 고려해서 script태그를 적재적소에 위치시켜야 했어요. 예를 들어서, 모듈 A가 모듈 B에 의존한다고 가정해보아요. 그렇다면, 모듈 B가 로드되고, 모듈 A가 로드 되어야 맞겠지요? 수십, 수백개의 모듈들에 대해서 이 모든 지난한 의존성 관리를 손으로 다 하겠다면, 그것은 개발자의 생산성을 떨어뜨리는 일이었을 것이에요. 정말 필요한 코드를 짜는 데에 시간을 쓰지 못하고, 의존성 관리에 시간을 다 썼을 것이에요.

셋째로, 로드 시간도 문제입니다. 스크립트 태그는 새로운 HTTP 커넥션을 필요로 해요. 더 정확히 말하자면, 스크립트 태그마다 HTTP 커넥션을 필요로 해요. 이게 무슨 문제인가 싶겠지만은, http 커넥션을 통해 자바스크립트 모듈을 다운받을 때, 클라이언트 유저의 브라우저는 하얗게 오랫동안 멈춰있게 되거든요. 스크립트 태그가 많으면 많을수록 유저는 더욱 오래 기다려야 했어요.

물론, 2015년에 HTTP/2가 등장했다는 점에서 현재는 로드 시간과 관련된 고민이 필요 없어진 것일 수도 있어요. 왜냐하면, HTTP/2에서는 병렬적으로 스크립트 태그를 로드하니까요. 하지만, 당시는 이 기술이 나오기 10년도 더 전이었어요.당시에 사용한 HTTP 커넥션은 병렬적이지 않고 직렬적으로 로드했어요. 파일 하나를 요청하려면, 반드시 한 번의 HTTP 커넥션이 필요했어요. script 태그에 파일 하나씩 총 100개의 script태그가 있었다면, 100번의 HTTP 커넥션이 필요했던 것이지요.

HTTP/2 커넥션은 멀티플렉싱을 통해 HTTP/1으로 했다면 여러번 해야 하는 요청을 하나로 묶어줄 수 있었어요. 위 그림으로 보자면, HTTP/2는 위 그림과 같이 한번으로 족했을 것이구요. 반면, HTTP/1은 파란색, 주황색, 초록색 커넥션 세번을 했어야 했겠지요. (링크)

이러한 이유로, script태그로 올리는 것은 스마트한 해결책이 아니었어요. 쓰고싶은 프레임워크와 모듈은 많아졌지만, 이를 감당할 수 있는 개발 패턴은 아직 없는 상황이었어요.

.

우리는 모듈화 기법을 만들 수 있을까요?

.

방금의 질문은 중요한 질문이니, 조금 더 명확한 언어로 구체화 시켜볼게요. 어떻게 하면, 우리는 수많은 모듈을 사용하면서도, (1) 각 모듈의 스코프 독립을 유지시키고, (2) 각 모듈의 의존 모듈을 명확하게 관리하면서도, (3) 세멘틱하게 임포트 시키면서, (4) 웹페이지를 방문한 고객이 쓸데없이 대기하는 시간을 최소화할 수 있을까요? 첨언하자면, (1)~(3)은 앞서서 언급한 모듈화를 위한 3개의 요건이라는 점을 다시 한 번 강조하고 싶고, (4)는 고객을 응대하는 웹개발자로서의 기본 마인드가 아닐까 언급해보고 싶어요. 예쁜 사이트는 빨라야죠.

그때, 절반의 solution을 외친 그룹이 하나 있었어요. 그 그룹의 명칭은 앞에서 잠깐 언급했던 commonJS 그룹이었습니다. 그들이 제시한 솔루션은, 자바스크립트 내에서도 JAVA에서 사용되는 import처럼 제대로된 모듈화 표준을 갖자는 것이었어요. (1)~(3)문제를 해결해야 한다면서요.

하지만, 절반의 해결책이었지요. 왜냐하면 이들은 웹 환경에 최적화된 모듈 시스템을 만드는 것이 목적이 아니었고 서버사이드의 모듈화 자체가 목적이었기 때문이에요. 너무 실망하거나 질책하지 마세요. 물병에 무려 물이 절반이나 차올랐다구요.

물병에 무려 물이 절반이나 차올랐다구요!(Photo by James Perez on Unsplash)

.

CommonJS, 절반의 해답을 외치다.

.

앞서서 미리 언급한 바 있듯이, CommonJS(이하 CJS)그룹은 Kevin Dangoor가 조직한 그룹이었습니다. 이 그룹의 목표는 서버사이드 모듈화 표준을 만드는 것이었습니다.

kevin dangoor가 자기 트위터 메인으로 올려둔 사진입니다.(링크)

2009년 당시를 비롯해서 그 이전 2~3년 동안 javascript를 웹브라우저 뿐만 아니라 서버 쪽에서도 사용하려는 노력이 이어졌었습니다. 2009년에는 V8 엔진을 근간으로 하는 nodeJS가 등장하기도 했었습니다. 이제, javascript를 대중화하고 널리 보급시키는 데에 하드코어 개발자들도 관심을 갖기 시작한 시점이라고 볼 수 있겠지요. AJAX, jQuery라는 좋은 프레임워크/패키지도 생겼고, V8이라는 좋은 엔진도 생겼지만, 여전히 남아있는 문제는 javascript가 scalable한 언어가 아니라는 점이었습니다. 이를 해결하는 데에 필요한 여러 요건들 중 중요한 하나가 모듈화였습니다.

CJS이전에는 제대로된 모듈화 기법이 없었습니다. global namespace에서의 변수 충돌을 피하기 위해 최소한의 스코프를 확보하기 위한 코딩 패턴들이 겨우 있었지요. addy osmani의[자바스크립트 디자인 패턴]이라는 책을 보면 이 당시에 사용되었던 여러 가지 스코프 확보를 위한 코딩 스타일이 나옵니다만, 그 중에서도 사람들이 많이 사용하곤 하는 두 가지 스타일이 있었어요.

.

모듈 황무지에는 어떤 코드 패턴이 있었는가?

.

하나는 IIFE 스타일이고, 다른 하나는 object interface 스타일이었습니다. IIFE와 object interface를 보기 전에, 잘생긴 Addy의 얼굴은 한번 보고 가야겠습니다.

TVN에서 방영했던 “지니어스” 의 어느 누군가(한의사 분이셨는데 이름이 기억이 안나요)를 닮은 기분이 듭니다…(링크)
이 책이 addy가 집필한 책입니다. 나름 JS계의 고전처럼 여겨지는 것으로 판단되어요. 많이들 이 책을 인용하더라구요.

IIFE를 간략히 설명하자면, 말 그대로 즉시 실행 함수입니다. 즉시 실행함수 개념은 함수 선언과 엮어서 알아두면 편합니다.

일반적으로 JS내에서 함수를 선언한다 함은 다음과 같습니다.

//IIFE_foo.jsfunction function_name_foo(){
// 함수 내에서 실행할 코드들이 담겨있겠지요?
}

하지만, 선언된 함수는 함수콜 없이는 실행되지 않습니다.

//IIFE_foo.jsfunction function_name_foo(){
// 함수 내에서 실행할 코드들이 담겨있겠지요?
}
function_name_foo() //이렇게 함수콜을 해서 실행을 합니다.

초기 웹에서는 굳이 JS 선언과 실행을 나눌 필요가 없었습니다. 그래서 즉시실행 함수 표현식을 사용하게 됩니다. 즉시 실행 함수 표현식은 그 의미를 두 개로 쪼갤 수 있어요. 하나는 즉시 실행이고, 다른 하나는 함수표현식이지요. 함수 표현식은 다음과 같으며, 리턴값으로 함수를 내보내줍니다.

//IIFE_bar.js(function function_name_bar(){
//
}) //여기까지가 함수표현식.

그렇게 리턴된 함수에 괄호를 붙여 실행을 시켜주는 식입니다.

//IIFE_bar.js(function function_name_bar(){
//
})() //괄호까지 붙여주면, 즉시실행함수표현식

이것이 IIFE 패턴입니다.

IIFE는 스코프 문제를 해결해주었습니다. 하지만, 바로 실행해야한다는 점에서 모듈화의 해결책은 아니었습니다. 왜냐하면 다른 IIFE에서 어떤 변수나 함수를 가져올 수 없었기 때문이지요. 또한, 디펜던시 관리도 안되었습니다. 여전히 메뉴얼하게 각 괄호 안에다가 모두 써붙였어야 했습니다.

다음으로 알아봐야할 것은 object interface 패턴입니다. object interface 스타일 코드를 말하자면, 리턴값으로 object를 내보내주는 식입니다.

//object_interface_foo.jsfunction function_name_foo(){
function function_inside_foo(){
//}
var variable_inside_foo = 4
return {function_inside_foo: function_inside_foo,
variable_inside_foo: variable_inside_foo
}
}

필요한 모든 함수를 function_name_foo()안에 숨겨두고, 필요한 함수들만 리턴해서 쓰는 방식입니다.

object interface 스타일은 함수로 스코프를 확보한 후, 리턴값으로 object를 내어주는 식이었습니다. 이 방식 역시 스코프 문제는 해결했고, 즉시 실행되는 함수가 아니라는 점에서 다른 스코프에서 함수를 가져올 수 있었습니다. 하지만, 글로벌 네임스페이스를 침범하는 문제가 있었습니다. 위의 사례로 예를 들자면, 최상위 스코프에서 정의된 function_name_foo()는 글로벌 네임스페이스를 침범하는 문제가 있습니다. 변수들이 많아진다면, 변수 충돌이 생길 수 있고, 결국 object interface 방식은 IIFE와 마찬가지로 scalable하지 않은 방식이지요.

모듈화의 3요건을 충족시킬 수 있는 새로운 스타일이 필요했습니다. CJS는 이 요건을 충족시키는 새로운 방식을 제시했던 것입니다. 이 방법은 node.js를 사용하는 사람들 입장에서는 익숙할 것입니다. 일단 export를 할 함수를 정의합니다.

//CJS_export_foo.jsmodule.exports.foo = function() {
//함수 내 정의될 코드들이 담기겠지요?
}

그리고는 해당함수를 require 명령어로 임포트합니다.

//CJS_import_foo.jsvar foo = require('./CJS_export_foo.js').foo

module.exports, require()이 돌아가는 방식은 모듈화의 3요건을 충족시켰습니다.

첫째, 모듈 파일마다 스코프가 설정되므로, 각 모듈을 불러오는(require하는) 상위 파일 내에서 변수 충돌이 없다면, 이 시스템에서는 변수 충돌이 발생하지 않았습니다. 사람의 인지 능력 범위 안에서 변수를 관리할 수 있도록 문제를 해결해준 방식이었습니다. 게다가 글로벌 네임스페이스에 대한 침범도 없었지요.

둘째, <script> 태그로 임포트하는 것처럼 파일 덩어리를 임포트하는 것이 아니라 필요한 함수나 변수를 가져올 수 있었습니다.

셋째, 의존성 관리 역시 편리해졌습니다. module.exports, require 명령어를 통해서 의존되어있는 파일이나 패키지들을 재귀적으로 관리할 수 있게 되었습니다.

CJS가 나온지 1년도 채 안되어서 나온 서버사이드 JS의 거두 nodeJS도 이 방식을 차용하게 되었습니다. 물론, 지금은 뼈대만 남고 그 스펙 및 방식은 다르지만 CJS의 숨결은 충분히 느낄 수 있습니다. [**CJS에서는 object만 exports에 넣을 수 있었는데, node는 여러 형식을 넣을 수 있다는 점 역시 스펙 차이 중 하나입니다.]

하지만, CJS의 대안은 절반의 해결책이었습니다. 왜냐하면, CJS의 방식은 blocking 방식으로 웹브라우저에서 직접 활용하면 너무 느렸기 때문입니다.

blocking 방식을 간단히 이해해보는 차원에서 예를 들어보겠습니다. 개발자들이 node.js에서 CJS스타일로 코드를 여러줄 작성하면, 각각의 코드라인들이 line by line으로 읽혀지고 수행되길 기대합니다. 아래와 같이 코드를 작성한다면, 언제나, foo.js가 먼저 require 되고 그 다음 bar.js가 require되길 기대합니다.

//main.jsconst foo = require("foo.js")
const bar = require("bar.js")
//bar가 foo보다 먼저 읽히는 경우는 없습니다.

이것은 require 함수가 blocking 함수이기 때문에 가능합니다. 자바스크립트 인터프리터는 require(“foo.js”)를 읽으면 현재의 main.js의 첫번째 코드라인에서 잠시 멈추었다가, 컨텍스트를 바꿔서 require()안의 과정을 모두 끝내고 그 리턴값을 foo안에 넣고나서야, 그 다음 코드라인을 읽을 수 있습니다.

foo.js와 bar.js 등의 임포트하는 파일들을 디스크에서 읽어오는 경우, 이러한 방식은 큰 문제가 없습니다. 빠르니까요.

하지만, 웹브라우저에서는 서버에 리퀘스트를 날려서 파일들을 읽어오게 되는데 이 경우 속도측면에서 문제가 발생합니다. foo.js를 require할때 http request 한 번, bar.js를 require할 때 http requrest를 또 한 번 사용하게 되어요. 그러면 통신 비용에 부하가 많이 걸리게 됩니다.

예나 지금이나, 유저는 인터넷 브라우저 창에서 아무런 변화 없이 하얀 화면이 오래 지속되는 상황을 기다리지 않으니, 이 문제는 당시에도 큰 문제였을 것입니다.

이것이 serverside 방식을 곧이 곧대로 web 환경에 적용하기 어려운 이유입니다. IIFE를 비롯한 태초의 모듈화 방식은 모듈화의 3대 요건을 충족시키지 못하고, 그렇다고 CJS는 웹환경에 적용하기 어렵고. 도대체 대안이 있을까요?

물론 있었습니다. 브라우저의 event loop 덕분에 async하게 돌아갈 수 있는 환경이었어요. 특히 2008년에 도입된 V8 크롬 엔진 덕분이었죠. async하다고 함은, 논블로킹이라서 실행될 때까지 기다리지 않고 함수를 큐에 던져두고 여러 개의 작업들이 동시적으로 수행될 수 있다는 것이에요. 이 성질을 적극적으로 이용하면, 모듈화 문제는 또 다른 방식으로 해결될 수 있었던 것입니다.

브라우저의 event loop에 대한 구체적인 설명은 다음의 두 블로그에서 정말 명쾌하게 설명해주셨으니, 확인해보시길 바랍니다.

CJS 그룹 내에서는 다행히 반골들이 있었습니다. 서버사이드에서 활용할 목적으로 모듈화 기법을 만드는 것이 1차 목표가 되어서는 안되며, 웹브라우저에서 활용할 모듈화 기법이 1차 목표라고 주장한 사람들이 있었습니다. 브라우저의 non-blocking, async 성질을 적극적으로 활용해서 웹페이지의 퍼포먼스를 확보해야한다는 사람들 말이에요. 이들은 CJS로부터 결국 독립하게 되고, 웹에 조금 더 적합한 방식으로 코드 패턴을 완성했습니다. 바로 AMD였습니다. AMD 개발자는 물병의 나머지 절반을 채워주었습니다.

.

AMD, 나머지 절반을 채우다.

.

AMD는 이름에서부터 웹에 적합하게 모듈화 시스템을 만든다는 철학이 담겨있습니다. Asynchronous Module Definition. Async한 브라우저 환경에 맞게 모듈을 정의하고 올린다는 것이지요.

AMD의 해결책은 브라우저의 event loop이 제공해준 async 특성을 적극적으로 활용하는 것이었습니다. 다음과 같은 방식이었어요.

//main.jsdefine(['./foo.js', './bar.js'], function(foo, bar){
//이 안에서 foo와 bar를 통해 코드를 짤 수 있었어요.
})

이 방식은 define 내에 코드를 작성함으로써 스코프 분리도 가능했습니다.(모듈화의 조건1 충족.)

//main.js*******GLOBAL NAMESPACE******
define(['./foo.js', './bar.js'], function(foo, bar){
*******LocaL NAMESPACE******
})

또한 세멘틱한 모듈 활용도 가능했어요.(모듈화의 조건2 충족.) 예를 들면, 다음과 같이 쓰는 거죠.

//main.jsdefine(['./foo.js', './bar.js'], function(foo, bar){
const functionA_in_foo = foo.functionA
//이 안에서 foo와 bar를 통해 코드를 짤 수 있었어요.
})

그리고 보다시피 디펜던시 관리도 효과적으로 수행했습니다.(모듈화의 조건3 충족.) 어떤 함수를 돌리기에 앞서서 필요한 모듈들을 한꺼번에 가져오는 식이었지요.

define(['./foo.js', './bar.js'], function(foo, bar){
//이 안에서 foo와 bar를 통해 코드를 짤 수 있었어요.
})

여기까지는 CJS도 제공한 기능이지요? 게다가 CJS의 느린 퍼포먼스까지도 AMD는 해결하였습니다. AMD는 define의 첫번째 인자로 받아들인 [‘./foo.js’, ‘./bar.js’] 배열 내부의 모듈들을 async하게 올릴 수 있었기 때문입니다. 이로써, 웹에 아주 적합한 모듈화 기법이 완성될 수 있었습니다.

웹에 모듈을 올리기 위한 아주 기초적인 퍼즐조각은 이제 다 맞췄습니다.

.

하지만, 백가쟁명은 이제 시작입니다.

.

AMD 그리고 이에 기반한 모듈 로더 require.js 에도 문제가 있었습니다. 기술적인 문제라기보다는 레거시의 문제 혹은 대세의 문제였습니다.

첫번째 문제는 서버사이드에서 쓰려고 만든 함수를 브라우저에서 쓰려면 AMD 형식에 맞춰서 다 바꿔주어야 하는 “귀찮음”이 바로 그 문제였습니다. 즉, 같은 내용을 서버사이드 함수로도 쓰고 AMD 형식으로도 써야 합니다. 물론, r.js라는 변환 스크립트를 통해 CJS스타일을 AMD스타일로 변환하는 것을 도와주기는 했습니다만, 모든 상황을 완벽하게 커버하지는 못했습니다.

두번째 문제로, cjs와 amd가 앞다퉈 모듈화기법의 베이스를 만든지 1년 채 안된 2010년 즈음, npm이 등장하고 그때 이후로 많은 개발자들이 자발적으로 node.js의 세계에 자신이 만든 멋진 모듈들을 업로드하기 시작했다는 점입니다. 이들은 대부분 CJS 형식으로 만들어졌습니다. CJS 스타일로 만든 코드를 AMD 스타일로 변환해야할 상황들이 더욱 많아졌습니다.

붉은 선이 npm에 등록된 모듈의 수 입니다. 현재 등록된 모듈의 수만 해도 70만개로, npm 릴리즈 초창기에 해당하는 7년 전에 비해 140배 성장률을 기록하고 있군요.

많은 사람들은 CJS를 사용하고 있었고, 또 그만큼 많은 사람들이 웹에 올리기 위해 AMD 스타일에 맞춰서 변환하고 있었습니다. 무엇인가 변화가 필요했습니다. 귀찮은 과정을 쳐내는, 혹은 통합시켜주는 툴이 필요했습니다.

이 투트랙 방식을 일원화 할 수는 없을까요? 코드는 서버사이드에서 commonJS 형식으로 짜고, 로드 상황을 고려해서 AMD 형식 로드 코드를 따로 써줘야 하는 과정을 하나로 줄일 수는 없을까요? 이 문제를 해결하기 위해 등장한 것이 모듈 번들러, Browserify였습니다.

.

browserify의 이후의 이야기는 다음 글에 싣겠습니다! 글의 말미가 되었으니, 다시 한번 요약해볼게요.

.

1부 TL;DR

.

JS가 1996년에 탄생한 후, 근 10년동안은 모듈이라는 것이 등장조차 하지 않았습니다. IIFE 혹은 object interface 등의 모듈화 패턴 정도가 있었을 뿐이었습니다. 하지만, 2009년 CJS가 등장하면서 서버사이드 모듈화의 규칙이 마련되었고, 같은 해 nodeJS가 CJS를 활용해서 모듈화 시스템을 구현했습니다. 이러한 방식은 당시에 웹에서 직접 적용하기에는 부적합했습니다.

.

(1) CJS 모듈화 기법의 웹 부적합성 문제

CJS처럼 sync한 방식으로 로드할 때의 문제는 여러 번의 http request가 필요하다, 즉 request/response에 너무 많은 시간이 걸린다는 점이었습니다. 그 문제를 해결하고자 AMD 측에서는 async하게 모듈들을 로드할 수 있는 방법을 고안했습니다.

(2) AMD 모듈화 기법이 대세를 거스르는 문제

하지만 그 상황에서 또 발생했던 문제는 투트랙 개발이었습니다. 개발자들이 서버사이드용 코드, 브라우저용 코드를 따로 작성해나가고 있다는 점이 문제였습니다. CJS 스타일 기반의 nodeJS로 자바스크립트기반 서버를 개발하고, npm으로 패키지를 모듈로 장착하고 싶었던 개발자들에게 AMD는 계륵이었습니다.

(3) CJS 모듈화 기법을 쓰면서도, 퍼포먼스를 확보하려면?

nodeJS의 모듈화기법에서 벗어나지 않으면서도 request / response에 걸리는 시간을 획기적으로 줄일 수는 없었을까요.

입코딩을 해보자면, 여러 차례 http request 날리는 것이 문제라면 한 번 날리는 것으로 바꿔주면 됩니다. 대신, 한 번의 request를 통해서 모든 모듈들을 한꺼번에 로드하면 됩니다.

입코딩은 참 쉽지만, 어디서부터 시작해야하는 것인지 당황스러운데요. 방금 입코딩을 실제 코드로 구현한 집단은 2012년도에 등장합니다. CJS, AMD가 탄생했던 2009년으로부터 3년 후였고, nodeJS가 등장한지 약 2년 후였고, Node Package Manager가 등장한지 약 1년 후였습니다. 바로, Browserify입니다.

.

이만 개발자 Chullin이었습니다. 박수와 댓글은 큰 힘이 됩니다.

--

--