모듈과 import / require , Bundling 에 대한 이해

ChoiYongHyeun·2024년 3월 24일
0

프로그래밍 공부

목록 보기
7/17

이번에 깃허브 API 를 이용해서 무엇인가 해보려고 뚝딱이면서 라이브러리들을 불러와야했다.

그래서 별 생각없이 라이브러리들을 불러왔다.

import {Octokit} from 'octokit' // API 라이브러리 
require('dotenv').config() // .process.env 에 접근 가능하게하는 라이브러리
...

octokit 라이브러리는 공식문서에서 요구하는데로 불러왔고

dotenvnode.js 환경에서 사용할 때 쓰던데로 불러왔는데

누가봐도 이상하고 누가봐도 에러가 날 거 같이 생겼다.

그래서 저장하고 리액트 환경에서 실행해보면

webpack compiled with 3 errors

웹팩이 컴파일 과정에서 3가지 에러를 발견했다고 이야기한다.

다른 환경에선 어떨까 하고 nodeJS 환경에서 실행해봤다.

SyntaxError: Cannot use import statement outside a module

얘는 또 다른 에러문을 제공한다.


왜 이런 에러들이 발생하지 .. 모듈이라는게 정확히 모징 ...

이런 기초부실 멍청이같은 생각들이 있어서 호다닥 공부해봤다.

다행히도 잘 정리해놓은 블로그 글들과 유튜브 영상들이 있어 이해가 빠르게 되었다.


import / export 가 존재하기 전 브라우저 환경의 특징

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
  </head>
  <body>
    ...
  </body>
  <script src="index.js" type="text/javascript"></script>
</html>

초기 웹페이지 환경에서는 자바스크립트를 이용한 인터렉션이 그다지 복잡하지 않았기 때문에

사용하는 자바스크립트 코드 파일의 크기가 크지 않았다.

이에 개발자들은 하나의 자바스크립트 파일에서 모든 로직을 처리해도 유지 보수하는데 있어 큰 불편함을 겪지 않았다.

하지만 AJAX 를 활용한 부드러운 , 마치 어플리케이션과 같은 환경을 웹에서 제공 해주는 웹 어플리케이션(웹앱) 이 성행하자

자바스크립트의 중요도는 올라가게 되었고 이로 인해 자바스크립트 파일의 크기가 늘어나게 되었다.

하나의 파일에서 여러 코드를 관리하는 것은 코드를 유지 보수하는데 있어 어려움이 있기 때문에

파일들을 관심사별로 나눠 분리하여 관리하였다.

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
  </head>
  <body>
    ...
  </body>
  <script src="getRestaurantsName.js" type="text/javascript"></script> // 1
  <script src="getHotelsName.js" type="text/javascript"></script> // 2
  ...
  <script src="index.js" type="text/javascript"></script> // 엔트리파일 
</html>
// getRestaurantNames.js 의 코드  
function getRestaurantsName (location) {
	... 엄청나게 긴 코드 줄
    가고자 하는 location 에 존재하는 여러 가게의 이름과 정보를 불러오는 .. 
}

// getHotelNames.js 의 코드
function getHotelsName (location) {
	... 엄청나게 긴 코드 줄
    가고자 하는 location 에 존재하는 여러 호텔의 이름과 정보를 불러오는 .. 
}

// index.js 
var yourlocation = ... 사용자의 장소를 입력받은 값을 가져옴 

var restaurant = getRestaurancName(yourlocation)
var hotel = getHotelsName(yourlocation)
alert(`당신에게 꼭 맞는 식당과 호텔 정보는 ~ ${restaurant , hotel} .. `)

이런식으로 같은 관심사를 가진 스크립트 파일별로 분리하고 index.js 와 같은 메인 파일이 파싱 되기전 필요한 함수나 변수들을 먼저 선언하여

메인 파일에서 필요한 함수 혹은 변수들을 사용 할 수 있게 하였다.

이렇게 파일을 관심사 별로 분리함에 따라 코드의 가독성을 높혀주고 유지보수를 쉽게 하였다.

전역 변수는 모두 window 프로퍼티와 메소드가 됩니다

각기 다른 파일에서 var 선언문으로 생성된 전역변수가 다른 파일에서 접근 가능한 이유는

var 선언문이나 function () {} 으로 선언된 함수 선언문은 모두

window 객체의 프로퍼티나 메소드가 되었기 때문이다.

// getHotelNames.js 의 코드
function getHotelsName (location) {...}
는 사실 
window.getHotelsName = function(location){...}
// index.js
var restaurant = getRestaurancName(yourlocation) 
는 사실 
var restaurant = window.getRestaurancName(yourlocation)

그럼 서로 다른 파일에서 전역 변수들을 선언하고

실행 할 때에는 같은 공간의 전역 변수로 취급 하기 때문에 간편하다는 장점이 있지만

치명적인 단점도 존재했다.

전역 변수 선언은 window 객체의 프로퍼티로 선언해주는 것이기 때문에

동일한 프로퍼티가 중복적으로 선언되면 마지막으로 선언된 프로퍼티로 설정되는 over riding 문제가 존재했다.

이에 여러 파일에서 선언된 전역 변수 간 이름이 중복되는 순간 메인 파일에서는 예기치 못한 오류들이 발생하곤했다.


전역 변수를 관리하기 위한 여러가지 시도들

개발자들은 전역 변수를 효과적으로 관리하기 위한 여러가지 시도들을 했었다.

전역변수 관리 문제의 원인을 먼저 살펴보면

전역 변수들이 모두 window 라는 전역 객체의 프로퍼티로 설정되는 것이 문제였다.

네임 스페이스 패턴

이에 개발자들은 각 파일 별 선언되어야 하는 전역 객체들을

파일에 존재하는 최상단 객체의 프로퍼티와 메소드들로 넣어버렸다.

// getHotelsName.js 의 코드
var hotelObj = {} //전역 객체들을 관리할 네임 스페이스 

hotelObj.methods = {} // 메소들을 관리할 네임 스페이스 
hotelObj.properties = {} // 프로퍼티들을 관리할 네임 스페이스 

hotelObj.methods.setLocation = function(location){}
hotelObj.methods.getHotelsName = function (location){}
hotelObj.properties.version = '1.1.2' ... 

// getRestaurantsName.js 의 코드 
var restauranctsObj = {} //전역 객체들을 관리할 네임 스페이스 

restauranctsObj.methods = {} // 메소들을 관리할 네임 스페이스 
restauranctsObj.properties = {} // 프로퍼티들을 관리할 네임 스페이스 

restauranctsObj.methods.setLocation = function(location){}
restauranctsObj.methods.getHotelsName = function (location){}
restauranctsObj.properties.version = '1.1.2' ... 

여러 전역 객체들에 접근 할 수 있는 식별자들을 모아둔 공간을 name space 라고 한다.

각 파일 별 선언될 객체와 함수들을 파일 별 네임 스페이스에 담아줌으로서

window.전역변수 로 선언되는 것이 아닌 window.파일별네임스페이스.전역변수 로 선언되게 하였다.

이를 통해 전역 변수간 충돌하는 오버라이딩 문제를 해결하였다.

모듈 패턴

모듈 패턴은 클로저와 즉시 실행 함수기법을 활용한 패턴으로

변수를 전역 공간에서 선언하는 것이 아닌 함수의 지역 공간에서 선언하는 것이다.

// getHotelsName.js 의 코드
var hotelObj = (function () {
  function setLocation(location) {}
  function getHotelName(location) {}
  var version = '1.1.2';

  return {
    method: {
      setLocation: setLocation,
      getHotelName: getHotelName,
    },
    properties: {
      version: version,
    },
  };
})();


console.log(version) // Ref errror 
console.log(hotelObj.properties.version) // 1.1.2

이를 통해 지역 공간 내에서 선언된 변수들에 접근하기 위해선

hotelObj 객체에 접근해야만 얻을 수 있게 하였다.


NodeJS 의 등장

구글에서 V8 엔진을 구글 크롬과 함께 발표하면서 nodeJS 의 개발자인 라이언 달은

브라우저 환경이 아닌 로컬 환경에서도 자바스크립트를 실행 시킬 수 있으면 어떨까 하는 생각으로

nodeJS 를 개발한다.

브라우저에서만 구동되던 자바스크립트를 로컬에서도 구동되도록 하여

이제는 서버를 자바스크립트로도 구현하는 것이 가능하게 된 것이다.

export / require 등장

nodeJS 에서 복잡한 서버 로직을 관리하기 위해 다양한 파일에 따라 나눠 코드를 작성했는데

로컬에서는 window 객체가 존재하지 않기 때문에 전역 변수들을 여러 파일에서 공유 할 수 있는 로직이 필요했다.

이에 nodeJS 에서는 각 로컬 파일의 전역 객체들은 서로 다른 컨텍스트에서 존재하지만

Common JSexport / require 를 활용해 서로 다른 로컬 파일 내 객체들을 내보내고 , 불러올 수 있게 하여

로컬 파일 간 기능의 공유를 가능하게 했다.

// hotel.js (기능을 내보내는 모듈)
let hotels = ["Hotel California", "The Grand Budapest Hotel", "The Overlook Hotel"];

// Export a function to get all hotels
exports.getAllHotels = function() {
    return hotels;
};

// Export another function, adding a hotel
exports.addHotel = function(hotel) {
    hotels.push(hotel);
};
// app.js (모듈을 가져오는 기본 애플리케이션 파일)
// Import the 'hotel' module
const hotelModule = require('./hotel');

// Use the imported module
console.log(hotelModule.getAllHotels()); // Output the list of hotels
hotelModule.addHotel("The Great Northern Hotel");
console.log(hotelModule.getAllHotels()); // Output the updated list

이 때 export / require 로 불러 올 수 있는 기능들은 모듈에 해당한다.

모듈이란 다른 모듈들과 종속성이 적으며 최소한의 기능을 캡슐화 한 독립적인 단위를 의미한다.

종속성이란

종속성이란 어떤 모듈을 사용하기 위해 필요한 모듈간의 관계를 의미한다.
예를 들어 multiply 라는 모듈이 sum 이라는 모듈을 이용해 구현되었을 때 multiplysum 모듈에 종속성을 갖고 있다고 이야기 한다.

캡슐화 된 모듈을 공유하는 행위를 통해 코드의 캡슐화 , 독립성을 높혀 각자의 전역 객체들이 충돌하는 문제를 없앴을 뿐 아니라

재사용성 , 상호 운용성을 높혀 코드의 가독성과 단순한 구성요소의 조합으로 복잡한 아키텍쳐를 구현 할 수 있게 하였다.


ESMoudle 등장 (import / export의 등장)

브라우저 단에서도 모듈을 이용한 개발을 하기 위해 여러 라이브러리들이 사용됐었다.

그러던 도중 ECMAScript 에서 자바스크립트의 정식 모듈 체계인 ESM (ECMAScript Modules) 을 발표한다.

ECMAScript 에서는 import / export 를 이용한 모듈 공유를 가능하게 하였다.

다만 html 에서 모듈 파일을 불러오기 위해선

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    ...
  </body>
  <script src="index.js" type="module"></script>
</html>

scripttypemodule 로 설정해줘야 한다.

만일 모듈로 설정해주지 않으면 이전에 봤던

index.js:1 Uncaught SyntaxError: Cannot use import statement outside a module (at index.js:1:1)

와 같이 모듈 외부에선 import 문을 사용할 수 없다는 에러가 발생한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p></p>
  </body>
  <script src="index.js" type="module"></script> // 엔트리 파일 
</html>

다음처럼 엔트리 파일을 설정해주고 엔트리파일을 다른 모듈과 연결해줘 사용하는 것이 가능하다.

// korea-data.js
var greeting = '안녕하세요';

export default greeting;
// index.js
import greeting from './korea-data.js'; // greeting 이란 모듈을 import  

const $p = document.querySelector('p');

$p.textContent = greeting;

다만 이는 nodeJS 와 차이점이 존재하는데

모듈 파일은 브라우저 제공자 (호스트) 측 로컬 파일에 존재하기 때문에

브라우저에서 import 문을 만나면 해당 모듈을 불러오기 위해

호스트측으로 HTTP request 를 보내야 했다는 것이다.

만약 엔트리 파일이 100가지의 모듈들의 조합으로 구성되어 있다면 서버와 100번의 HTTP 통신을 거쳤어야 하는 단점이 존재했다.


번들러 등장


웹팩 : https://webpack.kr/

이에 엔트리 파일을 실행하기 위해 N 개의 모듈들을 모두 통신하여 가져와야 했던 기존의 방식에서

배포하기 전 엔트리파일과 종속성을 맺고 있는 N 개의 모듈들을 하나의 파일로 통합해주는 ,

즉 하나로 번들링 (Bundling) 해주는 라이브러리가 등장했다.

이렇게 번들링 해주는 라이브러리들을 번들러라고 하며

여러 번들러들이 존재한다.

우리가 일반적으로 npm bulid 를 하면 bulid.js 와 같이 배포용 엔트리 파일 하나가 생성되는데 이 것은

리액트가 번들러로 웹팩을 이용하기 때문이다.

// 웹팩 라이브러리의 사용 예시 
const path = require('path');

module.exports = {
  entry: './src/index.js', // 이 엔트리 파일을
  output: {
    path: path.resolve(__dirname, 'dist'), // 이 경로로 번들링 하고 
    filename: 'bundle.js', // 이름은 이렇게 하세요
  },
};

웹앱을 배포하기 전 번들링을 통해 엔트리파일을 종속성을 따라 하나의 파일로 변경하고

클라이언트는 한 번의 HTTP 통신만으로 웹앱을 이용 할 수 있게 되었다.

번들러에 대해 보다보니 다양한 번들러가 존재한다는 것을 알았다.
리액트에서는 웹팩 번들러를 이용하는데 번들러 별 성능의 장단점이 존재한다더라
나중에 배포까지 공부하려면 번들러에 대한 내용도 공부해야겠다.


정리

모듈이란 최소한의 기능을 하며 다른 모듈과 종속성이 적은 독립적인 캡슐화된 코드 구조이다.

모던 자바스크립트에서는 파일들을 여러 모듈의 조합으로 구성하여 코드의 캡슐화 독립성, 재사용성 , 상호운용성을 높히도록 한다.

이 때 여러 모듈과 종속성을 맺고 있는 엔트리 파일을 번들러를 통해 하나의 엔트리 파일로 변경한 후 배포하여 불필요한 통신이 일어나지 않도록 한다.


짝짝짝

import {Octokit} from 'octokit' // API 라이브러리 
require('dotenv').config() // .process.env 에 접근 가능하게하는 라이브러리
...

바보같은 이 코드가 왜 동작 안했는지를 살펴보면 importESModule 의 용법이고 requireCommon JS 의 문법이니 브라우저에서도 안되고 nodeJS 에서도 안됐던 것이다.

추후 알아보니 dotenv 자체는 애초에 브라우저에서 사용이 안된다. 키킥 ..

nodeJS 에서는 실험적으로 import 문법을 지원하기도 한다. 파일의 타입을 모듈이나 확장자명을 mjs 로 설정하는 경우엔 ESMimport /export 문법이 사용 가능하다.

그래도 바보같은 오류에서 번들러까지 알게되었으니 나름 잘 되었다고 생각한다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글