Node.js는 Chrome V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임 환경(실행 환경)으로, 주로 서버 사이드 어플리케이션 개발에 사용되는 소프트웨어 플랫폼이다.
본래 자바스크립트는 웹 브라우저 내부에서만 동작하는 스크립트 언어이다.
하지만 웹 브라우저 없이도 자바스립트를 실행시키고자 하는 요구가 많아졌고, 이에 node.js
는 chrome의 v8 자바스크립트 엔진을 활용해 자바스크립트를 웹 브라우저로부터 독립시켰다.
👉 결국 node.js
를 통해 자바스크립트는 브라우저 외 다른 환경에서도 작동하고, 더불어 자바스크립트로 서버 구현도 가능해졌다.
👉 그 덕에 웹 어플리케이션은 더욱 발전할 수 있게 되어 실시간 온라인 채팅이나 유저 인증 관리 등 실시간 통신이나 대량의 데이터 처리와 같은 다양한 기능 개발이 가능해졌다.
Node.js
는 V8과 더불어 libuv 라이브러리를 사용한다. libuv 라이브러리는 Node의 특성인 이벤트 기반, 논블로킹 I/O 모델을 구현하고 있다.
Node.js 표준 라이브러리의 모든 I/O 메서드는 Non-blocking인 비동기 방식을 제공하고, 콜백 함수를 받는다.
Non-blocking 비동기 작업을 통해 모든 API를 비동기 방식으로 동작할 수 있다.
자바스크립트의 실행 환경(runtime)인 Node는 싱글 스레드 기반이다.
해당 과정을 한눈에 표현한다면 다음 그림과 같이 표현할 수 있다.
Node.js는 HTTP 서버 모듈을 내장하고 있어 아파치같은 별도의 웹서버를 설치하지 않아도 된다.
// app.js
const http = require('http'); // ❶
http.createServer((request, response) => { // ❷
response.stateCode = 200;
response.setHeader('Content-type', 'text/plain');
response.end('Hello World');
}).listen(3000); // ❸
console.log('Server running at http://127.0.0.1:3000/');
http 모듈
을 로딩해 변수 http에 할당module.exports
또는 exports
객체를 통해 정의하고 외부로 공개한다.require
함수를 사용해 import한다.http
는 기존 선언된 모듈로, 이를 require
함수를 통해 import한 것이다.createServer([requestListener])
메서드를 사용해 HTTP 서버 객체 생성createServer
메소드는 HTTP 서버 객체를 반환하고, 이 객체를 listen
메소드에 포트 번호 3000
을 전달하여 서버를 실행한다.const
나 function
같은 키워드 및 process 등의 전역 객체파일 시스템 모듈("fs")
, 경로 모듈("path")
, Http 모듈("http")
등const http = require('http');
npm
을 통해 설치한 외부 패키지도 Path를 명시하지 않아도 무방하다.const mongoose = require('mongoose');
const foo = require('./lib/foo');
// commonjs
const fs = require('fs'); // 파일 시스템 작업
// esm (after es6)
import fs from 'fs';
// commonjs
const path = require('path'); // 파일 시스템 경로 유틸
// esm (after es6)
import path from 'path';
// commonjs
const http = require('http'); // http 서버 및 관련 유틸
const https = require('https'); // https 서버 및 관련 유틸
// esm (after es6)
import http from 'http';
import https from 'https';
require('assert') // 테스트 목적
require('child_process') // 외부 프로그램을 실행할 때 필요
require('cluster') // 다중 프로세스를 이용해 성능을 올릴 수 있게 한다.
require('crypto') // 내장된 암호화 라이브러리
require('dns') // 네트워크 이름 해석에 쓰이는 DNS 함수
require('domain') // 에러를 고립시키기 위해 IO 비동기 작업을 묶는다.
require('events') // 비동기 이벤트 지원
require('fs') // 파일 시스템 작업
require('http') // http 서버 및 관련 유틸
require('https') // https 서버 및 관련 유틸
require('net') // 비동기 소켓 기반 네트워크 API
require('os') // 운영체제 유틸리티
require('path') // 파일 시스템 경로 유틸
require('punycode') // 유니코드 인코딩
require('querystring') // URL 쿼리스트링 해석 및 생성
require('readline') // 대화형 IO 유틸. CLI 프로그램에 사용
require('smalloc') // 버퍼에 메모리를 명시적으로 할당
require('string_decoder') // 버퍼를 문자열로 변환
require('tls') // 보안 전송 계층 통신 유틸
require('tty') // 저수준 TTY 함수
require('dgram') // 사용자 데이터그램 프로토콜(UDP) 네트워크 유틸
require('url') // URL 파싱 유틸
require('util') // 내부 노드 유틸
require('vm') // 가상머신. 컨텍스트 생성에 사용.
require('zlib') // 압축 유틸
const http = require('http'); // 코어 모듈
const routes = require('./routes'); //파일 모듈
const server = http.createServer(routes.handler);
server.listen(3000);
const fs = require('fs'); // 코어 모듈
const requestHandler = (req, res) => {
const url = req.url;
const method = req.method;
if(url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title></head>');
res.write(
'<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>'
);
res.write('</html>');
return res.end(); // 해당 요청에 대한 응답 종료
}
if(url === '/message' && mehtod === 'POST') {
const body = [];
req.on('data', (chunk) => {
body.push(chunk);
});
return req.on('end', () => {
const parseBody = Buffer.concat(body).toString();
const message = parseBody.split("=")[1];
fs.writeFile('message.txt', (message, err) => {
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end(); // 해당 요청에 대한 응답 종료
});
});
}
res.setHaader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title></head>');
res.write('<body><h1>Hello from my Node.js Server!</h1></body>');
res.write('</html>');
res.end();
};
// module.exports = requestHandler;
// module.exports = {
// handler: requestHandler,
// someText: 'Some hard coded text'
// };
// module.exports.handler = requestHandler;
// module.exports.someText = 'Some text';
exports.handler = requestHandler;
exports.someText = 'Some hard coded text';
const http = require('http');
const server = http.createServer((req, res) => {
const url = req.url;
if (url === '/') {
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>Assignment 1</title></head>');
res.write(
'<body><form action="/create-user" method="POST"><input type="text" name="username"><button type="submit">Send</button></form></body>'
);
res.write('</html>');
return res.end();
}
if (url === '/users') {
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>Assignment 1</title></head>');
res.write('<body><ul><li>User 1</li><li>User 2</li></ul></body>');
res.write('</html>');
return res.end();
}
// Send a HTML response with some "Page not found text
if (url === '/create-user') {
const body = [];
req.on('data', chunk => {
body.push(chunk);
});
req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
console.log(parsedBody.split('=')[1]); // username=whatever-the-user-entered
});
res.statusCode = 302;
res.setHeader('Location', '/');
res.end();
}
});
server.listen(3000);
📌 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/
디렉토리 하위의 모든 모듈들을 사용할 수 있다.
path 모듈
를 통해 상위 디렉토리에 접근하고자 할 때, 파일 구조가 복잡할수록 경로 작성이 복잡해진다.
이때 Helper 함수를 통해 보다 쉽게 파일 모듈 경로에 도달할 수 있다.
project/
├── app.js
├── routes/
├ ├── admin.js
├ └── shop.js
└── util/
└── path.js
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const adminRoutes = require('./routes/admin');
const shopRoutes = require('./routes/shop');
app.use(bodyParser.urlencoded({extended: false}));
app.use('/admin', adminRoutes);
app.use(shopRoutes);
app.use((req, res, next) => {
res.status(404).sendFile(path.join(__dirname, 'views', '404.html'));
});
app.listen(3000);
const path = require('path');
const express = require('express');
const rootDir = require('../util/path');
const router = express.Router();
// /admin/add-product => GET
router.get('/add-product', (req, res, next) => {
res.sendFile(path.join(rootDir, 'views', 'add-product.html'));
});
// /admin/add-product => POST
router.post('/add-product', (req, res, next) => {
console.log(req.body);
res.redirect('/');
});
module.exports = router;
const path = require('path');
const express = require('express');
const rootDir = require('../util/path');
const router = express.Router();
router.get('/', (req, res, next) => {
res.sendFile(path.join(rootDir, 'views', 'shop.html'));
});
module.exports = router;
const path = require('path');
module.exports = path.dirname(process.mainModule.filename);
🗣️ Module :
모듈
이란 어플리케이션을 구성하는 개별적 요소를 말한다.
브라우저 상에서 동작하는 JS는 script tag
로 로드하며 복수의 JS파일을 로드할 경우, 하나의 파일로 Merge되고 해당 파일들은 동일한 유효 범위를 갖게 된다.
Node.js는 모듈 단위로 각 기능을 분할한다.
- 모듈과 파일은 1:1 대응 관계를 가지며
- 하나의 모듈은 자신만의 독립적인 스코프를 가진다.
- 전역변수 중복 문제가 발생하지 않는다.
모듈은 module.exports또는 expots 객체를 통해 정의하고 외부로 공개된다. 공개된 모듈은 require 함수를 통해 import된다.
모듈 안에 선언한 모든 것들은 기본적으로 해당 모듈 내부에서만 참조 가능하다.
// 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)}`);
require()
로 할당받은 변수는 module.exports
에 할당한 값 자체이다.
exports
는module.exports
의 참조이며module.exports
의 alias이다.
👉 즉exports
==module.exports
와 같다 보아도 무방하다.
구분 | 모듈 정의 방식 | require 함수의 호출 결과 |
---|---|---|
exports | exports 객체에는 값을 할당할 수 없고, 공개할 대상을 exports 객체에 프로퍼티 또는 메소드로 추가한다. | exports 객체에 추가한 프로퍼티와 메소드가 담긴 객체가 전달된다. |
module.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);
// 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
I/O
란 Input/Output
으로 데이터를 입력하고 출력하는 것을 의미한다.
컴퓨터는 우리가 입력(input)
한 데이터를 적절히 처리하여 화면에 출력(Output)
해 보여준다.
그렇다면 이때 데이터는 어떤 방법으로 전달되고 있을까?
Stream(스트림)은 배열이나 문자열 같은 데이터를 운반하는데 사용되는 연결 통로로, 데이터가 들어온 순서대로 흐르는 단반향의 통로이다.
쉽게 말해 자료를 입출력하기 위해 사용하며, 프로그램과 입출력 장치 사이에서 자료들을 중계하는 역할을 담당한다.
Stream은 동기적(blocking)으로 동작한다.
Java에서는 모든 기본 I/O는 Stream을 기반으로 하고 있어 빈번하게 사용되는데, 사용 후 Stream을 닫아주지 않으면 심각한 메모리 누수가 발생할 수 있으므로 예외처리에 주의를 기울여 사용해야 한다.
InputStream
, 출구는 OutStream
이라고 한다. byte
또는 byte[]
형태로 이동된다.Buffer는 데이터를 임시로 담아 둘 수 있는 일종의 큐 형태의 공간이다.
용량이 큰 데이터의 경우 한번에 전송하는 데에 어려움이 있기 때문에, Buffer
라는 특정 단위만큼 데이터를 담아 전송한다.
byte
단위의 데이터가 입력될 때마다 Stream은 즉시 전송하게 되는데 → 이로 인해 디스크 접근/네트워크 접근 같은 오버헤드가 발생하기 때문에 이 방법은 매우 비 효율적이다.Buffer
을 통해 중간에서 입력을 모아 한번에 출력함으로써 I/O의 성능을 향상시키는 역할을 한다.버퍼
로 전송하고, 버퍼
가 가득차면 버퍼
의 내용이 스트림
이 된다.// ❶ Buffer 사용하지 않고 출력
public static void nonBufferIO {
for (int i = 0; i < 100000; i++) {
System.out.print(i); // 입력이 있을 때마다 출력
}
System.out.println();
}
// ❷ Buffer를 사용해 출력
public static void bufferIO {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100000; i++) {
sb.append(i); // 1) 버퍼에 모두 담는다.
}
sb.append("\n");
System.out.print(sb); // 2. 출력
}
/*
* 수행 속도 비교
* nonBufferIO() : 281 (ms)
* bufferIO() : 15 (ms)
* → bufferIO > nonBufferIO
*/
❗️ Java는 Buffer의 장점을 Stream에 적용하여 BufferedInputStream
과 BufferedOutputStream
을 제공한다.
📌 Stream > Buffer > Chunks
📂 참고자료
👀 [유튜브 강의] Node.js 서버 만들기