JavaScript Module System(1) - IIFE부터 CJS AMD까지

date
Dec 2, 2023
slug
js-module-1
author
status
Public
tags
Module
summary
type
Post
thumbnail
christian-boragine-AHuzfLnlnos-unsplash.jpg
category
updatedAt
Nov 11, 2024 01:51 PM

Module

모듈이란? - 애플리케이션을 구성하는 개별적 요소이자, 재사용 가능한 코드 조각을 말합니다.
모듈은
  • 기능을 기준으로 파일 단위로 분리하며
  • 자신만의 파일 스코프(모듈 스코프)를 가질 수 있어야 합니다.
 

IIFE를 사용해 모듈 패턴을 만들던 시절

IIFE 즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression) 
→ 정의되자마자 즉시 실행되는 함수를 말합니다.
*IIFE로 모듈 패턴 만들기 - 출처 IIFE라는 이름을 지은 Bem Almen의 블로그(2010년)
var counter = (function(){ var i = 0; return { get: function(){ return i; }, set: function( val ){ i = val; }, increment: function() { return ++i; } }; }()); // `counter` is an object with properties, which in this case happen to be // methods. counter.get(); // 0 counter.set( 3 ); counter.increment(); // 4 counter.increment(); // 5
 
IIFE로 모듈을 만들어 사용함으로 얻었던 이점
  1. 전역 스코프 오염 방지
    1. JavaScript는 기본적으로 전역 스코프를 사용합니다. IIFE로 모듈을 만들어 사용하던 때에는 ES2015(ES6)의 let, const가 나오기 이전이었습니다. var 변수 선언자만 있는 상황에서 전역 변수를 오염시키지 않기 위해서는 변수를 함수 스코프에 캡슐화하는 작업이 필요했습니다.
  1. 캡슐화
    1. IIFE를 사용하면 모듈 내에서만 접근 가능한 변수를 만들 수 있습니다. → 외부에서 직접 접근할 수 없는 private 변수 만들기 위해 사용했습니다.
  1. 모듈화
    1. 독립적이고 재사용 가능한 코드 블록을 만들 필요가 있었습니다.
 
IIFE로 모듈을 만들어 썼을 때의 아쉬운점
  • 코드 가독성 - 많은 IIFE를 사용할 경우 코드가 난해해보입니다.
  • 전역 네임스페이스 문제 - 네임스페이스가 충돌하는 문제가 있었습니다.
  • 모듈간 의존성 관리가 어려움 - 의존성 주입을 수동으로 처리해야 했습니다.
  • 재사용성 제한 - 제한적인 재사용성을 갖고 있었습니다.
 

 
IIFE를 통한 모듈 패턴이 사용되던 시기에 JavaScript는 주로 브라우저 환경에서 사용되었는데요.
서버 측 JavaScript 환경이 등장하면서(Node.js - 2009년 5월 27 처음 등장) 더 나은 모듈 시스템의 중요성이 더욱 커졌습니다. 이 때 등장한 것이 CommonJS 모듈 시스템입니다.

CommonJS(CJS)

JavaScript를 서버 측에서 사용할 수 있는 표준을 만들기 위해 모인 그룹 ServerJS에 의해 시작되었습니다.
CommonJS는 동기적으로 모듈을 로드합니다. 서버 측 환경에서는 동기식 로딩이 자연스럽기 때문입니다.
 
왜 그럴까요?
  1. 초기화의 간단함
    1. 서버 애플리케이션은 일반적으로 시작 시점에 필요한 모든 모듈을 로드합니다. 애플리케이션의 초기화 단계에서 간단하게 필요한 모듈들을 로드하고 사용할 수있도록 하면 이후에 바로 요청을 처리할 수 있게됩니다. 모듈이 로드된 시점 이후에만 해당 모듈을 사용할 수 있기에 초기화 단계에서 발생할 수 있는 의존성 문제를 예방할 수 있습니다.
  1. 명확한 실행 흐름
    1. 동기식 로딩은 코드의 실행 흐름을 명확하게 합니다. 모듈이 로드될 때까지 다음 코드가 실행되지 않기에 언제 로드되고 사용 가능한지 확실하게 알 수 있습니다.
  1. I/O 속도의 차이
    1. 서버 측 환경에서는 파일 시스템 접근이 상대적으로 빠르기에 동기식 파일 시스템 접근이 큰 문제가 되지 않습니다. 서버 애플리케이션의 성능은 네트워크 요청 처리와 데이터베이스 접근 속도에 더 크게 영향을 받습니다.파일 시스템에서 모듈을 로드하는 작업은 네트워크 요청에 비해 훨씬 빠릅니다. 또한 클라이언트와 달리, 페이지 로드 시간을 줄이기 위해 비동기 로딩을 최적화할 필요가 없습니다.
  1. 복잡한 의존성 관리
    1. 서버 애플리케이션은 종종 많은 모듈과 복잡한 의존성을 가집니다. 동기식 로딩은 의존성 그래프를 쉽게 이해하고 관리하게 해줍니다. 필요한 모듈을 즉시 로드하고 사용할 수 있기 때문에 의존성 관리가 간단해집니다. 모듈간 의존성을 쉽게 추적할 수 있기에 디버깅과 유지보수가 용이합니다.
 
문법
require 함수로 모듈을 로드하고, module.exports 또는 exports객체로 모듈을 내보냅니다.
//모듈 정의하기 const message = "Hello, CommonJS"; function greet() { console.log(message); } module.exports = { greet };
//모듈 가져오기 const example = require('./example'); example.greet();
 
특징
파일 단위 모듈 - 각 파일이 하나의 모듈로 간주되며, 모듈 간의 의존성은 파일 시스템 경로로 관리됩니다.
캐싱 - 모듈은 처음 로드될 때 한 번만 실행되고, 이후에는 캐시된 버전이 사용됩니다.
 
장단점
장점 - 단순한 문법, 동기식 로딩, 풍부한 생태계(npm)
단점 - 클라이언트 측 환경에 부적합(동기식 로딩이어서)
 
*CommonJS 모듈 시스템에서 Factory 패턴
CommonJS 모듈 시스템에서 Factory 패턴은 module.exports를 통해 모듈을 정의하고, require 함수로 모듈을 로드할 때 사용됩니다. CommonJS에서는 각 파일이 자체적으로 하나의 모듈로 간주되며, module.exports는 모듈을 내보내는 Factory 역할을 합니다.
var add = function(x, y) { return x + y; }; var subtract = function(x, y) { return x - y; }; module.exports = { add: add, subtract: subtract };
var math = require('./math'); console.log(math.add(2, 3)); // 출력: 5 console.log(math.subtract(5, 2)); // 출력: 3
브라우저 환경을 지원하는 비동기식 로딩 모듈시스템에 대한 필요성 대두되었습니다. ⇒ AMD의 등장

AMD(Asynchronous Module Definition)

비동기식 로딩을 지원하는 브라우저 환경에 적합한 모듈 시스템
 
브라우저에서는 왜 비동기식 로딩이 적합한가요?
  1. 페이지 로드 성능 향상
    1. 웹 페이지 로드 성능을 최적화하기 위해서입니다. 브라우저에서 컨텐츠들을 로드하여 웹 서비스를 그릴 때, 여러 요소들을 로드하는 동안 다른 작업을 할 수 있도록 하면 HTML 문서의 파싱과 렌더링이 중단되지 않습니다. 여러 script와 stylesheet들을 동시에 로드할 수 있게 해줍니다.
  1. 사용자 경험 개선과 리소스 최적화
    1. 초기 렌더링에 필요한 최소한의 리소스만 먼저 로드하고 나머지는 비동기적으로 로드할 수 있어 사용자가 핵심 콘텐츠를 빠르게 볼 수 있게 합니다. → 초기 렌더링 속도 개선
  1. 네트워크 효율성
    1. 필요한 리소스만 로드할 수 있어 불필요한 네트워크 트래픽을 줄이고, 네트워크 자원을 효율적으로 사용할 수 있게 합니다.
 
문법
RequireJS라는 AMD 규격을 따르는 모듈 로더 라이브러리를 사용합니다.
*RequireJS는 파일 경로 매핑, 템플릿 로딩, 비동기적 의존성 로딩 등을 지원합니다.
define 함수로 모듈을 정의하고, require 함수로 모듈을 로드하여 사용합니다.
// math.js define([], function() { var add = function(x, y) { return x + y; }; var subtract = function(x, y) { return x - y; }; return { add: add, subtract: subtract }; });
// main.js require(['math'], function(math) { console.log(math.add(2, 3)); console.log(math.subtract(5, 2)); });
 
장점
  1. 비동기식 로딩으로 인한 성능 향상
  1. 명시적인 의존성 선언과 관리
  1. 모듈화된 코드 구조
 
단점
문법이 상대적으로 복잡하고 많은 함수 호출과 콜백이 필요해 코드가 장황해집니다.
! CommonJS 모듈과 직접 호환되지 않습니다. (호환성 문제)
⇒ 호환성 문제 해결을 위해 UMD 등장
 

AMD와 factory pattern 톺아보기

AMD 모듈 로더 라이브러리인 RequireJS의 define 함수
define 함수는 다음과 같은 구조를 가집니다.
define(id, dependencies, factory);
  • id (optional): 모듈의 이름을 지정합니다. 생략하면 파일 이름이 모듈 이름으로 사용됩니다.
  • dependencies (opitional): 모듈이 의존하는 모듈들의 배열입니다. 생략하면 기본적으로 require, exports, module이 사용됩니다.
  • factory: 모듈의 실제 구현입니다. 함수나 객체를 반환합니다.
 
*AMD의 facotry패턴
객체 생성의 책임을 팩토리 클래스나 메서드에 위임하는 디자인 패턴입니다. 객체 생성의 복잡성을 숨기고, 객체 생성 로직을 중앙 집중화합니다.
 
define함수의 factory parameter
모듈을 정의하고 반환하는 역할을 합니다. 함수는 주어진 의존성들을 받아 모듈의 인터페이스를 정의하고 이를 호출하는 코드에 반환합니다.
// math.js 모듈 정의 define('math', [], function() { var add = function(x, y) { return x + y; }; var subtract = function(x, y) { return x - y; }; return { add: add, subtract: subtract }; });
factory파라미터로 전달된 함수는 의존성 배열을 통해 모듈의 의존성을 주입받고, 모듈을 정의하여 반환합니다.
이 함수는 팩토리 함수로서의 역할을 합니다. (의존성을 주입받아 모듈을 생성하고 이를 호출하는 코드에 반환합니다.) → 모듈 간의 의존성을 명확히 하고, 모듈의 재사용성과 유지보수성을 높이는데 기여합니다.
 
AMD의 호환성 문제로 인해 UMD가 등장하는데… (이어서)