JavaScript 진화사: ES1부터 최신 ES14, CommonJS에서 ESM까지
프론트엔드 개발을 하다 보면 import와 export, async/await, let, const 같은 문법을 자연스럽게 사용하곤 한다. 그런데 이 기능들이 언제, 왜 생겨났는지 깊이 이해하면 더 나은 코드 설계와 기술 선택을 할 수 있다. 그 중심에 바로 ECMAScript(ES) 가 있다.
1. ECMAScript란?
ECMAScript는 JavaScript의 표준 사양이다. JavaScript는 브라우저, Node.js 등 다양한 환경에서 실행되지만, 그 문법과 동작 규칙을 통일하기 위해 만들어진 것이 ECMAScript이다. 이 표준은 ECMA International이라는 기관에서 관리하며, 명세 번호는 ECMA-262이다.
1. ECMAScript의 역할
초기 자바스크립트는 브라우저마다 동작이 달라, 코드 호환성이 낮았다. 이를 해결하기 위해 1997년에 ECMAScript가 등장했고, 자바스크립트 엔진(V8, SpiderMonkey, JavaScriptCore 등)이 이를 구현하는 방식으로 발전했다.
2. CommonJS의 등장 (Node.js 환경)
2009년 Node.js가 등장하면서, 자바스크립트를 브라우저 밖에서도 실행할 수 있게 되었다. 하지만 ECMAScript 표준에는 모듈 시스템이 없었기 때문에, Node.js는 자체적으로 CommonJS라는 모듈 시스템을 도입했다.
// CommonJS 예시 (Node.js)
const fs = require('fs');
module.exports = { readFile: fs.readFileSync };
- 장점: 단순하고 서버 환경에서 효율적
- 단점: 동기적 로딩 기반이기 때문에 브라우저에서는 비효율적이며, *정적 분석이 어려워 **트리 셰이킹(tree-shaking)이나 ***번들 최적화가 불가능했다.
*정적 분석: 빌드나 실행 전에, 즉 코드를 실제로 실행하지 않고 코드 자체를 분석하는 것이다.
**트리 셰이킹: 사용되지 않는 코드를 제거해 번들 크기를 줄이는 최적화 기법이다.
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// main.js
import { add } from './math.js';
console.log(add(2, 3)); // subtract는 번들에서 제거됨!
***번들 최적화: 모듈 시스템(특히 ESM)을 통해 모듈 간 의존성을 정적으로 분석하고, 불필요한 코드 제거, 코드 분할(Code Splitting), 캐싱 최적화 등을 적용하여 번들 크기와 로딩 속도, 런타임 성능을 개선하는 기법이다.
3. ECMAScript Modules (ESM)의 도입
프론트엔드 규모가 커지면서 브라우저에서도 모듈 시스템이 필요해졌다. CommonJS는 서버(Node.js) 환경에서 설계되어, 모듈을 동기적으로 로드한다. 하지만 브라우저는 여러 파일을 비동기로 병렬 로드하는 것이 효율적이기 때문에, ECMAScript는 2015년 ES6부터 표준 모듈 시스템인 ESM (ECMAScript Module)을 도입했다.
// ESM 예시 (브라우저/Node.js)
import fs from 'fs';
export const readFile = fs.readFile;
- 비동기 로딩 가능 → 브라우저에서 병렬 로드 지원
- 정적 분석 지원 → 트리 셰이킹, 번들 최적화 가능
- 표준화된 문법 → 브라우저와 Node.js 모두에서 동일하게 사용 가능
4. ECMAScript, CommonJS, ESM 요약
| 구분 | ECMAScript | CommonJS | ESM (ES6) |
| 등장 시기 | 1997 | 2009 | 2015 |
| 역할 | JS 언어 표준 | Node.js 모듈 시스템 | 표준 모듈 시스템 |
| 환경 | 모든 JS 환경 | Node.js | 브라우저 + Node.js |
| 로딩 방식 | - | 동기적 | 비동기적 |
| 문법 예시 | - | require, module.exports | import, export |
ECMAScript는 JavaScript의 기반 표준이며, CommonJS는 그 위에서 동작한 Node.js 전용 모듈 시스템이었다. 이후 브라우저 환경까지 고려한 ESM이 ECMAScript에 통합되면서, 현대 자바스크립트는 환경에 관계없이 하나의 표준 모듈 시스템을 사용하게 되었다.
5. Node.js가 ESM을 도입한 이유
| 구분 | CommonJS (CJS) | ESM (ECMAScript Module) |
| 로드 방식 | 동기적 (require) | 비동기적 (import) |
| 분석 가능성 | 정적 분석 불가 | 정적 분석 가능 |
| 환경 | Node.js 중심 | 브라우저·Node.js 모두 |
| 최적화 | 트리셰이킹 불가 | 트리셰이킹, 번들 최적화 가능 |
| 문법 예시 | const fs = require('fs') | import fs from 'fs' |
1. 표준화된 모듈 시스템으로의 통합
CommonJS는 Node.js 내부에서 독자적으로 만들어진 비표준 모듈 시스템이었다. 하지만 ECMAScript(ES6)에서 공식적으로 import / export 문법을 표준화하면서, “전 세계 자바스크립트 코드가 동일한 방식으로 모듈을 사용할 수 있어야 한다”는 필요성이 커졌기 때문에 Node.js도 결국 표준을 따르는 방향으로 진화한 것이다.
즉, 단순히 “통일”이 아니라, “브라우저와 서버 간의 호환성 확보 + 언어 차원에서의 일관성”이 목적이었던 것이다.
2. 프론트엔드와 백엔드 간의 코드 호환성
프론트엔드에서는 이미 import/export (ESM)가 기본이 되었기 때문에 라이브러리나 유틸 함수가 서버(Node.js)에서도 같은 문법으로 사용되면 훨씬 개발 효율이 높아진다.
// ✅ ESM 기반: 브라우저와 Node.js에서 동일하게 동작
import fetch from 'node-fetch';
import { formatDate } from './utils/date.js';
과거에는 같은 코드를 require()로 다시 써야 했지만, 이제는 한 가지 문법으로 양쪽에서 재사용 가능하다.
3. 정적 분석 및 최적화 지원
CommonJS는 동기적 로딩이라 실행 시점에 어떤 모듈이 필요한지 알아야 했다. 이 때문에 트리 셰이킹, 코드 스플리팅 같은 최적화가 불가능했다. 반면 ESM은 정적 분석(static analysis) 이 가능해서, 빌드 도구(Webpack, Vite, Rollup 등)가 코드를 미리 분석하고 불필요한 코드를 제거할 수 있다.
// ✅ ESM은 어떤 모듈이 import되는지 미리 알 수 있음
import { useState } from 'react'; // 필요없는 부분은 번들에서 제거 가능
4. Node.js 내부 아키텍처 개선
Node.js는 내부적으로 CommonJS를 유지하기 위해 복잡한 “require 캐시” 시스템을 관리했지만, ESM 도입 이후 모듈 해석 로직이 단순화되고, 비동기 I/O와의 궁합도 좋아졌다. 특히 dynamic import(import())를 통해 런타임에서 모듈을 지연 로드할 수 있게 되었다.
// ✅ 런타임에서 모듈 동적 로드 가능
if (process.env.NODE_ENV === 'dev') {
const { debug } = await import('./dev-tools.js');
debug();
}
5. 점진적 전환과 하위 호환성
Node.js는 여전히 CommonJS(require)를 지원하는데, 단, 새 프로젝트는 ESM(import/export)을 기본으로 권장하며, 오래된 패키지와는 .cjs, .mjs 확장자 또는 type: module 설정으로 공존 가능하게 설계했다.
// package.json 예시
{
"type": "module"
}
즉, Node.js가 ECMAScript를 도입한 이유는 단순한 통일이 아니라, 표준화, 호환성, 성능 최적화, 그리고 개발자 경험(DX)을 모두 개선하기 위한 진화였다. 지금은 CommonJS와 ESM이 공존하지만, 미래의 자바스크립트 생태계는 완전히 ESM 중심으로 이동하고 있다.
2. 왜 ECMAScript의 변화를 알아야 할까?
ECMAScript의 변화와 최신 문법을 이해하면, 단순히 코드를 작성하는 수준을 넘어 언어를 설계한 의도와 사용 패턴을 이해할 수 있다. 이를 통해 개발자는 다음과 같은 장점을 얻는다.
- 최신 문법 선택의 이유를 설명할 수 있다.
- 예: async/await가 등장한 배경을 알면, 단순히 편리해서 쓰는 것이 아니라 콜백 지옥을 피하고 비동기 코드를 가독성 있게 관리하기 위해 설계된 기능임을 설명할 수 있다.
- 호환성과 트랜스파일링 전략을 명확히 판단할 수 있다.
- 예: ES6 모듈(ESM)을 브라우저와 Node.js에서 사용하려면, Babel이나 SWC 같은 트랜스파일러를 활용해 호환성을 확보해야 한다는 것을 이해한다.
- 어떤 기능은 최신 엔진에서만 지원되고, 어떤 기능은 트랜스파일이 필요하다는 판단이 가능하다.
- 런타임 변화와 모듈 시스템 전환에 대응할 수 있다.
- 예: CommonJS에서 ESM으로 전환되는 흐름을 이해하면, 패키지 선택, 빌드 설정, 배포 전략 등을 합리적으로 결정할 수 있다.
- 단순히 문법만 사용하는 개발자가 아니라, 생태계를 이해하고 대응할 수 있는 개발자가 된다.
- 언어를 깊이 이해한 엔지니어로 성장한다.
- 단순히 문법을 외워 사용하는 것을 넘어, “왜 이 기능이 생겼고, 어떤 문제를 해결하기 위해 설계되었는가?”를 이해한다.
- 이는 코드 리뷰, 설계 결정, 문제 해결 능력으로 직결된다.
정리하면, ECMAScript의 역사를 이해하는 것은 단순히 문법 습득이 아니라, 언어 철학과 진화, 생태계의 변화까지 이해하는 능력을 의미한다. 이 능력은 코드를 작성하는 개발자를 넘어 언어와 플랫폼을 이해하는 엔지니어로 성장하게 만든다.
3. ECMAScript 주요 버전별 변화 요약
| 버전 | 연도 | 주요 변화 | 특징 요약 |
| ES1 | 1997 | 최초 표준화 | 기본 문법, 타입, 객체 정의 |
| ES3 | 1999 | try/catch, 정규표현식 | 브라우저 간 호환성 시작 |
| ES5 | 2009 | strict mode, JSON, Array 메서드 | 근대 JS의 기반 |
| ES6 (ES2015) | 2015 | let, const, class, arrow function, Promise, import/export | 현대 JS의 대도약 🚀 |
| ES7 (2016) | 2016 | Array.includes(), ** 연산자 | 문법적 미세 개선 |
| ES8 (2017) | 2017 | async/await, Object.entries, Object.values | 비동기 코드 혁신 |
| ES9 (2018) | 2018 | for-await-of, Rest/Spread 개선 | 문법 확장, 비동기 반복 |
| ES10 (2019) | 2019 | flat(), trimStart/End, optional catch binding | 배열·문자열 편의성 향상 |
| ES11 (2020) | 2020 | ?., ??, dynamic import() | DX 향상, ESM 확장 |
| ES12~14 | 2021~2023 | WeakRef, top-level await, findLast() 등 | 지속적 최적화 및 ESM 완성 |
버전별 예시 코드
1. ES3 (1999) — 예외 처리
try {
throw new Error("에러 발생!");
} catch (e) {
console.log(e.message);
}
2. ES5 (2009) — 배열 메서드 & Strict Mode
"use strict";
const arr = [1, 2, 3];
const doubled = arr.map(n => n * 2);
console.log(doubled); // [2, 4, 6]
3. ES6 (2015) — 현대 JS의 시작
import { sum } from './math.js';
const add = (a, b) => a + b;
console.log(sum(2, 3)); // 5
4. ES8 (2017) — async/await 도입
async function fetchData() {
const res = await fetch('https://api.example.com');
const data = await res.json();
console.log(data);
}
fetchData();
5. ES11 (2020) — Optional Chaining & Nullish Coalescing
const user = { profile: { name: "Jay" } };
console.log(user.profile?.name ?? "Unknown"); // Jay
6. ES14 (2023) — findLast()
const arr = [1, 2, 3, 4, 5];
console.log(arr.findLast(num => num % 2 === 0)); // 4
4. 마무리
ECMAScript의 발전은 단순히 “문법이 늘어난 것”이 아니라 자바스크립트가 진짜 프로그래밍 언어로 성숙한 과정이라고 할 수 있다. 초기에는 브라우저 간 호환성 문제를 해결하고, Node.js 환경에서는 CommonJS를 통해 모듈 시스템을 구현했다. 그 뒤 브라우저와 서버 환경을 아우르는 ESM이 도입되면서, 자바스크립트는 표준화된 모듈 시스템과 정적 분석 기반 최적화, 비동기 로딩 지원을 갖춘 현대적 언어로 진화했다.
이 과정을 이해하면 단순히 문법을 사용하는 개발자를 넘어, 언어 설계 의도를 이해하고, 환경 변화에 대응하며, 최적화와 코드 재사용을 고려할 수 있는 엔지니어로 성장할 수 있다. 따라서 import/export, async/await, let/const 같은 최신 문법을 접할 때, “왜 이 기능이 생겼는가?”를 생각하며 코드를 작성하는 습관이 중요하다.
결국 ECMAScript의 역사를 이해하는 것은, 프론트엔드와 백엔드, 브라우저와 Node.js 환경을 모두 아우르는 견고한 자바스크립트 개발 능력을 갖추는 가장 빠른 지름길이 된다.