[Node.js] 1. 기본개념, 모듈, npm

rin·2020년 12월 10일
5
post-thumbnail

https://poiemaweb.com/nodejs-basics

Basic

Introduction

Node.js는 Chrome V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임 환경으로 주로 서버 사이드 어플리케이션 개발에 사용되는 소프트웨어 플랫폼이다.

  • Node.js는 자바스크립트를 사용해 개발한다.
  • Non-blocking I/O와 단일 스레드 이벤트 루프를 통한 높은 Request 처리 성능을 가지고 있다.
    • 모든 API가 비동기 방식으로 동작한다.
    • 단일 스레드 이벤트 루프 모델을 사용함으로써 보다 가벼운 환경에서도 높은 요청 처리 성능을 보여준다.
  • 데이터를 실시간 처리하는 SPA에 적합하다.
    • 단, CPU 사용률이 높은 어플리케이션에는 권장하지 않는다.
  • Socket.io라는 실시간 통신 라이브러리를 사용하여 대량의 데이터 처리와 실시간 통신 모두 구현할 수 있다.

Example

  • Node.js는 http 서버 모듈을 내장하고 있어서 아파치와 같은 별도의 웹서버를 설치할 필요가 없다.
// app.js
const http = require('http'); // 1

http.createServer((request, response) => { // 2
  response.statusCode = 200;
  response.setHeader('Content-Type', 'text/plain');
  response.end('Hello World');
}).listen(3000); // 3

console.log('Server running at http://127.0.0.1:3000/');
  • (1) http 모듈을 로딩하여 변수 http에 할당하였다.
    • Node.js는 파일과 1대1 대응 관계를 가지는 module 단위로 각 기능을 분할한다.
    • 하나의 모듈은 자신만의 독립적인 스코프를 가지므로 전역 변수의 중복 문제가 발생하지 않는다.
    • 모듈은 module.exports 또는 exports 객체를 통해 정의하고 외부로 공개한다.
    • 공개된 모듈은 require 함수를 사용하여 임포트한다.
    • 위 예제에서 http는 기존 선언된 모듈이며 이를 require 함수로 import 한 것이다.
  • (2) http 모듈createServer([requestListener]) 메소드를 사용하여 HTTP 서버 객체를 생성한다.
    • HTTP 서버 객체는 EventEmitter 클래스를 상속한 것으로 request Listener 함수(request 이벤트가 발생하면 이를 처리하고 response를 반환)를 호출한다.
    • request Listener 함수는 request와 response 객체를 전달받으며 HTTP 요청 이벤트마다 한 번씩 호출된다.
  • (3) createServer 메소드는 HTTP 서버 객체를 반환하고, 이 객체의 listen 메소드에 포트 번호 3000을 전달하여 서버를 실행한다.

모듈화와 npm

모듈화와 CommonJS

모듈이란 어플리케이션을 구성하는 개별적 요소를 말한다.

  • 파일 단위로 분리
  • 필요에 따라 명시적으로 로드
    • 어플리케이션에 분리되어 개별적으로 존재하다가 어플리케이션의 로드에 의해 어플리케이션의 일원이된다.
  • 기능별로 분리되어 작성되므로 개발효율성과 유지보수성의 향상

자바스크립트를 Client-side에 국한하지 않고 범용적으로 사용하고자하며 모듈에 대한 필요성이 대두되었다. (자바스크립트 파일은 독립적인 Scope를 가지지않고 하나의 전역 객체에 바인딩됨)

CommonJSAMD는 사양(spec)으로 라이브러리가 아니다.

Spec문법동작 방식
CommonJS비교적 간단함동기 방식
모듈 시스템의 사실상 표준(de facto standard)
AMD다소 까다로움비동기 방식
대표적인 모듈 로더는 RequireJS

Node.js는 독자적인 진화를 거쳐 CommonJS 사양과 100% 동일하지는 않지만 기본적으로 CommonJS 방식을 따른다.

🤔 브라우저는 ES6 모듈을 지원하지 않으므로 Browserify 또는 webpack 과 같은 모듈 번들러를 사용해야한다.


JavaScript 표준을 위한 움직임: CommonJS와 AMD

https://d2.naver.com/helloworld/12864

✔️ CommonJS는 JS를 브라우저에서 뿐만 아니라, 서버사이드 어플리케이션이나 데스크톱 어플리케이션에서도 사용하려고 조직한 자발적 워킹 그룹이다.

  • 이 그룹은 JS를 범용적으로 사용하기 위한 명세(Specification)를 만드는 일을 한다.

탄생 배경

  • 1996년, 자바스크립트의 탄생
  • 브라우저 밖에서 사용하기 위한 프로젝트 대거 출현 (Helma, AppJet, Jaxer 등) 했으나 큰 성공을 거두지 못함
  • 2005년, Ajax의 부상
    • Javascript 연산 증가와 함께 이를 빠르게 처리할 수 있는 엔진이 요구됨
  • 2008년, Google의 V8 JavaScript 엔진 발표
  • 2009년, Kevin Dangoor 서버사이드 JS에 대한 아이디어 제시
  • CommonJS 그룹의 탄생

서버사이드 JavaScript의 주요 쟁점
Kevin은 자바스크립트에 다음과같은 문제를 제기하였다.

  • 서로 호환되는 표준 라이브러리의 부재
  • DB에 연결가능한 표준 인터페이스의 부재
  • 다른 모듈을 삽입하는 표준 방법이 없음
  • 코드를 패키징하여 배포하고 설치하는 방법이 필요
  • 의존성 문제를 해결하는 공통 패키지 모듈 저장소가 필요

핵심은 모듈화
앞에서 언급한 문제점들은 결국 모듈화로 귀결된다.
👉 CommonJS의 주요 명세는 이 모듈을 어떻게 정의하고, 어떻게 사용할 것인가에 대한 것이다.

모듈화는 다음과 같은 세 부분으로 이루어진다.

  • 스코프(Scope) : 모든 모듈은 자신만의 독립적인 실행 영역이 있어야 한다. 👉 지역변수와 전역변수 분리
  • 정의(Definition) : 모듈 정의는 exports 객체를 이용한다.
  • 사용(Usage) : 모듈 사용은 require 함수를 이용한다.

exports.${변수명}이나 require(${모듈명})을 이용하여 다른 모듈의 객체를 사용하는 것은 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 한다. (=서버사이드 JS 환경)

이는 필요한 모듈을 모두 내려받을 때까지 아무것도 할 수 없게 되는 것을 의미하며, 동적으로 <script>태그를 삽입하는 방법으로 극복하였다.

비동기 모듈 로드 문제
브라우저에서 JS를 사용할 때는 파일 단위 스코프가 없기 때문에 <script>를 이용해서 파일을 로드하면 동일한 이름의 변수는 덮어씌워지는 문제가 발생한다.

이를 해결하기 위해 서버모듈을 비동기적으로 클라이언트에 전송할 수 있는 모듈전송 포맷을 추가로 정의하였다.

/* 서버사이드에서 사용하는 모듈 */

// complex-numbers/plus-two.js

var sum = require("./math").sum;  
exports.plusTwo = function(a){  
   return sum(a, 2);  
};


/*************************************/
/* 브라우저(클라이언트사이드)에서 사용하는 모듈 */

// complex-numbers/plus-two.js

require.define({"complex-numbers/plus-two": function(require, exports){
   //콜백 함수 안에 모듈을 정의한다.
   var sum = require("./complex-number").sum;  
   exports.plusTwo = function(a){  
      return sum(a, 2);  
   };
},["complex-numbers/math"]);
//먼저 로드되어야 할 모듈을 기술한다.

전송 포맷으로 서버사이드의 모듈을 감싸면 비동기적으로 이를 로드할 수 있다.

✔️ AMD 그룹은 비동기 상황에서도 JavaScript 모듈을 쓰기 위해 CommonJS와 함께 논의하다 합의점을 이루지 못하고 독립한 그룹이다.

  • AMD(Asynchronous Module Definition)의 목표는 필요한 모듈을 네트워크로 내려받아야하는 브라우저 환경에서도 모듈을 사용할 수 있는 표준을 만드는 것이다.

Vs. CommonJS

  • 서버사이드-필요한 파일이 모두 로컬 디스크에 있어 바로 불러 쓸 수 있는 상황 👉 CommonJS가 더욱 간결
  • 클라이언트사이드-필요한 파일을 네트워크를 통해 내려받아야하는 환경 (like browser) 👉 AMD가 더 유연한 방법을 제공

모듈 명세

  • 비동기 모듈에 대한 표준안
  • CommonJS와 많은 부분이 유사하거나 호환 가능한 기능을 제공
    • require() 함수를 사용할 수 있으며
    • exports 형태로 모듈을 정의할 수 있다.
  • define() 함수를 이용해 파일 스코프를 대신한다. (일종의 네임스페이스 역할) 👉 모듈에서 사용하는 변수와 전역변수를 분리 가능
    • 물론, 이 함수는 전역함수로써 AMD 명세를 구현하는 서드파티 벤더가 모듈 로더에 구현해야 한다.

define() 함수
전역함수로써 다음과 같이 정의한다.
define(id?, dependencies?, factory);

  • id : 모듈을 식별하는데 사용하는 인수. 선택적으로 사용
    • id가 없는 경우, 로더가 요청하는 script 태그의 src 값으로 자동 치환된다.
    • id를 명시하는 경우, 파일의 절대 경로를 식별자로 지정해야 한다.
  • dependencies : 정의하려는 모듈의 의존성을 나타내는 배열. 반드시 먼저 로드돼야 하는 모듈을 나타낸다.
    • 로드된 모듈은 세번째 인수은 factory 함수의 인수로 넘겨진다.
    • 생략하는 경우, ['require', 'exports', 'module'] 이라는 이름이 기본으로 지정되는데, 이 세 모듈은 CommonJS에서 정의한 전역객체와 동일한 역할을 하게 된다.
  • factory : 함수. 모듈이나 객체를 인스턴스화하는 실제 구현을 담당한다.
    • 인수가 함수라면, 싱글톤으로 한 번만 실행되고 반환되는 값이 있다면, 그 값을 exports 객체의 속성값으로 할당한다.
    • 인수가 객체라면, exports 객체의 속성값으로 할당된다.

전역변수와 define.amd 프로퍼티
AMD 명세에서 정의하는 전역변수는 다음과 같다.

  • define
  • require
  • exports
  • define.amd 프로퍼티 : 전역 모듈을 명시적으로 가리킬 때 사용

하지만, 그 밖에 다른 전역변수나 메서드, 프로퍼티를 추가하면 안된다.

example

define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {  
   exports.verb = function() {

      // 넘겨받는 인수를 사용해도 되고
      return beta.verb();

      // 또는 require()를 이용해
      // 얻어 온 모듈을 사용해도 된다.
      return require("beta").verb();  
   }
});

alpha(id)라는 모듈을 정의하는데 beta(dependencies) 모듈이 필요함을 나타낸다.

다음과 같이 CommonJS 형태의 모듈을 래핑할 수도 있다.

define(function (require, exports, module) {  
   var a = require('a'),  
   b = require('b');

   exports.action = function () {};  
});

AMD의 장점

  • 비동기 환경에서도 매우 잘 동작할 뿐만 아니라, 서버사이드에서도 동일한 코드로 동작한다.
  • CommonJS의 모듈 전송 포맷에 비해 간단, 명확하다.
  • define() 함수를 이용하여 모듈을 구현함으로써 전역변수가 없다.
  • 해당 모듈을 필요한 시점에 로드하는 Lazy-Load 기법을 응용할 수도 있다.

npm

npm(node package manager)은 Node.js에서 사용할 수 있는 모듈들을 패키지화하여 모아둔 저장소 역할 겸 패키지 설치 및 관리를 위한 CLI를 제공한다.

패키지 설치

$ npm install <package>

# 버전 명시
$ npm install <package@version>

지역 설치와 전역 설치

# 지역 설치
$ npm install <package>

# 전역 설치
$ npm install -g <package>

지역 설치 시 프로젝트 루트 디렉터리에 node_modules 디렉터리가 자동 생성되고 그 안에 패키지가 설치된다. 이는 해당 프로젝트 내에서만 사용할 수 있다.

package.json과 의존성 관리
npm은 package.json 파일을 통해 프로젝트 정보와 패키지 의존성을 관리한다.

# package.json 생성 -> 프로젝트에 대한 정보를 입력하라고 요청한다.
$ npm init

# package.json 생성 -> 기본 설정값으로 생성된다.
$ npm init [-y | --yes]

package.json에서 가장 중요한 항목은 nameversion이다.

  • 이 값을 통해 패키지의 고유성을 판단한다.
  • 생략할 수 없다.
{
  "name": "emoji",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "node-emoji": "^1.10.0"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

dependencies 항목에는 해당 프로젝트가 의존하는 패키지(해당 프로젝트에서 참조하는 모듈)들의 이름과 버전을 명시한다.

유의적 버전

...
  "dependencies": {
    "node-emoji": "^1.5.0"
  },
...

버전의 명시적 선언과 함께 install을 하면 버전 앞에 ^이 추가된다. 이는 이후 해당 패키지 버전이 업데이트되었을 경우, 마이너 버전 범위 내에서 업데이트를 허용한다는 의미이다.

즉, 다시 한 번 npm install node-emoji 를 실행하면 최신 버전으로 자동 업데이트되는 것이다.

버전 정보 앞에는 기호를 부여하여 업데이트 범위를 지정할 수 있다.

표기법Description
version명시된 version과 일치
>version명시된 version보다 높은 버전
>=version명시된 version과 같거나 높은 버전
<version명시된 version보다 낮은 버전
<=version명시된 version과 같거나 낮은 버전
~version명시된 version과 근사한 버전
^version명시된 version과 호환되는 버전

~(틸트)와 ^(캐럿)의 차이는 아래와 같다

  • ~(틸트)는 패치 버전 범위 내에서 업데이트한다. :
    • ~0.0.1 : 0.0.1 <= version < 0.1.0
      ~0.1.1 : 0.1.1 <= version < 0.2.0
  • ^(캐럿)는 마이너 버전 범위 내에서 업데이트한다. :
    • ^1.0.2 : 1.0.2 <= version < 2.0

Node.js의 module loading system

Node.js 모듈

브라우저 상에서 동작하는 JS는 script tag로 로드하며 복수의 JS 파일을 로드할 경우 하나의 파일로 merge되며 동일한 유효범위를 갖게된다.

  • ES6에서는 클라이언트 사이드 JS 에서도 동작하는 모듈 기능을 추가하였다. 👉 키워드 export, import

Node.js는 모듈 단위로 각 기능을 분할한다.

  • 모듈과 파일은 1:1 대응 관계를 가지며
  • 하나의 모듈은 자신만의 독립적인 스코프를 가진다.
  • 전역변수 중복 문제가 발생하지 않는다.

모듈은 module.exports 또는 exports 객체를 통해 정의하고 외부로 공개한다. 공개된 모듈은 require 함수를 사용해 임포트한다.

exports

모듈 안에 선언한 모든 것들은 기본적으로 해당 모듈 내부에서만 참조 가능하다.

  • 이를 외부에 공개하여 다른 모듈에서 사용할 수 있도록 해주는 것이 export 객체이다.
  • 전역 함수 require()로 추출한다.
// circle.js
const { PI } = Math;
exports.area = (r) => PI * r * r;
exports.circumference = (r) => 2 * PI * r;

// app.js
const circle = require('./circle.js'); // == require('./circle')

console.log(`지름이 4인 원의 면적: ${circle.area(4)}`);
console.log(`지름이 4인 원의 둘레: ${circle.circumference(4)}`);

module.exports

export 객체는 프로퍼티/메소드를 여러개 정의 할 수 있는 것에 반해 module.exports에는 하나의 값(원시 타입, 함수, 객체)을 할당할 수 있다.

require()로 할당받은 변수는 module.exports에 할당한 값 자체이다.

exports는 module.exports의 참조이며 module.exports의 alias이다. 즉, exports는 module.exports와 같다고 보아도 무방하다.

구분모듈 정의 방식require 함수의 호출 결과
exportsexports 객체에는 값을 할당할 수 없고 공개할 대상을 exports 객체에 프로퍼티 또는 메소드로 추가한다.exports 객체에 추가한 프로퍼티와 메소드가 담긴 객체가 전달된다.
module.exportsmodule.exports 객체에 하나의 값(원시 타입, 함수, 객체)만을 할당한다.module.exports 객체에 할당한 값이 전달된다.

module.exports에 함수를 할당하는 방식

// foo.js
module.exports = function(a, b) {
  return a+b;
};

// app.js
const add = require('./foo');

const result = add(1, 2);
console.log(result);

module.exports는 1개의 값만을 할당할 수 있기에 다음과 같이 객체를 사용하여 복수의 기능으 ㄹ하나로 묶어 공개하는 방식을 사용할 수 있다.

exports에 객체를 할당하는 방식

// foo.js
module.exports = {
  add (v1, v2) { return v1 + v2 },
  minus (v1, v2) { return v1 - v2 }
};

// app.js
const calc = require('./foo');

const result1 = calc.add(1, 2);
console.log(result1); //3

const result2 = calc.minus(1, 2);
console.log(result2); //-1

require

require 함수의 인수에는 파일뿐만 아니라 디렉터리를 지정할 수도 있다.
모듈을 명시하지 않는 경우에는 해당 디렉터리의 index.js를 로드한다.

project/
├── app.js
└── module/
    ├── index.js
    ├── calc.js
    └── print.js
// app.js
const myModule = require('./module');

// module/index.js
module.exports = {
  calc: require('./calc'),
  print: require('./print')
};

app.js 에서의 한 번의 require로 module/ 디렉터리 하위의 모든 모듈들을 사용할 수 있다.

코어 모듈과 파일 모듈

  • 코어모듈 : Node.js가 기본으로 포함하는 모듈. 패스를 명시하지 않아도 무방하다.
    const http = require('http');
  • npm의 통해 설치한 외부 패키지 : 패스를 명시하지 않아도 무방하다.
    const mongoose = require('mongoose');
  • 파일 모듈 : 위 두 종류를 제외한 나머지. 반드시 패스를 명시하여야 한다.
    const foo = require('./lib/foo');
profile
🌱 😈💻 🌱

0개의 댓글