개요
프론트엔드 개발을 막 접했을 시점에 VScode에서 이런 경고 문구를 보게 되었다. ▼
JS에 대해 잘 모르기도 했고, 애초에 ES 모듈이 뭔지 그리고 CommonJS 모듈이 뭔지도 몰랐기도 하고 잘 동작하기에 그냥 넘어갔었다. 그런데 이런 사소한 것들을 그냥 지나치면 나중에 큰 문제가 될 수 있을 거 같아 한 번 찾아보면서 정리를 해봤다.
CommonJS
CommonJS란
CommonJS는 Node.js의 초기 모듈 시스템으로, 모듈을 로드하고 관리하기 위해 설계된 시스템이다. 주로 서버사이드에서 사용이 된다. 상당히 레거시한 느낌이 강하지만 Node.js 생태계 전반에서 사용되고 있으며 많은 라이브러리에서 지원이 된다. (개요에서 필자가 뭔지도 모르고 썼음에도 동작할 수 있었던 이유다.)
CommonJS사용
CommonJS는 아래와 같이 작성이 된다. `module.exports = add`로 exports하고 `require('./add')`로 모듈을 로드한다. ▼
/**** add.js ****/
function add(a, b) {
return a + b;
}
module.exports = add;
/**** app.js ****/
const add = require('./add');
console.log(add(1, 2));
CommonJS의 몇가지 특징
단일 진입점
`require` 키워드로 다른 모듈을 가져오고, `module.exports`로 데이터를 내보낸다. CommonJS는 모든 파일을 모듈로 간주하고 독립성을 제공한다.
동기적 모듈 로드
CommonJs는 동기적으로 모듈을 로드한다. 모듈을 require하는 시점에 해당 파일을 읽은 다음, 모든 코드가 실행된다. 이 동기적 모듈 방식을 하면 로드 순서를 보장해준다. 개요에서 주로 서버사이드로 사용이 된다고 서술했는데, 서버사이드로 사용되는 이유가 바로 이 동기적 모듈 방식 때문이다. 의존관계가 복잡한 서버 환경에서는 동기적 모듈 로딩이 이점이 된다. 하지만 브라우저 환경에서는 네트워크 지연으로 인해 동기적 로딩이 비효율적일 수 있다.
모듈 export
CommonJS는 모든 모듈을 기본적으로 하나의 객체로 내보낸다. 전체 모듈을 하나의 객체로 내보내기에 받을 때도 전체 모듈을 받아야 한다. CommonJS는 하나의 객체로 모듈을 내보낼 때 동적으로 내보낼 수 있다. 예를 들어, 조건에 따라 다른 값을 내보낼 수 있다.
env값에 따라 다른 모듈을 내보낸다고 하면, 아래와 같이 분기를 통해 다른 모듈을 내보낼 수 있다. CommonJS는 런타임에 분석이 되기 때문에 이와 같은 분기에 따른 export가 가능하다. ▼
if (process.env.NODE_ENV === 'production') {
module.exports = productionApi;
} else {
module.exports = developmentApi;
}
즉시 실행
CommonJs는 모듈을 `require()`로 동적으로 로딩하고, 즉시 실행할 수 있다. 아래와 같이 require()로 조건에 맞는 모듈을 실행하는 것이 가능하다. 아래와 같이 특정 조건에서 모듈을 로딩하고 즉시 실행시킬 수 있다. ▼
if (someCondition) {
const myModule = require('./myModule');
}
그 외에도 특징들이 더 있지만, 중요한 특징들을 다루면 이정도가 된다.
ES 모듈
ES모듈이란
ES모듈은 ECMAScript(자바스크립트 표준)에서 공식적으로 정의된 모듈 시스템으로, 브라우저와 Node.js 환경 모두에서 네이티브로 지원된다. 자바스크립트의 모듈화를 통해 코드의 재사용성을 높이고 유지보수를 용이하게 한다. ES모듈은 전역 스코프를 오염시키지 않으며, 정적 분석이 가능한 구조를 가진다.
ES모듈 사용
ES모듈은 export와 import로 작성된다. `export`로 데이터를 내보내고, `import`로 모듈을 로드한다. ▼
/***** add.js *****/
function add(a, b) => {
return a + b;
}
export default add;
/***** app.js *****/
import add from './add'
console.log(add(1, 2));
ES모듈의 몇가지 특징
정적 구조
ES 모듈은 정적으로 분석된다. 즉, 모듈이 어떻게 연결되는지(의존성 그래프)를 컴파일 단계에서 알 수 있다. 이로 인해 트리 셰이킹(Tree Shaking, 사용하지 않는 코드를 제거하는 최적화하는 것을 말한다)이 가능하며, 번들 크기를 줄이고 성능을 최적화할 수 있다.
비동기적 로딩
ES 모듈은 브라우저에서 네이티브로 비동기 로딩을 지원한다. `<script type="module">`로 선언된 스크립트는 브라우저가 자동으로 병렬 다운로드하여 성능을 개선한다. 브라우저 환경에서는 병렬적 로딩이 사용자 경험, UX도 개선해주기에 좀 더 적합한 방식이라고 할 수 있다.
스코프 격리
ES 모듈은 파일 단위로 자체적인 스코프를 가진다. 모듈 내부의 변수는 기본적으로 외부에서 접근할 수 없으며, export된 것만 접근 가능하다. 전역 스코프 오염을 방지하여, 대규모 프로젝트에서도 안전한 코드 구조를 제공한다.
여기까지만 보면 'CommonJS는 스코프 격리가 없는건가?' 라고 생각할 수 있는데 CommonJS도 스코프 격리가 있다. 하지만 ES모듈과는 다르게 전역 스코프 오염을 막을 수 없다. 전역 스코프 오염에 대해서는 다른 포스트에 좀 더 자세하게 다뤄보겠다.
브라우저 네이티브 지원
ES 모듈은 브라우저에서 네이티브로 실행된다. Node.js도 ES모듈을 지원하며, 번들링 없이 바로 사용할 수 있다. 앞에서 나온 이야기지만, 브라우저에서는 <script type="module">을 사용하여 모듈 파일을 로드한다.
그 외에도 특징들이 더 있지만, 중요한 특징들을 다루면 이정도가 된다.
CommonJS도 꽤 쓸만한데 왜 ES모듈로 전환되었을까?
표준화와 호환성의 문제
이렇게만 보면 CommonJS에 치명적인 결함이 있다거나 못쓸 정도로 성능에 문제가 있다거나 하는게 아닌데 요즘에는 ES모듈로 넘어가려는 것일까? 그 중 가장 첫번째 문제는 표준화와 호환성이다.
CommonJS는 처음 나왔을 당시 Node.js만의 독자적인 모듈 시스템으로 나왔기에 브라우저에서 사용하는데에 문제가 있었다. (이 부분은 따로 포스팅을 해보겠다.) 이로 인해 별도의 번들링 과정(Webpack)이 추가되어야했다.
이 부분을 ES모듈이 해결해줄 수 있다. ES모듈은 ECMAScript(자바스크립트의 공식 표준)에서 정의된 모듈 시스템이며, 브라우저와 Node.js 모두에서 네이티브로 지원되기에 브라우저에서 별도의 번들링 과정 없이 사용할 수 있다.
동기적 로딩
이 부분의 문제로 인해 표준화와 호환성의 문제가 생긴 것이기도 하지만, 동기적으로 모듈을 로딩한다는 것 자체가 네트워크 환경에서 좋지 않은 방식이다.
만약 대용량 이미지가 다수 존재하는 글을 로딩한다고 해보자. 그리고 사진이 가장 위에 있어서 제일 먼저 로딩된다고 하면 우리는 해당 사진이 전부 로딩될 때 까지 아무것도 할 수 있는게 없다. 사용자 입장에서는 뭐라도 빨리 보고 싶은데, 사진이 로딩조차 안되니 아무것도 할 수 없이 그저 기다리기만 해야한다.
하지만 비동기적으로 로딩하게 되면 로딩이 짧은 글들을 먼저 전부 로딩이 될 것이고, 용량이 큰 사진들은 느리지만 계속 로딩이 진행이 될 것이다. 이러면 사용자는 글의 모든 요소를 기다리지 않고, 글을 읽으며 사진을 기다릴 수 있게 된다. 이런 면에 있어서 동기적 로딩은 브라우저 환경에서 좋지 못하다는 것이다.
트리 셰이킹 지원
CommonJS는 런타임에 모듈을 로드하고 실행한다. 이 방식에서는 코드의 사용 여부를 컴파일 단계에서 확인할 수 없어, 불필요한 코드가 포함될 가능성이 높다. 개발자의 실수든 로직에서 생기는 예기치 못한 불필요한 코드들이 성능에 그대로 반영이 될 수 있다.
그에 비해 ES모듈은 정적 구조를 가지고 있어, 컴파일러가 어떤 모듈과 코드가 사용되는지 미리 분석할 수 있다. 이를 통해 사용되지 않는 코드를 트리 셰이킹하여 번들 크기를 줄이고 성능을 개선할 수 있다.
스코프 관리 및 전역 충돌 방지
앞에서 잠깐 스코프 관리에 대해 언급을 하면서 이 부분에 대해 간략하게 이야기를 했었다. CommonJS는 모듈마다 독립적인 스코프를 가지지만, 전역 컨텍스트와의 상호작용에 따라 충돌이 발생할 여지가 있다. 이를 전역 스코프 오염이라고 한다.
ES 모듈은 파일 단위로 스코프를 격리하고, import와 export를 사용하여 명확하게 의존성을 정의한다. ES모듈의 이런 방식은 전역 스코프를 오염시키지 않으므로, 대규모 프로젝트에서 더 안전하다.
CommonJS의 전역 스코프 오염이 무조건 일어나는 것은 아니다. 하지만 사람이라면 실수를 하기 마련이고, 이런 실수가 발생했을 때의 대비가 없다는 것이 문제다.
이 문제들 말고도 여러가지 문제들이 있으나, 대표적으로 이런 문제들로 인해 CommonJS에서 ES모듈로 전환이 이루어지고 있다.
마치며
그동안에 왜 두 가지로 존재하는걸까? 하고 생각만 하고 그 이유를 찾아보지 않았는데, 이런 이유들에 대해서 알아보는 것이 굉장히 중요한 것 같다. 무언가를 사용하는데 왜 그걸 사용하는지 모르고 사용하는 것 만큼 위험한게 없는거 같다.
의사결정에서도 그렇다. 아무 이유 없이 그냥 다들 쓰니까 라거나 이거 밖에 몰라서 라는 이유로 결정하는 것은 굉장히 좋지 못하다. 왜 그런 결정을 내렸고, 왜 이게 여기서 적합한 지를 설명할 수 있어야 하며 내가 내린 의사결정에 대해서 충분히 디펜스가 가능해야한다.
그런 차원에서 이런 사소한 것들에 대해서도 정확히 알아야한다고 생각한다.
'Develop > React' 카테고리의 다른 글
[React][개발기] CI/CD 도입 (0) | 2024.08.08 |
---|---|
[React][개발기] 10. 무한 슬라이드 개발 (0) | 2024.07.23 |
[React][개발기] 9. 리팩토링과 총 점검 (0) | 2024.07.19 |
[React][개발기] 8. Login 모달, 그리고 전체 점검의 필요성 (0) | 2024.07.18 |
[React][개발기] 7. RoutePaths, NavBarData 등 데이터를 효율적으로 (0) | 2024.07.17 |