Node Module System
•
console.log(module)
•
Node.js에서 Module를 통해 Script 간의 함수, 변수, 상수 등을 Import 및 Export를 할 수 있다.
•
module.exports는 객체 할당도 가능할 뿐 아니라, 직접 .(Dotting)을 통해서 객체 속성도 할당할 수 있다. (객체 할당을 하지 않을 수도 있다.) 이와 같이 객체에 대해서 할당할 때는 module.exports === exports로 간주하고, 이것이 가능한 이유는 서로 참조 관계에 있기 때문이다.
•
반면 객체에 대한 할당이 아니라, 함수를 할당을 한다거나 (속성이 없는 것들) 하면 module.exports !== exports이다. 따라서 이때는 exports로 대체하여 쓸 수 없다.
global 객체
•
console.log(global)
•
Node.js의 내장 객체이며, 전역 객체이다.
•
전역으로 설정된 값들을 모두 조회할 수 있다. 전역으로 선언되어 있기 때문에 별도의 객체를 Chaining하지 않고도 객체 속성만으로 호출할 수 있다.
•
global은 누군가가 특정 Key에 Value가 할당되어 있는지 정확히 알 수 없고, 누구나 어디에서든 global에 접근할 수 있기 때문에 쉽게 조작이 가능하다. 따라서 최대한 global에 직접 값을 대입하여 사용하는 것은 지양해야 한다.
console 객체
•
console.log(console)
•
일반적으로는 console.log(), console.error() Method 정도를 많이 쓴다.
•
성능 체크를 원한다면 console.time() Method를 쓰면 된다. 성능 체크를 시작할 부분에 console.time(), 끝 부분에 timeEnd()를 쓰면 되고 두 인자가 서로 같아야 한다.
•
console.dir()은 객체 전용 로그라고 보면 된다.
•
console.trace()를 통해서 Error 잡을 때 용이하게 할 수 있다. 이는 Call Stack을 추적하는 역할을 한다.
타이머 (setTimeout, setInterval, setImmediate)
•
setTimeout()을 통해서 Timer설정을 할 수 있고, setInterval()을 통해서 Interval을 설정할 수 있다. 두 Method 모두 Callback Function을 인자로 받으며, 시간 설정에 대한 단위는 ms이다.
•
두 Method는 const나 let에 할당하여 사용하고, 이렇게 했을 때 특이한 점은 할당된 const나 let은 일종의 아이디로 작용하기 때문에 할당된 const나 let으로 별도의 호출하지 않아도 된다. (즉, 할당 즉시 호출되는 것이다.)
•
두 Timer, Interval에 대한 설정은 clearTimeout()과 clearInterval()을 통해서 해제 할 수 있다.
•
Node.js에는 setImmediate()와 clearImmediate()도 존재한다. 즉시 실행 시킨다. 하지만 이를 거치지 않아도 Code는 Line by Line으로 바로 바로 실행되는데 이런게 왜 필요한 것일까? → setImmediate(), setTimeout(), setInterval()은 이전에 배운 대로 인자로 갖는 Callback Function들을 Event Loop를 통해서 Task Queue로 보내서, 실행 순서가 달라지게끔 비동기 Code로 처리하게 한다. 이 비동기 처리 과정 중에 setImmediate()를 통해서 바로 즉시 처리를 할 수 있게 되는 것이다. 즉, setImmediate()의 즉시 실행은 동기 Code에 대한 것이 아닌 비동기 Code에 대한 것으로 보면 된다. (위에서 말한 즉시 처리란, 호출 즉시 Event Loop를 통해서 Task Queue에 보내는 것을 의미한다.)
__filename, __dirname, process
•
__filename은 File까지 포함한 pwd를, __dirname은 FIle이 있는 폴더까지의 pwd를 나타낸다.
•
process는 전역 객체의 객체 속성이다. global이기 때문에 global을 생략한 채로 process만으로 쓸 수 있다. 이는 Node.js가 실행하고 있는 JavaScript의 정보들이 들어가 있다.
◦
preocess.version: 설치된 Node.js의 Version
◦
process.arch: 컴퓨터의 아키텍쳐
◦
process.platform: 운영체제
◦
process.pid: 실행중인 Process의 ID
◦
process.uptime(): 얼마나 실행되었는지 시간
◦
process.cwd(): 어디서 실행되고 있는지의 경로 (__dirname은 File의 위치를 말해주는 것이고, cwd()는 실행 중인 process의 위치이다.)
◦
process.execPath: 설치된 Node.js의 위치
◦
process.cpuUsage(): CPU 사용량
◦
process.exit(): Node.js의 종료
•
이런 Process 관련한 정보들은 몇 개를 제하고는 잘 사용은 하지 않는다. 그렇다면 이렇게 많은 정보들이 왜 존재하는 것인가? → Node.js로 Web Server 뿐만 아니라 Desktop Application을 만들 수도 있다. 이 때 사용되는 것이다. Desktop의 정보를 모으고, Resource를 관리할 때 쓰이는 것이다.
•
process에 대한 객체는 process.env, process.nextTick에서 자주 쓰이게 된다.
os Module
•
const os = require('os')
•
Node.js의 내장 모듈
•
os.arch(): 구동 중인 컴퓨터의 아키텍쳐
•
os.platform(): 구동 중인 운영 체제
•
os.type(): 구동 중인 운영 체제의 타입
•
os.uptime(): process.uptime()과의 차이점은 process.uptime()이 Process가 시작되고 흐른 시간이고, os.uptime()은 운영 체제가 시작되고 흐른 시간이다.
•
os.hostname(): 컴퓨터의 Host Name
•
os.release(): 컴퓨터 운영 체제의 Version
•
os.homedir(): 컴퓨터의 Home Directory
•
os.tmpdir(): 컴퓨터의 Temp Directory
•
os.freemem(): 추가로 사용 가능한 메모리
•
os.totalmem(): 총 사용 가능 메모리
•
os.cpus(): cpu에 대한 정보를 알려준다.
•
이 os 모듈도 Desktop Application을 만들 때 주로 사용한다. 참고로 os.cpus() 같은 경우는 현재 이용 중인 컴퓨터의 CPU Core 개수를 파악할 수도 있다. 이는 Node.js의 Single Thread 단점을 극복하는 Multi Processing에서 이용할 수 있다. Node.js는 Single Thread이기 때문에 Core를 하나 밖에 쓰지 않는다. 따라서 Core의 개수를 파악 후, 노는 Core 수 만큼을 반복문으로 돌려서 Process를 만든다. (Multi Processing)
path Module
•
const path = require('path')
•
Node.js의 내장 모듈
•
path.sep: 운영 체제 별, 경로 구분자 (Windows(\\), 나머지(/))
•
path.delimiter: 운영 체제 별, 환경 변수의 구분자 (Windows(;), 나머지(:))
•
path.dirname($fileName): File의 위치한 경로
•
path.extname($fileName): File의 확장자 이름
•
path.basename($fileName): File의 확장자를 제외한 이름
•
path.parse($fileName): File 이름의 구성 요소들로 나뉘어서 확인이 가능하다.
•
path.format($parsedFactors): Parsing된 객체를 인자로 주면 통합하여 나타내어 준다.
•
path.normalize($pathName): 인자로 받은 Path를 Normalize하여 나타낸다.
•
path.isAbosolute($pathName): 인자로 받은 Path가 절대 경로인지 확인한다.
•
path.relative($pathName1, $pathName2): 두 Path를 받아서 1에서 2로 가는 상대 경로를 보여준다.
•
path.join($pathFraction1, $pathFraction2, ... ): 절대 경로를 무시하고 그대로 인자들을 합친다.
•
path.resolve($pathFraction1, $pathFraction2, ...): 절대 경로를 고려하여 인자들을 합친다.
url Module
•
const url = require('url')
•
URL은 url.URL이라는 생성자를 통해서 생성할 수 있다. 이렇게 생성된 URL을 그냥 console에 찍게 되면 Object로 복잡하게 나타난다. 이는 WHATWG 방식의 주소 체계를 의미한다.
•
URL Object를 우리가 원하는 URL로 보기 위해서는 url.format($urlObject)을 통해서 볼 수 있다.
•
또한 url.parse($urlName)을 console에서 확인해보면 URL Object를 console로 찍은 것과 같이 복잡한 Object로 나타나게 되는 이는 새로운 방식의 WHATWG가 아닌, 기존 방식의 주소 체계이다.
•
그렇다면 기존 방식의 주소 체계와 새로운 WHATWG 방식의 주소 체계는 무엇이 다른 것인가? → 기존 방식의 경우 Host가 존재 하지 않아도 쓸 수 있다. 단, Search 처리는 새로운 주소 체계가 더 편하다.
•
WHATWG 방식의 URL에서 Search Params가 Search에 관여하는데 아래와 같은 Method들이 있다.
◦
$URL.searchParams.append()
◦
$URL.searchParams.getAll()
◦
$URL.searchParams.get()
◦
$URL.searchParams.has()
◦
$URL.searchParams.keys()
◦
$URL.searchParams.values()
◦
$URL.searchParams.set()
◦
$URL.searchParams.delete()
◦
$URL.searchParams.toString()
•
Search Parameter관련 Method는 FormData나 URLSearchParams 객체에도 비슷하게 사용된다.
•
url.parse()로 이뤄진 기존 주소 체계의 경우, 완벽한 형태의 URL이 아니라 중복되는 부분으로 인해 앞의 Domain 부분이 잘린 URL의 경우에 사용된다. WHATWG로는 이용이 불가능하기 때문이다.
querystring Module
•
const querystring = require('querystring')
•
querystring Module은 URL에서 url.parse()로 이용하던 기존 방식의 URL에서 자주 쓰는 Module이다. (WHATWG의 경우 Search Parameters라는 속성을 제공하기 때문에 별도의 querystring Module이 없어도 됐었다. → WHATWG가 Search에 조금 더 유용한 이유)
•
따라서 URL의 Query String을 얻을 수 있는 것인데, 이와 관련해서 Search Params에 비해 기능이 많이 부실하다.
•
Search Params의 경우, 여러 Search Param를 하나의 String로 합쳐주는 Method가 $URL.searchParams.toString()이었다면, Query String에서는 querystring.stringify() Method를 통해서 가능하다.
crypto 단방향 암호화 (Hash)
•
const crypto = require('crypto')
•
다양한 방식의 암호화를 돕는 Module이다. crypto의 단방향 함수로는 Hash를 이용하게 되고, 이는 비밀번호 암호화에 많이 쓰인다. (단방향 함수이기 때문에 복호화가 쉽게 되지 않는다.)
•
crypto.createHash()의 경우 동일 String에 대해서는 동일한 Hash값이 나오게 된다. 이 때, 주의해야 할 것이 있다. 간혹 동일하지 않은 String에 대해서 동일한 Hash 값이 나오는 경우도 있다! 따라서 createHash() Method 보다 더 안정적인 pbkdf2 암호화 알고리즘을 이용하는 Method를 사용한다. Code를 참고하자.
•
pbkdf2는 Salt값을 이용하는 암호화 방식이다. 일반적으로 Salt 값은 Random으로 생성된 64 Bytes를 base64로 나타낸 String이다. (base64 이외에도 다른 방식들도 있다.) crypto.createHash()의 경우 Hash에 대한 Brute Force에 조금 취약한 부분이 있기 때문에, 이를 보완 하고자 crypto.pbkdf2() Method를 사용한다. 이는 Salt String을 원래 비밀 번호에 추가한 다음, Iteration 수를 높여서 이용하게 된다. 즉, 암호화 하려는 String과 Salt 값, Iteration 수, 암호화된 String의 Byte 값, 암호화 방식을 인자로 받는다.
•
pbkdf2로 암호화를 진행했을 때, 암호화된 String과 Salt 값은 같이 저장하게 되고, Iteration 수는 1초 정도 소요되는 정도로 올려주는 것이 좋다.
crypto 양방향 암호화
•
단방향 함수의 경우에는 암호화만 신경 썼다면, 양방향 암호화는 암호화와 복호화를 모두 신경 쓰게 된다.
•
암호화
•
crypto.createCipher()라는 Method를 이용하며, 인자로 암호화 알고리즘(aes-256-cbc와 같은)과 암호화에 쓰일 Key String을 받는다. 이를 Cipher에 담는다.
•
위와 같이 생성된 암호화 방식을 .update()로 Cipher에 Chaining한다. update()의 인자는 암호화할 String, Encoding 방식, 출력 String 방식이다. 이렇게 나온 값을 Result 값에 담는다.
•
암호화의 마지막 단계는 .final() Method를 Cipher에 Chaining하는 것이다. 이를 통해 나온 값을 이전에 update() Method를 통해 얻은 Result에 Append한다. final() Method의 인자는 출력 String 방식이다.
** utf8 Encoding 평문을 base64 방식의 암호문으로
•
복호화
•
crypto.createDecipher() Method를 이용해서 복호화를 한다. 인자는 createCipher() Method와 마찬가지로 암호화 알고리즘과 암호화에 쓰이는 Key String을 받는다. 이를 Decipher에 담는다.
•
위와 같이 생성된 복호화 방식을 .update()로 Decipher에 Chaining한다. update()의 인자는 복호화할 값, 출력 String 방식, Encoding 방식이다. (암호화와 인자 순서가 다르다!) 이렇게 나온 값을 Result에 넣는다.
•
복호화의 마지막 단계 역시 암호화처럼 final() Method를 Decipher에 Chaining하여 마무리 짓는다. 이를 통해 나온 값을 update() Method를 통해 얻은 Result에 Append한다. final() Method의 인자는 암호화 때와 반대로 Encoding 방식이다.
** base64 방식의 암호문을 utf8 Encoding 평문으로
util Module (deprecate, promisify)
•
const util = require('util')
•
util Module에서 가장 많이 쓰이는 것은 callbackify, promisify, deprecate이 되겠다.
•
util.deprecate() Method를 통해, 지원이 곧 중단될 Method임을 알릴 수 있다. (API 작업 관련해서 많이 쓰게 된다.)
•
Method를 사용하다 보면 Callback Function들을 쓰게 되고, Callback Function의 Callback Function을 이용하게 되기도 한다. 이렇게 깊어지는 Depth 때문에 Code의 가동성이 떨어지는 문제를 해결하고자 Promisify를 이용하게 된다. 이는 Promise를 지원하지 않는 (err, data)와 같은 Callback Function에 대해서 Promise로 만들어 줄 수 있다. Code를 참고하자.
•
Callbackify는 반대로 Promise를 Callback으로 바꿔주는 역할을 한다.
fs Module (Sync & Async)
•
const fs = require('fs')
•
일반적인 JavaScript는 File System에 대한 접근이 제한적이지만, Node.js는 File System을 이용할 수 있다.
•
fs는 Promise를 꽤 최신 Version에서만 지원하기 때문에 Callback Function으로 처리하는 것이 일반적이다.
•
fs.readFile()을 통해서 File을 읽게 되면 Buffer에 있는 Binary로 읽게 된다. 이를 읽을 수 있는 String으로 바꾸기 위해선, toString() Method를 이용한다.
•
File을 읽고 쓰는데 있어서 동기적으로 처리를 하고 싶다면, Sync()가 붙은 Method를 이용하든가 아니면 Nested Callback Function으로 처리한다. (Sync()가 붙은 Method를 이용 시에 동기므로 Callback Function을 인자로 받지 않는다.)
•
이렇게 Sync()를 붙인 Method의 경우 Blocking이기 때문에 File이 큰 경우 Server에 문제가 생길 수 있다. 따라서 단순 Read, Write를 많이 쓰고, Callback Hell을 방지하기 위해 Promisify나 최신 fs에서 지원하는 Method를 이용한다. Sync()를 붙인 Method의 경우 Desktop Application에서 자주 쓰거나, 오로지 딱 한 번만 호출되는 것에 한에서는 쓰기도 한다.
Buffer & Stream
•
큰 데이터가 존재할 때, 이런 데이터는 한 번에 전송하는 것이 불가능하다. 따라서 나눠서 지속적으로 보내야 하는데, 이 때 사용하는 것이 Buffer이다. Buffer에 큰 데이터의 일부를 옮겨 담아서, Buffer가 꽉 차면 전송하고 Buffer를 비우고 다시 채우고 하는 식으로 반복된다. 이런 반복 행위를 Stream이라 한다. 즉, Stream은 일종의 연속된 Buffer라고 할 수 있다.
•
Stream의 경우 Readable Stream, Writable Stream으로 나뉜다. 각각 fs.createReadStream(), fs.createWriteStream()을 이용하며, 인자로 주어지는 옵션 Object의 highWaterMark는 몇 바이트 단위로 Stream을 이용할지 설정하는 것이다. 이렇게 나뉘어서 전송되는 데이터들을 Chunk라고 부른다.
•
Stream은 동작할 때, Event Driven으로 작동한다. 따라서 Buffer에 담긴 Chunk를 받을 때, Event가 발생하게 된다.
•
Readable Stream의 경우 data, end, error와 같은 Event들에 따라서 처리하도록 로직을 작성한다. Writable Stream의 경우 finish와 같은 Event를 쓸 수 있으며, $stream.write(), $stream.end()와 같은 Method도 이용한다.
•
Buffer라고 하는 Object도 global Object에 존재하는 Object이다. Buffer.concat($binaryChunks).toString()으로 값을 읽을 수 있도록 변환할 수 있다.
•
Buffer와 Stream에서 가장 중요한 것은 $stream.pipe($stream)라는 것이다. Stream은 Buffer의 흐름이기 때문에 여러 Stream을 이어서 Buffer가 흐르기 할 수 있다. 이 역할을 해주는 것이 pipe()이다. 정말 pipe() Method의 기능이 필요한 것이 아니라 단순히 파일에 대한 복사가 필요한 것이라면 편한 방법이 있다. pipe() Method 대신 fs.copyFile($readFilePath, $writeFilePath, $errorCallbackFunction)를 이용하도록 하자. 알아서 Stream을 연결해준다.
•
Stream은 일반적으로 zlib와 같이 File에 대한 압축이 필요할 때도 자주 사용하게 된다. (zlib.createGzip()도 Stream이다.)
기타 fs Module
•
위에서 제시한 Buffer, Stream, File의 Read와 Write를 제외하고 나머지 fs에서는 File이름을 바꾸거나 폴더를 생성하고 삭제하는 등의 작업을 할 수 있다. 자세한 것들은 Code를 참고하자.
•
fs.access($filePath, $authorization)는 폴더 혹은 File이 존재하는지 확인하는 Method이다.
•
fs.mkdir() Method를 통해서 폴더를 생성할 수 있다.
•
fs.open()이라는 Method를 통해서 File을 열 수 있다. 이 때 주어지는 인자 중에 열기 옵션을 줄 수있는데, 이 옵션 값이 'w'라면 열려고 하는 File이 존재하지 않을 시 새로 생성하게 된다. ('r', 'a'와 같은 옵션들도 있다.)
•
fs.readdir()을 통해서 폴더 내용을 확인할 수 있다. 해당 Method가 갖는 Callback Function의 인자로 존재하는 dir 값에 폴더 내용들이 들어 있다.
•
fs.unlink()를 통해서 파일을 삭제할 수 있다.
•
fs.rmdir()을 통해서 폴더를 삭제할 수 있다.
•
이와 같은 Method들을 이용하게 되면 Nested해서 Callback Function을 수행할 수 밖에 없는데, 이에 따라서 최근에 나온 fs Module은 Promise를 지원한다. const fs = require('fs').promises와 같이 이용한다.
events Module
•
const EventEmitter = require('events')
•
events Module은 Event를 발생시킬 수 있는 일종의 장치이다.
•
Node.js는 Event Driven이기 때문에, Event에 대해서 Handling하는 것은 굉장히 중요하다.
•
사용자가 서버에 방문을 하거나 글을 남기거나 댓글을 쓰는 등의 모든 행동들은 Event가 될 수 있다. 이를 위해서는 Event Listener를 미리 만든다. 이 말은 특정 Event가 발생했을 때 어떤 동작을 수행할지 미리 정의한 다는 것이다.
•
Event Listener의 추가는 $EventEmitter.addListener($eventName, $callbackFunction)을 통해서 이뤄지며, $EventEmitter.on()은 addListener() Function의 별칭 같은 것이다. (동일한 기능이다.)
•
동일한 Event에 대해서 여러 Event Listener를 추가할 수 있다.
•
$EventEmitter.once()라는 Method는 오로지 한 번만 수행된다. 두 번째 Event 발생의 경우 Callback Function이 수행되지 않는다.
•
Event의 정의는 위와 같이 하고, 호출은 $EventEmitter.emit($eventName)이라는 Method를 통해서 만들 수 있다.
•
Event Listening 도중에 Event Listener를 제거할 수도 있다. 특정 Event의 모든 Listener를 삭제하는 것은 $EventEmitter.removeAllListeners($eventName)이다. (동일 Event에 대해서 여러 Listeners를 추가할 수 있기 때문에...)
•
특정 Event의 한 Event Listener만 삭제하려면, $EventEmitter.removeListener($eventName, $eventCallbackFunction)이 된다. 즉, 특정 Event Listener를 삭제하기 위해선 Callback Function이 필요하므로 이런 Callback Function들을 const에 담아둘 필요가 있다.
•
특정 Event의 Listener 개수도 확인할 수 있다. $EventEmitter.listenerCount($eventName)을 통해서 가능하다.
예외 처리하기
•
예외란 Error에 대해서 미처 처리하지 못한 부분들을 말한다. 이런 부분들에 대해서 처리를 해주어야 Server Crash가 나지 않는다.
•
다른 Multi Thread 프로그램의 경우, 하나의 Thread가 죽더라도 다른 Thread들이 그 일을 대신 맡아서 하는 것이 가능하다. 하지만 Node.js의 경우 Single Thread이기 때문에, 이 Thread가 문제가 생기면 Server 자체가 문제가 생길 수 있는 것이다. 따라서 Node.js에서의 예외 처리는 매우 매우 중요하다.
•
Syntax Error와 Logical Error가 아닌 Running 도중에 예기치 못한 Error가 생겼을 때 처리를 어떻게 하는지 알아보자.
•
JavaScript에서 Error를 만들기 가장 쉬운 방법은 throw를 이용하는 것이다.
•
이 throw를 catch를 통해서 받아주지 않으면 Application은 Crash 된다. 일반적인 방법은 try catch 구문을 이용하는 것이다. 따라서 Application Crash를 방지하기 위해서, 무조건 Error에 대해서 Catch 해줘야 한다.
•
일반적으로는 Async Await를 이용하는 구문을 try catch로 감싸서 발생할 수 있는 Error에 대해서 대처한다. (Node.js 내장 Module을 이용하는 경우에는 Error를 Handling 할 수 있도록 Callback Function이 존재하기 때문에 Error가 발생하더라도 멈추진 않는다.)
•
try catch를 이용하지 않고 Error를 Catch하는 방법을 알아보자.
•
process Object에 uncaughtException이라는 이름의 Event Listener를 달아서 처리하면 된다.
•
그렇다면 process.on('uncaughtException') Method를 통해서 예기치 못한 Error들을 모두 처리할 수 있는가? → 반은 맞고, 반은 틀렸다. 모든 예기치 못한 Error가 기록되는 것은 사실이나, 기록만 남는 것이니, 이에 의존하지 않고 근본적인 Error의 원인을 찾아서 해결할 수 있어야 한다.
** process.on('uncaughtException') Method 안에 Server를 재구동 시키는 Code를 넣을 수도 있다. 하지만 이는 그렇게 권장되는 Code는 아니다. 왜냐하면 이런 Code는 해당 Event의 Callback Function안에 작성되어 돌아가게 되는데, 이 Callback Function의 호출 자체를 보장할 수 없어서 Server 재구동이 안될 수 있기 때문이다.