Ch 01. Node.js 소개
01. JavaScript 생태계와 Node의 역사
2008년 구글에서 새로운 브라우저 크롬과 함께 크롬에 들어간 V8 이라는 자바스크립트 엔진을 공개했습니다.
구글은 이것을 오픈소스로 공개합니다.
환경에 상관없이 작동합니다.
2009년에 누군가가 노드를 생각해냈다.
Express 는 2010년에 등장해서 Node 웹 서버 프레임워크의 표준이 되었다.
React 는 2013년에 등장해서 프론트의 트렌드를 바꿔놓았다.
2014년에 바벨과 웹팩이 등장했다.
자바스크립트는 리액트 네이티브로 안드로이드와 iOS 도 개발할 수 있다.
서버사이드 렌더링과 같은 고급기술을 사용할 수 있다.
02. Node의 특징, 강점, 약점
IO needs to be done differently.
IO 는 (지금까지와는) 다르게 되어야 한다.
Ryan Dahl, JSConf 2009.
2009년 Node.js 의 저자 라이언달의 프레젠테이션에서 했던 말.
JavaScript 식 비동기 처리 방식
저수준의 오래 걸리는 일은 Node 에게,
고수준의 로직은 메인 스레드에서.
Node 가 빠른 속도와 매우 높은 확장성을 갖는 근본적인 이유입니다.
C 와 WebAssembly
이미지 파일 전체를 돌면서 특정한 픽셀을 찾거나, 오디어 파일을 가지고 합치는 등의 작업은
C, Rust, Go 등이 빠르다.
CPU 레벨, 기계어 레벨로 컴파일하기 때문에 그 이상으로 더 빨라질 수 없는 정도이다.
C 와 WebAssembly 모듈을 바인딩해 사용하는 방법을 제공하고 있습니다.
C 는 node-gyp 를 통해, WebAssembly 는 Node 12 버전부터 제공되고 있습니다.
WebAssembly 란 ?
브라우저를 위한 어셈블리이다.
자바스크립트는 스크립트 언어이므로 런타임 때의 문제를 잡을 수 없다.
NPM 이란 ?
노드를 위한 패키지 매니저.
노드 패키지 매니저.
03. Glitch로 설정 없이 바로 Node 웹 서버 코딩해보기
server.js 에 모든 내용을 지운다.
require
어떤 모듈을 사용하겠다.
const http = require('http')
const server = http.createServer((req, res) => {
res.statusCode = 200
res.end('Hello!')
})
const PORT = 3000
server.listen(PORT, () => {
console.log('The server is listening at port ', PORT)
})
PREVIEW 의 Open preview pane 을 누르면 나오는 화면이다.
STATUS 와 LOGS, TERMINAL 도 있다.
팔레트: Cmd Shift P
node --version
버전확인node test.js
실행
Ch 02. 든든한 개발 환경 설정하기
01. VSCode 설치하기
Open Settings (JSON)
Open Default Settings (JSON)
- 단축키
모든 탭 닫기: Cmd K W
다른 탭 닫기: Option Cmd T
02. NodeJS 설치와 Node 버전 관리
LTS: Long Term Service. 안정적인 버전. 짝수버전이 보통이다.
NVM: Node Version Manager.
TJ/N
TJ/N: 강사가 추천하는 노드버전관리.
바이너리 CLI 이다.
which n
N 이 깔려있는지 확인한다.n
N 을 실행한다.n latest
최신 노드를 설치한다.$N_PREFIX
N 이 설치된 위치
- 설치
npm install -g n
노드가 이미 설치가 되어있다면 다음과 같이 설치한다.
설치가 되어 있지 않다면
curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n
bash n lts
# Now node and npm are available
npm install -g n
n lts
로 최신버전의 LTS Node.js 를 다운받을 수 있다고 한다.
Error: sudo required (or change ownership, or define N_PREFIX)
sudo n lts
를 입력 후 Password 를 입력한다.
만약 homebrew 로 설치한 노드 때문에 n
명렬어로 버전이 변경되지 않는다면
cd /opt/homebrew/bin
에서 node 를 삭제한다.
rm -rf node
03. npm, 린터, 포매터
패키지매니저 npm 을 통해 설치할 수 있다.
yarn 등 이 있다.
npm 은 노드만 설치하면 딸려온다.
성능상의 문제로 다른 패키지매니저를 사용할 수 있다.
npm init -y
package.json 을 만든다.
- 항목
name
필요없음version
필요없음description
필요없음main
scripts
프로젝트 관리하며 자주 사용하는 스크립트. test
npm run 키
라고 scripts 에 있는 명령어를 실행할 수 있다.
에디터창으로 커서: Cmd 1, Control Tab
Formatting: 미적인 것. 세미콜론이 붙었는지 등
Linting: 베스트 캠퍼스. 최대한 지키면 좋은 것들, 에러가 적게 나도록 돕는다.
- 프리티어 설치
npm install --save-dev prettier
의존성을 나열한다.
package-lock.json: 실제로 설치된 것
lock.json 도 git 에 저장하는게 좋다.
^2.2.1
버전이 정확하지 않아도 된다.
npm 도 lock 파일을 보고 정확한 버전을 설치한다.
.pretierrc 파일 추가
{
"semi": false,
"singleQuote": false
}
VSCODE 에 이 프리티어를 보고 포매팅을 하도록 지시할 수 있다.
- vscode
- settings.json
VSCODE 가 보는 로컬세팅을 모아둔다.
이 프로젝트에만 적용되는 세팅이다.
{
"[javascript]": {
"editor.formatOnSave": true,
}
}
prettier 를 플러그인에 검색하면 Prettier - Code formatter
설정을 하면 포매팅이 자동으로 된다.
04. ESLint
패키지이자 플러그인이다.
npm install --save-dev eslint
node modules 폴더를 추가한다.
.eslintrc.js 를 만들어 설정을 여기에 저장한다.
에어비엔비에서 내놓은 config 가 있다.
https://github.com/airbnb/javascript
npm install --save-dev eslint-config-airbnb-base eslint-plugin-import
두 가지를 동시에 설치한다.
npm install --save-dev eslint-config-prettier
formatter 가 고친 것을 eslint 가 문제삼지 않는다.
이 프리티어 eslint 플러그인이 잘 동작하려면 항상 마지막에 와야한다.
/* eslint-disable-next-line */
eslint 를 한 줄 끈다.
/* eslint-disable-next-line no-console */
module.exports = {
extends: ['airbnb-base', 'prettier'],
}
npm install
--save-dev eslint-plugin-node
module.exports = {
extends: ['airbnb-base', 'plugin-node/recommended', 'prettier'],
}
best practice 에 대해 신경쓰게 하기위해 린터를 사용하자.
05. TypeScript로 타입 에러 체크하기
컴파일언어에서는 타입문제가 일어나지 않는다.
- 타입스크립트 설치
install --
save-dev typescript
단축키
- 다음에러 ?
- 에러문구 띄우기 ?
지정 단축키
- Option +: Font zoom in
- Option -: Font zoom out
06. node 환경에서 TypeScript 도움 받기
main.js 가장 위에 // @ts-check
라고 작성하면 타입스크립트가 동작한다.
노드에 타입스크립트를 적용하기 위해
노드에서 자주 쓰는 타입들을 적용한다.
npm install --save-dev @types/node
// @ts-check
const http = require('http')
const server = http.createServer((req, res) => {
res.statusCode = 200
res.end('Hello!')
})
const PORT = 4000
server.listen(PORT, () => {
console.log('The server is listening at port: ${PORT}.')
})
- 서버를 실행
node main.js
- 서버를 확인
http localhost:4000
07. TypeScript와 jsconfig
tsconfig.js 에 타입스크립트 설정을 저장한다.
{
"compilerOptions": {
"strict": true
},
"include": [
"src/**/*"
]
}
strict
깐깐하게 검사. true 로 두면 좋다.
단축키
- (Shift) F8: 현재 프로젝트에서 다음 에러를 찾는다.
- 에러
json script for a javascript project using typescript
jsconfig.json 레퍼런스
https://code.visualstudio.com/docs/languages/jsconfig
08. 환경설정 종합
Ch 03. JavaScript 기초 이론 다지기
01. call stack, non-blocking IO, event loop
Node 를 잘 이해하기 위해서는 자바스크립트의 동시성 모델에 대해 잘 이해해야 합니다.
이벤트 루프 모델은 여러 스레드를 사용합니다.
그 중 우리가 작성한 자바스크립트 코드가 실행되는 스레드를 메인 스레드라 부릅니다.
한 Node.js 프로세스에서 메인 스레드는 하나이며, 한 순간에 한 줄씩만 실행합니다.
그러나 그 외의 일 (file I/O, network...) 을 하는 워커 스레드는 여럿이 있을 수 있습니다.
Call Stack
콜 스택이란, 지금 시점까지 불린 함수들의 스택입니다.
함수가 호출될 때 쌓이고, 리턴할 때 빠집니다.
Run-to-completion
이벤트 루프가 다음 콜백을 처리하려면 지금 처리하고 있는 콜백의 실행이 완전히 끝나야 합니다.
call stack 이 완전히 빌 때까지 처리한다는 것과 동일합니다.
Callback Queue
콜백 큐 (메세지 큐) 는 앞으로 실행할 콜백 (함수와 그 인자) 들을 쌓아두는 큐입니다.
콜백은 브라우저나 Node 가 어떤 일이 발생하면 (event) 메인 스레드에 이를 알려주기 위해 (callback) 사용됩니다.
이벤트는 파일처리의 완료, 네트워크 작업의 완료, 타이머 호출 등이 있습니다.
1 -> 3 -> 2 가 찍히게 된다.
1, 3 을 찍고난 후 Call stack 이 빈 상태가 되면 2 가 출력된다.
Event Loop - Blocking
call stack 이 빌 때까지는 다음코드를 실행하지 않는다.
이런 경우 event loop 를 block 한다고 합니다.
non-blocking I/O & offloading
02. scope, hoisting
Hosting - var
변수의 선언은 위에서 한다.
초기화만 되지 않았다.
그래서 에러가 나지 않는다.
이것을 hoisting 이라고 한다.
function 도 아래에 작성해도 된다.
함수의 선언과 값의 초기화는 서로 다릅니다.
Function, lexical scope
exical scope 는 안에서 밖을 볼 수는 있지만 밖에서 안을 참조할 순 없다.
var, block scoping
그러나 let 과 const 는 block scoping 이 됩니다.
03. closure
Closure
closure = function + environment
closure 는 funcion 이 하나 생길 때마다 하나씩 생깁니다.
- 함수: print
- 환경: x -> "salt"
colsure 는 higher-order function 을 만드는 데 유용합니다.
각각의 다른 closure 가 생긴 것을 볼 수있다.
즉, 둘은 서로 다른 closure 이다.
Closure 의 예시 - counter
노드 디버거
.vscode 폴더 내에 launch.json 파일을 만든다.
Add Configuration 을 누르고
Launch via NPM
를 선택한다.
RUN AND DEBUG 탭에 가서 Launch via NPM 을 선택하고 breakpoint 를 찍어 실행해본다.
- 다음 브레이크 포인트: F5 (인텔리제이 F9)
- Step over: F10 (인텔리제이 F8)
- Step into: F11
- Step out: F12
04. prototype
상속을 구현한 개념.
단축키
- 디버그: F5
- 라인삭제: Cmd Shift K
- 라인이동: Control G
- 라인복제: Option Shift Up/Down
- 디버그 콘솔
- Hi
작은 따옴표가 아니라 ~ 의 점을 사용한다.
// Prototype
function Student(name) {
this.name = name
}
Student.prototype.greet = function greet() {
return `Hi, ${this.name}!`
}
const me = new Student('Connor')
console.log(me.greet())
// Prototype
function Person(name) {
this.name = name
}
Person.prototype.greet = function greet() {
return `Hi, ${this.name}!`
}
function Student(name) {
this.__proto__.constructor(name)
}
Student.prototype.study = function study() {
return `${this.name} is studying.`
}
Object.setPrototypeOf(Student.prototype, Person.prototype)
const me = new Student('Connor')
console.log(me.greet())
console.log(me.greet())
const anotherPerson = new Person('Foo')
console.log(anotherPerson instanceof Student)
console.log(anotherPerson instanceof Person)
console.log([] instanceof Array)
스튜던트의 prototype 안에 prototype 이 있다.
이것은 ES5 syntax (구형 문법) 이다.
다음시간에 ES6 를 배워보자.
- ES6 문법
class Person {
constructor(name) {
this.name = name
}
greet() {
return `Hi, ${this.name}.`
}
}
class Student extends Person {
constructor(name) {
super(name)
}
study() {
return `${this.name} is studying.`
}
}
const me = new Student(`Connor`)
console.log(me.study())
console.log(me.greet())
문자열은 ` 혹은 ' 에 작성해도 되지만 ` 에 작성하면 this.name 과 같이 작성할 수 있다.
Ch 04. 모던 JavaScript 살펴보기
01. 자바스크립트의 최근 변화, TC39, 그리고 node
비영리 가구 Ecma International 은 JavaScript 를 포함한 다양한 기술 표준 정립을 목적으로 하는 단체입니다.
그 중 TC39 위원회 (committee) 는 자바스크립트 (ECMAScript) 표준 제정을 담당합니다.
이 위원회에는 Microsoft, Google, Apple 등 웹 기술과 관계가 깊은 거대 기술벤더들이 참여합니다.
대부분의 논의 내용이 Github 등 웹에 공개되어 있습니다.
People 탭에 들어가면 TC39 커뮤니티와 함께 일하는 분들을 알 수 있다.
GitHub, Microsoft, Redhat, Google, 테슬라 등에서 일하시는 분들이 있다.
https://tc39.es/process-document/
Proposal 은 챔피언을 선정한다.
Proposal - Draft - Candidate -
Proposals 레포지토리에 들어가면 현재 진행중인 안건들을 볼 수 있다.
https://github.com/tc39/proposals
브라우저, 노드 (브라우저가 아닌 런타임) 에서 확인할 수 있다.
또는 지원하는 기능들을 여기서 확인할 수 있다.
02. let과 const
ES2015 에 추가되었다.
var 와는 다르게 let 과 const 가 있다.
hoisting 규칙이 없고 block scoping 을 지원한다.
var 보다 예측가능하게 한다.
const 는 상수이므로 값을 바꿀 수 없다.
let 은 같은 scope 에서 다시 선언할 수 없다.
let 과 const 는 변수를 정의하기 전에는 사용할 수 없다.
const x = 1
{
const x = 2
console.log(x) // 2
}
console.log(x) // 1
결론
- let 과 const 의 예측 가능성과 유지보수성이 var 보다 훨씬 뛰어납니다.
- 가능하다면 const 만 쓰고, 필요한 경우에 한해 let 을 쓰고, var 는 절대 쓰지 마세요!
03. spread operator의 다양한 활용
Spread syntax
- ES2015 에서 새로 추가된 syntax
- 병합, 구조 분배 할당 (destructuring) 등에 다양하게 활용할 수 있습니다.
object merge (1)
const personalData = {
nickname: 'JH',
email: 'jh12@email.com',
}
const publicData = {
age: 22,
}
const user = {
...personalData,
...publicData,
}
object merge (2)
덮어씌우기
const overrides = {
DATABASE_HOST: 'myhost.com',
DATABASE_PASSWORD: 'mypassword',
}
const config = {
DATABASE_HOST: 'default.host.com',
DATABASE_PASSWORD: '****',
DATABASE_USERNAME: 'myuser',
...overrides,
}
/*
{
DATABASE_HOST: 'myhost.com',
DATABASE_PASSWORD: 'mypassword',
DATABASE_USERNAME: 'myuser',
}
*/
Object rest
나머지를 뽑아내는 것도 가능하다.
const user = {
nickname: 'Connor',
age: 22,
email: 'kor.connor@gmail.com',
}
const {nickname, ...personalData} = user
console.log(personalData)
array merge
병합
const pets = ['dog', 'cat']
const predators = ['wolf', 'cougar']
const animals = [...pets, ...predators]
console.log(animals) // ['dog', 'cat', 'wolf', 'cougar']
const ary = [1, 2, 3, 4, 5]
const [head, ...rest] = ary
console.log(head, ...rest)
const personalData = {
email: 'abc@def.com',
password: '****'
}
const publicData = {
nickname: 'foo'
}
const overrides = {
email: 'fff@fff.com',
}
const user = {
...personalData,
...publicData,
...overrides,
}
console.log(user)
email 이 덮어씌어지는 것을 볼 수 있다.
const shouldOverride = true
const user = {
...{
email: 'abc@def.com',
password: '****'
},
...{
nickname: 'foo',
},
...(shouldOverride
? {
email: 'fff@fff.com',
}
: null),
}
다양한 활용이 가능하다.
function foo(head, ...rest) {
console.log(head)
console.log(rest)
}
foo(1, 2, 3, 4)
04. functional approach(1)
함수 자체가 객체로 취급될 수 있다.
자바스크립트를 함수형 언어로 보는 사람도 있다.
함수형 어프로치를 많이 사용된다.
단축키
- 블럭주석: Option Shift A
const people = [
{
age: 20,
city: '서울',
pet: ['cat', 'dog'],
},
{
age: 40,
city: '부산',
},
{
age: 31,
city: '대구',
pet: ['cat', 'dog'],
},
{
age: 36,
city: '서울',
},
{
age: 27,
city: '부산',
pet: 'cat',
},
]
/*
다음 문제들을 풀어봅시다.
A. 30 대 미만이 한 명이라도 사는 모든 도시
B. 각 도시별로 개와 고양이를 키우는 사람의 수
*/
function solveA() {
const cities = []
for (const person of people) {
console.log(person)
}
return cities
}
console.log('solveA', solveA())
function solveA() {
const cities = []
for (const person of people) {
if (person.age < 30) {
if (!cities.find(city => person.city === city)) {
cities.push(person.city)
}
}
}
return cities
}
console.log('solveA', solveA())
- 두번째 방법
function solveAModern() {
const allCities = people.filter(person => person.age < 30).map(person => person.city)
const set = new Set(allCities)
return Array.from(set)
}
console.log('solveAModern', solveAModern())
- 세번째 방법
function solveAModern() {
const allCities = people.filter(({age}) => age < 30).map(({city}) => city)
const set = new Set(allCities)
return Array.from(set)
}
04. functional approach(2)
// B. 각 도시별로 개와 고양이를 키우는 사람의 수
/*
{
"서울": {
"dog": 2,
"cat": 1,
},
"대구": {
"dog": 1,
"cat": 1,
},
"부산": {
"cat": 1,
},
}
*/
/** @typedof {Object.<string, Object<string, number>>} PetsOfCities */
function solveB() {
/** @type {PetsOfCities} */
const result = {}
for (const person of people) {
const {city, pet: petOrPets} = person
if (petOrPets) {
const petsOfCity = result[city] || {}
if (typeof petOrPets === 'string') {
const pet = petOrPets
const origNumPetsOfCity = petsOfCity[pet] || 0
petsOfCity[pet] = origNumPetsOfCity + 1
} else {
for (const pet of petOrPets) {
const origNumPetsOfCity = petsOfCity[pet] || 0
petsOfCity[pet] = origNumPetsOfCity + 1
}
}
result[city] = petsOfCity
}
}
return result
}
console.log('solveB', solveB())
function solveBModern() {
return people.map(({pet: petOrPets, city}) => {
const pets = (typeof petOrPets === 'string' ? [petOrPets] : petOrPets) || []
return {
city,
pets,
}
/*
[
[
{"서울", "cat"},
{"서울", "dog"},
],
[
{"부산", "dog"},
],
]
*/
}).map(({city, pets}) => pets.map(pet => [city, pet]))
}
console.log('solveBModern', solveBModern())
3차원 배열이 나옴을 볼 수 있다.
- flat()
function solveBModern() {
return people.map(({pet: petOrPets, city}) => {
const pets = (typeof petOrPets === 'string' ? [petOrPets] : petOrPets) || []
return {
city,
pets,
}
/*
[
[
{"서울", "cat"},
{"서울", "dog"},
],
[
{"부산", "dog"},
],
]
*/
}).map(({city, pets}) => pets.map(pet => [city, pet])).flat()
- flatMap()
function solveBModern() {
return people.map(({pet: petOrPets, city}) => {
const pets = (typeof petOrPets === 'string' ? [petOrPets] : petOrPets) || []
return {
city,
pets,
}
/*
[
[
{"서울", "cat"},
{"서울", "dog"},
],
[
{"부산", "dog"},
],
]
*/
}).flatMap(({city, pets}) => pets.map(pet => [city, pet]))
}
flatMap 도 똑같이 동작한다.
optional chaining 이라는게 있다.
function solveBModern() {
return people.map(({pet: petOrPets, city}) => {
const pets = (typeof petOrPets === 'string' ? [petOrPets] : petOrPets) || []
return {
city,
pets,
}
/*
[
[
{"서울", "cat"},
{"서울", "dog"},
],
[
{"부산", "dog"},
],
]
*/
}).flatMap(({city, pets}) => pets.map(pet => [city, pet]))
.reduce((/** @type {PetsOfCities} */ result, [city, pet]) => {
if (!city || !pet) {
return result
}
return {
...result,
[city]: {
...result[city],
[pet]: (result[city]?.[pet] || 0) + 1
},
}
}, {})
}
// console.log('solveB', solveB())
console.log('solveBModern', solveBModern())
결과가 같은 것을 볼 수 있다.
05. Promise
MDN Web docs: https://developer.mozilla.org/ko/
Promise: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise
new Promise((resolve, reject) => {
console.log('Inside promise')
resolve('First resolve')
}).then(value => {
console.log('Inside first then')
console.log('value', value)
})
new Promise((resolve, reject) => {
console.log('Inside promise')
reject(new Error('First reject'))
resolve('First resolve')
}).then(value => {
console.log('Inside first then')
console.log('value', value)
}).catch(error => {
console.log('error', error)
})
new Promise((resolve, reject) => {
console.log('Inside promise')
reject(new Error('First reject'))
console.log('before resolve')
resolve('First resolve')
console.log('after resolve')
}).catch(error => {
console.log('error', error)
}).then(value => {
console.log('Inside first then')
console.log('value', value)
})
resolve 나 reject 중 먼저 타는 것으로 then 이나 catch 구문을 탄다.
- Promise chain
function returnPromiseForTimeout() {
return new Promise(resolve => {
setTimeout(() => {
resolve(Math.random())
}, 1000)
})
}
returnPromiseForTimeout()
.then((value) => {
console.log(value)
return returnPromiseForTimeout()
}).then(value => {
console.log(value)
return returnPromiseForTimeout()
}).then(value => {
console.log(value)
return returnPromiseForTimeout()
}).then(value => {
console.log(value)
return returnPromiseForTimeout()
})
returnPromiseForTimeout()
setTimeout(() => {
const value = Math.random()
console.log(value)
setTimeout(() => {
const value = Math.random()
console.log(value)
setTimeout(() => {
const value = Math.random()
console.log(value)
setTimeout(() => {
const value = Math.random()
console.log(value)
}, 1000)
}, 1000)
}, 1000)
}, 1000)
위는 시간 순서대로. 스코프가 나눠져있으므로 상호참조가 일어날 수가 없다.
Node 는 콜백스타일이였지만 지금부터는
// @ts-check
const fs = require('fs')
fs.readFile('.gitignore', 'utf-8', (error, value) => {
console.log(value)
})
// @ts-check
const { rejects } = require('assert')
const fs = require('fs')
/**
*
* @param {string} fileName
*/
function readFileInPromise(fileName) {
return new Promise((resolve, reject) => {
fs.readFile('.gitignore', 'utf-8', (error, value) => {
if (error) {
reject(error)
}
console.log(value)
})
})
}
readFileInPromise('.gitignore').then((value => console.log(value)))
- 최근엔 node 에서 promise 형태의 API 를 제공하고 있다.
fs.promises.readFile('.gitignore', 'utf-8')
.then((value => console.log(value)))
async 는 promise 를 돌려주는 함수이다.
/**
* @param {number} duration
*/
async function sleep(duration) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(undefined)
}, duration);
})
}
async function main() {
console.log('first')
await sleep(1000)
console.log('second')
await sleep(1000)
console.log('third')
await sleep(1000)
console.log('finish!')
}
main()
promise 를 리턴하는 함수는 async 로 만들 수 있다.
async 는 다른 async 함수 안에서 await 할 수 있다.
단축키
- 동시에 이름수정: F2
위 코드를 promise 형태로 짠다면
async function 안에서는 다른 async 를 await 할 수 있다.
const fs = require('fs')
async function main() {
const result = await fs.promises.readFile('.gitignore', 'utf-8')
console.log(result)
}
main()
아까와 똑같은 일이 일어나지만 훨씬 더 코드가 짧아진다.
promise 로 await 하면 되므로 코드가 오른쪽으로 깊어질 이유가 없다.
위에서 아래로 쭉쭉 쓴다.
callback hell 은 async 와 await 만 잘써도 사라진다.
promise 와 async 관계를 이렇게 볼 수 있다.
만약 파일을 오타를 낸다면 에러가 난다.
promise 는 catch 로 잡지만 async 와 await 에서는
- 오타
const fs = require('fs')
async function main() {
try {
const result = await fs.promises.readFile('.gitignora', 'utf-8')
console.log(result)
} catch (error) {
console.log('error', error)
}
}
main()
06. polyfill, transpile
모던 자바스크립트를 100% 사용할 수 있게 해준다.
Polyfill 이란 ?
core-js: https://github.com/zloirock/core-js
많은 feature 들이 브라우저나 노드에서는 구현되지 않는다.
불가피하게 노드 버전을 올릴 수 없을 때,
core-js 라이브러리를 사용하면 된다.
package.json 을 보고 dependency 를 볼 수 있다.
설치방법: https://github.com/zloirock/core-js#installation
npm install core-js
Array
- flat: node 14 이상이면 core-js 없이 사용할 수 있다.
const complicatedArray = [1, [2, 3]]
const flattedArray = complicatedArray.flat()
console.log(flattedArray)
강사님은 flat() 에 경고가 나서 "lib": ["es2019"] 를 추가했다.
n 10 이라고 입력해 노드버전을 10 으로 바꿔보자.
core-js 없이 노드 10 버전에서는 flat 함수를 쓰지 못하고 에러가 나는 것을 볼 수 있다.
node.green 은 node 가 es 2015 support 를 잘 해주고있는지 보여주기 위한 사이트였다.
tring.prototype.replaceAll 와 Promise.allSettled 로 폴리필을 테스트해보자.
require('core-js')
const original = 'abcabc123'
const changed = original.replace('abc', '123')
console.log(changed)
단축키
- Quick fix: Cmd .
require('core-js')
const original = 'abcabc123'
const changed = original.replaceAll('abc', '123')
console.log(changed)
/**
*
* @param {number} duration
* @returns
*/
function sleep(duration) {
return new Promise((resolve) => {
console.log('sleep start')
setTimeout(() => {
console.log('sleep done', duration)
resolve(null)
}, duration)
})
}
Promise.all([
sleep(1000),
sleep(1500),
sleep(2000),
]).then(() => {
console.log('Promise.all done!')
})
/**
*
* @param {number} duration
* @returns
*/
function sleep(duration) {
return new Promise((resolve) => {
console.log('sleep start')
setTimeout(() => {
console.log('sleep done', duration)
resolve(null)
}, duration)
})
}
function alwaysReject() {
return new Promise((resolve, reject) => {
reject()
})
}
Promise.all([
sleep(1000),
sleep(1500),
sleep(2000),
alwaysReject()
]).then(() => {
console.log('Promise.all done!')
})
reject 되며 then 의 console.log 가 실행되지 않았다.
모두가 resolve 되어야 다음으로 넘어가는데 reject 가 발생했다.
/**
*
* @param {number} duration
* @returns
*/
function sleep(duration) {
return new Promise((resolve) => {
console.log('sleep start')
setTimeout(() => {
console.log('sleep done', duration)
resolve(duration)
}, duration)
})
}
function alwaysReject() {
return new Promise((resolve, reject) => {
reject()
})
}
Promise.all([
sleep(1000),
sleep(1500),
sleep(2000),
// alwaysReject()
]).then((value) => {
console.log('Promise.all done!', value)
})
require('core-js')
/**
*
* @param {number} duration
* @returns
*/
function sleep(duration) {
return new Promise((resolve) => {
console.log('sleep start')
setTimeout(() => {
console.log('sleep done', duration)
resolve(duration)
}, duration)
})
}
function alwaysReject() {
return new Promise((resolve, reject) => {
reject()
})
}
Promise.allSettled([
sleep(1000),
sleep(1500),
sleep(2000),
alwaysReject()
]).then((value) => {
console.log('Promise.all done!', value)
})
replaceAll 은 node 15 부터 가능하다.
그 이전 버전에서는 core-js 를 가져와서 사용하면 된다.
- Transpile 이란 ?
코드를 A 언어에서 B 언어로 변환하는 작업을 뜻합니다.
자바스크립트를 대상으로 하는 트랜스파일러는 Babel, tsc (TypeScript Compiler), ESBuild 등이 있습니다.
esbuild main.js --bundle --outfile-build/main.js --target=node10.4
esbuild: https://esbuild.github.io/
트랜스파일러로 사용할 수 있다.
const objs = [
{
foo: {
bar: 1,
},
},
{},
{
foo: {},
},
]
console.log(objs.map((obj) => obj.foo.bar))
const objs = [
{
foo: {
bar: {
vaz: 1,
},
},
},
{},
{
foo: {},
},
]
console.log(
objs.map((obj) => {
const {foo} = obj
if (foo) {
const { bar } = foo
if (bar) {
return bar.baz
}
}
return undefined
})
)
- Optional chaining
const objs = [
{
foo: {
bar: {
vaz: 1,
},
},
},
{},
{
foo: {},
},
]
console.log(
objs.map((obj) => {
const {foo} = obj
if (foo) {
const { bar } = foo
if (bar) {
return bar.baz
}
}
return undefined
})
)
console.log(objs.map((obj) => obj.foo?.bar.baz))
n 12 도 에러가 난다.
노드가 구버전이라 문법조차 이해하지 못한다.
그래서 core-js 를 사용할 수 없다.
es build 를 설치해 해결하자.
npm install esbuild
"scripts": {
"debug": "node src/main.js",
build": esbuild main,js --bundle-out
- package.json 에 다음과 같이 build 를 추가한다.
"scripts": {
"debug": "node src/main.js",
"build": "esbuild src/main.js --bundle --outfile=build/main.js --target=node12.22"
},
esbuild 를 main.js 를 build 해서 번들링 후에 build/main.js 에 결과를 내놓고, node10.4 에서 실행될 수 있게 해달라.
현재 노드 버전이 v12.22.12 이므로
구버전에서도 신형 syntax 를 사용하고 싶다면
build 후 node build/main.js 로 실행하면 된다.
트랜스파일은 깊게 다룰 주제는 아니다.
Ch 05. 프레임워크 없이 간단한 RESTful API 서버 만들어보기
01. 프로젝트 개요
프레임워크란 ?
웹서버: express, core 등
jsconfig.json 은 타입스크립트의 config 를 도와준다.
.pretierrc 에서 프리티어를 설정한다.
VSCODE 를 언어별로 설정할 수 있다.
jsonc 란 VSCODE 에서만 특별하게 사용된다.
json with Comments 이다.
확장자는 json 이지만 주석을 넣을 수 있다.
강의환경의 버전은 다음과 같다.
02. 단순한 API 라우팅 처리와 HTTPie를 이용한 테스팅
HTTPie 라는 툴은 URL 만 넣으면 테스트할 수 있다.
Curl 이라는 유닉스 커맨드라인보다 사용하기 편하다.
맥에서의 설치는 brew 로 설치하면 된다.
윈도우에서는 sudo apt httpie 로 설치하면 된다.
- 맥에서 httpie 설치 명령어
brew install httpie
생각했던 것 보다 설치하는데 오래걸렸다..
이제 http 명령어를 사용할 수 있다.
- main.js
// @ts-check
// 프레임워크 없이 간단한 토이프로젝트 웹 서버 만들어보기
/**
* 블로그 포스팅 서비스
* 로컬파일을 데이터베이스로 활용할 예정 (JSON)
* 인증로직은 넣지 않습니다.
* RESTful API 를 사용합니다.
*/
const http = require('http')
const server = http.createServer((req, res) => {
res.statusCode = 200
res.end('Hello!')
})
const PORT = 4000
server.listen(PORT, () => {
console.log(`The server is listening at port: ${PORT}`)
})
넘어오는 url 을 확인해보자.
코드를 수정했다면 서버를 재실행해야 한다.
코드를 수정 후 서버 재실행을 하지않기 위해 노드몬을 설치해보자.
npm install --save-dev nodemon
개발용으로만 필요한 dependency 이므로 --save-dev 를 붙여준다.
노드몬의 버전도 맞춰준다.
"nodemon": "^2.0.7",
npm install 시 package-lock.json 파일도 업데이트 되는 듯 하다.
- 노드몬으로 서버 실행
npm run nodemon src/main.js
package.json 에 아래와 같은 코드를 추가하고
"scripts": {
"server": "nodemon src/main.js"
},
npm run server 명령어를 실행한다.
서버가 잘 실행되는 것을 볼 수 있다.
- URL 에 따른 분기처리
const { Console } = require('console')
const http = require('http')
/**
* POST
*
* GET /posts
* GET /posts/:id
* POST /posts
*/
const server = http.createServer((req, res) => {
if (req.url === '/posts' && req.method === 'GET') {
res.statusCode = 200
res.end('List of posts')
} else if (req.url === '/posts/:id') {
res.statusCode = 200
res.end('Some content of the post')
} else if (req.url === '/posts' && req.method === 'POST') {
res.statusCode = 200
res.end('Creawting post')
} else {
res.statusCode = 404
res.end('Not found.')
}
})
const PORT = 4000
server.listen(PORT, () => {
console.log(`The server is listening at port: ${PORT}`)
})
자바스크립트에서 정규표현식
} else if (req.url && /^\/posts\/[a-zA-Z0-9-_]+$/.test(req.url)) {
res.statusCode = 200
res.end('Some content of the post')
아마 정규표현식에 대해 잘 모르는 분들은 강의를 들으며 이해가 가지 않을 것 같긴 하다.
03. 정규식을 활용한 URL 인자 추출
정규식에 캡처라는 것이 있다.
캡처하고 싶은 부분을 괄호로 감싸면 캡처할 수 있다.
const PORTS_ID_REGEX = /^\/posts\/([a-zA-Z0-9-_]+)$/
위와 같이 괄호로 감싸면
배열에 캡처된 부분이 따로 담기는 것을 볼 수 있다.
- 정규표현식으로 뽑아낼 수 있다.
} else if (req.url && PORTS_ID_REGEX.test(req.url)) {
const regexResult = PORTS_ID_REGEX.exec(req.url)
if (regexResult) {
const postId = regexResult[1]
console.log(postId)
}
res.statusCode = 200
res.end('Some content of the post')
const postIdRegexResult = req.url && PORTS_ID_REGEX.exec(req.url) || undefined
- ||: 왼쪽 값이 true 이면 오른쪽 값이 반영되지 않는다.
- &&: 왼쪽값이 아니면 오른쪽 값으로 넘어간다.
트랜스파일을 안하기 때문에 구식이지만 이 syntax 를 알아두자.
res.statusCode 가 없으면 응답이 없어서 요청에서 돌아오지 않는다.
- 중간정리
// @ts-check
// 프레임워크 없이 간단한 토이프로젝트 웹 서버 만들어보기
/**
* 블로그 포스팅 서비스
* 로컬파일을 데이터베이스로 활용할 예정 (JSON)
* 인증로직은 넣지 않습니다.
* RESTful API 를 사용합니다.
*/
const { Console } = require('console')
const http = require('http')
/**
* POST
*
* GET /posts
* GET /posts/:id
* POST /posts
*/
const server = http.createServer((req, res) => {
const PORTS_ID_REGEX = /^\/posts\/([a-zA-Z0-9-_]+)$/
const postIdRegexResult
= req.url && PORTS_ID_REGEX.exec(req.url) || undefined
if (req.url === '/posts' && req.method === 'GET') {
res.statusCode = 200
res.end('List of posts')
} else if (postIdRegexResult) {
// GET /posts/:id
const postId = postIdRegexResult[1]
console.log(`postId: ${postId}`)
res.statusCode = 200
res.end('Reading a post')
} else if (req.url === '/posts' && req.method === 'POST') {
res.statusCode = 200
res.end('Creawting post')
} else {
res.statusCode = 404
res.end('Not found.')
}
})
const PORT = 4000
server.listen(PORT, () => {
console.log(`The server is listening at port: ${PORT}`)
})
04. JSDoc을 활용해 타입 세이프티 챙기기
포스트 타입을 정해보자.
jsdoc: https://jsdoc.app/
jsdoc 은 주석으로 타입정보를 만들 수 있다.
/**
* @typedef Post
* @property {string} id
* @property {string} title
* @property {string} content
*/
/** @type {Post} */
const examplePost = {
id: 'abc',
title: 'abc',
content: 'abc',
}
05. 모든 API 완성하기
if (req.url === '/posts' && req.method === 'GET') {
const result = posts.map(post => ({
id: post.id,
title: post.title,
}))
res.statusCode = 200
res.end(JSON.stringify(result))
if (req.url === '/posts' && req.method === 'GET') {
const result = {
posts: posts.map(post => ({
id: post.id,
title: post.title,
})),
totalCount: posts.length
}
res.statusCode = 200
res.setHeader('Content-Type', 'application/json; encoding=utf-8')
res.end(JSON.stringify(result))
header 의 Content-Type 을 application/json 으로 설정하면 아래와 같이 출력된다.
http POST loc
alhost:4000 title=foo content=bar --print=
hHbB
- h, b: 응답의 header 와 body 를 의미
- H, B: 요청의 header 와 body 를 의미
대문자만 쓰면 보는 것만 받을 수 있다.
} else if (req.url === '/posts' && req.method === 'POST') {
req.on('data', (data) => {
console.log(data)
})
아래 코드를 추가하면
req.setEncoding('utf-8')
} else if (req.url === '/posts' && req.method === 'POST') {
req.setEncoding('utf-8')
req.on('data', (data) => {
const body = JSON.parse(data)
console.log(body)
})
http post localhost:4000/posts title="long title" content=bar --print=HB
- 정규표현식
id: body.title.toLocaleLowerCase().replace(/\s/g, '_'),
- \s: 공백
- g: 모든
06. 가독성과 유지보수성을 살리는 리팩토링 1
라우트 ?
module.exports = {
routes,
}
각 파일은 모듈이다.
내보내는 것이 라우트이다.
main.js 등에서 routes 를 꺼내 쓸 수 있다.
async 함수가 아니면 await 를 해줄 수 없다.
중간 정리를 해보자.
- main.js
// @ts-check
// 프레임워크 없이 간단한 토이프로젝트 웹 서버 만들어보기
/**
* 블로그 포스팅 서비스
* 로컬파일을 데이터베이스로 활용할 예정 (JSON)
* 인증로직은 넣지 않습니다.
* RESTful API 를 사용합니다.
*/
const http = require('http')
const {routes} = require('./api')
const server = http.createServer((req, res) => {
async function main() {
const route = routes.find(
(_route) =>
req.url &&
req.method &&
_route.url.test(req.url) &&
_route.method === req.method
)
if (!route) {
res.statusCode = 404
res.end('Not found.')
return
}
const result = await route.callback()
res.statusCode = result.statusCode
res.end(result.body)
}
main()
})
const PORT = 4000
server.listen(PORT, () => {
console.log(`The server is listening at port: ${PORT}`)
})
- api.js
// @ts-check
/**
* @typedef Post
* @property {string} id
* @property {string} title
* @property {string} content
*/
/** @type {Post[]} */
const posts = [
{
id: "my_first_post",
title: "My first post",
content: "Hello!",
},
{
id: "my_second_post",
title: "나의 두번째 포스트",
content: "Second post!!",
},
]
/**
* POST
*
* GET /posts
* GET /posts/:id
* POST /posts
*/
/**
* @typedef APIResponse
* @property {number} statusCode
* @property {*} body
*/
/**
* @typedef Route
* @property {RegExp} url
* @property {'GET' | 'POST'} method
* @property {() => Promise<APIResponse>} callback
*/
/** @type {Route[]} */
const routes = [
{
url: /^\/posts$/,
method: 'GET',
callback: async () => ({
statusCode: 200,
body: 'All posts',
}),
},
{
url: /^\/posts\/([a-zA-Z0-9-_]+)$/,
method: 'GET',
callback: async () => ({
// TODO: implement
statusCode: 200,
body: {},
}),
},
{
url: /^\/posts$/,
method: 'POST',
callback: async () => ({
// TODO: implement
statusCode: 200,
body: {},
}),
},
]
module.exports = {
routes,
}
body 가 string 일 때는 잘 동작하지만 object 일 때는 다음과 같은 에러를 띄운다.
- 에러
node:internal/errors:477
ErrorCaptureStackTrace(err);
^
TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object
at new NodeError (node:internal/errors:387:5)
at write_ (node:_http_outgoing:802:11)
at ServerResponse.end (node:_http_outgoing:934:5)
at main (/Users/connor/Projects/Node.js-Web-Programming-Package-Online/src/main.js:33:13)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
code: 'ERR_INVALID_ARG_TYPE'
}
Node.js v18.6.0
[nodemon] app crashed - waiting for file changes before starting...
response.end 가 받을 수 있는 인자는 string 이므로 나는 에러이다.
if (typeof result.body === 'string') {
res.end(result.body)
} else {
res.setHeader('Content-Type', 'applcation/json')
res.end(JSON.stringify(result.body))
}
06. 가독성과 유지보수성을 살리는 리팩토링 2
@property {(matches: string[]) => Promise<APIResponse>} callback
string[] 를 받는다.
전부 다 await 으로 위에서 아래로 흘러가는 흐름을 만들고 싶으면 인라인으로 promise 를 만들거나 async function 을 받게 만들어서 호출하면 깔끔하다.
이렇게 처리하는 것도 하나의 방법이다.
에러가 난다면 package.json 에 아래 코드를 추가한다.
"engines": {
"node": "18.6.0"
},
강사님은 14.16.1 버전이다.
es lint 가 object destructing 으로
const {title} = body
로 작성하도록 요청한다.
07. JSON 파일을 데이터베이스로 활용해 마무리하기
database.json
{
"posts": [
{
"id": "first_post",
"title": "나의 첫 포스트",
"content": "안녕하세요!"
},
{
"id": "learn_nodejs",
"title": "노드 배우기",
"content": "JSON 으로 데이털르 관리해 볼까요?"
}
]
}
api.js
// @ts-check
/**
* @typedef Post
* @property {string} id
* @property {string} title
* @property {string} content
*/
/**
* POST
*
* GET /posts
* GET /posts/:id
* POST /posts
*/
/**
* @typedef APIResponse
* @property {number} statusCode
* @property {string | Object} body
*/
/**
* @typedef Route
* @property {RegExp} url
* @property {'GET' | 'POST'} method
* @property {(matches: string[], body: Object.<string, *> | undefined) => Promise<APIResponse>} callback
*/
const fs = require('fs')
const DB_JSON_FILENAME = 'database.json'
/** @returns {Promise<Post[]>} */
async function getPosts() {
const json = await fs.promises.readFile(DB_JSON_FILENAME, 'utf-8')
return JSON.parse(json).posts
}
/**
* @param {Post[]} posts
*/
async function savePosts(posts) {
const content = {
posts,
}
return fs.promises.writeFile(DB_JSON_FILENAME, JSON.stringify(content), 'utf-8')
}
/** @type {Route[]} */
const routes = [
{
url: /^\/posts$/,
method: 'GET',
callback: async (_, body) => ({
statusCode: 200,
body: await getPosts(),
}),
},
{
url: /^\/posts\/([a-zA-Z0-9-_]+)$/,
method: 'GET',
callback: async (matches) => {
const postId = matches[1]
if (!postId) {
return {
statusCode: 404,
body: 'Not found',
}
}
const posts = await getPosts()
const post = posts.find(_post => _post.id === postId)
if (!post) {
return {
statusCode: 404,
body: 'Not found',
}
}
return {
statusCode: 200,
body: post,
}
},
},
{
url: /^\/posts$/,
method: 'POST',
callback: async (_, body) => {
if (!body) {
return {
statusCode: 400,
body: 'Ill-formed request.'
}
}
/** @type {string} */
/* eslint-disable-next-line prefer-destructuring */
const title = body.title
const newPost = {
id: title.replace(/\s/g, '_'),
title,
content: body.content,
}
const posts = await getPosts()
posts.push(newPost)
savePosts(posts)
return {
statusCode: 200,
body: newPost,
}
},
},
]
module.exports = {
routes,
}
글 상세
글 작성
Ch 06. Node.js 핵심 개념 정리
01. require와 모듈, 모듈의 레졸루션
- Opt Cmd < >: 탭 이동
require 이란?
- animal.js
const animals = ['dog', 'cat']
module.exports = animals
- main.js
// @ts-check
// require
console.log(require('./animals'))
공식문서
링크: https://nodejs.org/dist/latest-v16.x/docs/api/
Modules: CommonJSmoodules
https://nodejs.org/dist/latest-v16.x/docs/api/modules.html
require 은 모듈을 가져오는 방식이다.
각 파일은 모듈이다.
module.require() 과 require() 은 동일하다.
// @ts-check
// require
const {path, paths, filename} = module
console.log({
path,
paths,
filename,
})
모듈에는 크게 두 가지 방식이 있다.
노드에서 사용하는 common js 방식과 에크마 스크립트의 방식이다.
CommonJS: require
ECMAScript: export, import
- animal.mjs
const animals = ['dog', 'cat']
export default animals
- main.mjs
import animals from './animals.mjs'
console.log(animals)
- birds.js
module.exports = ['Sparrow', 'Sea Gull']
- main.mjs
import birds from './birds.js'
console.log(birds)
에크마 스크립트는 common js 와 ES 표준대로 import 도 된다.
보통은 common.js 방식으로 해도 무관하다.
const animalsA = require('./animals')
const animalsB = require('./animals')
const animalsC = require('./animals')
console.log(animalsA === animalsB)
console.log(animalsA === animalsC)
true
true
node_modules 에 있는 것은 다음과 같이 절대경로로 가져올 수 있다.
const aniamls = require('animals')
console.log(aniamls)
const { paths } = module
console.log({paths})
{
paths: [
'/Users/connor/Projects/Node.js-Web-Programming-Package-Online/src/node_modules',
'/Users/connor/Projects/Node.js-Web-Programming-Package-Online/node_modules',
'/Users/connor/Projects/node_modules',
'/Users/connor/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
절대경로로 지정했을 때 가져올 수 있는 모듈들의 위치이다.
그래서 위의 경우에 절대경로로 import 해 올 수 있었다.
deep animals.js loaded!
[ 'dog', 'cat' ]
deep 에 노드모듈이 없다면 outer 를 호출한다.
outer animals.js loaded!
[ 'dog', 'cat' ]
절대경로를 지정하면 module.paths 의 경로들을 순서대로 검사하여 해당 모듈이 있으면 가장 첫 번째 것을 가져온다.
02. npm, yarn 등 패키지 매니저와 package.json(1)
npm 은 노드 패키지 매니저이자 레지스트리이다.
npm install decamelize
package.json 은 대략적인 버전이다.
package-lock.json 은 실제로 설치된 버전이 들어간다.
여러 다른 개발자와 협업을 하려면 package-lock.json 을 커밋해야 한다.
const decamelize = require('decamelize')
console.log(decamelize('unicornRainbow'))
- 에러
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/connor/Projects/Node.js-Web-Programming-Package-Online/node_modules/decamelize/index.js
require() of ES modules is not supported.
버전을 6 에서 "version": "5.0.1" 로 수정하니 에러가 해결됐다. 6 버전부터는 import 를 해야 하나보다.
require('decamelize') 와 require('decamelize/index') 가 같다고 보면 된다.
decamelize 를 require 하면 index.js 도 require 한다고 보면 된다.
- package.json
{
"dependencies": {
"decamelize": "^5.0.0"
},
"devDependencies": {
"eslint": "^7.25.0"
}
}
devDependencies 는 개발환경에서만 쓰인다.
- 노드모듈의 용량 구하기
du -h node_modules
package-lock.json
- 필요한 dependency 만 설치
npm install --production
버전을 세 숫자로 나눠적는 것을 시멘틱 버전이라고 한다.
- MAJOR 는 이전 버전과 맞지 않는 (breaking change) 를 만드는 경우이다.
- MINOR 버전은 똑같은 라이브러리를 버전을 올려 쓰더라도 전혀 기존 동작에 영향을 끼치지 않는 경우이다.
- 새로운 기능만 추가된 경우
- PATCH 는 버그픽스를 포함한 경우이다.
npm docs - 시멘틱 버저닝에 대해: https://docs.npmjs.com/about-semantic-versioning
- 삭제
npm uninstall eslint
npm 홈페이지: https://www.npmjs.com/
- 특정 버전의 패키지 설치
npm install decamelize@3.1.0
- 패키지 업데이트
npm update decamelize
강사님은 3.1 에서 3.2 로 버전이 올라가는데 나는 안올라간다.. 왜지..
~3.1.0 으로 되어 있다면 npm docs 에 나온 것 처럼 3.1.1 로 업데이트가 된다.
02. npm, yarn 등 패키지 매니저와 package.json(2)
npm cli 에 대해 알아보자.
npm install --save-dev eslint@7.25.0
CI 에서 미리 검사해서 merge 되는 것을 막을 수 있다.
현재는 실제로 커맨드라인에서 린터가 모든 파일에서 문제가 없는지는 확인하지 않는다.
- eslint 검사
./node_modules/.bin/eslint src/**/*
- 에러
Oops! Something went wrong! :(
ESLint: 7.25.0
ESLint couldn't find the config "airbnb-base" to extend from. Please check that the name of the config is correct.
The config "airbnb-base" was referenced from the config file in "/Users/connor/Projects/Node.js-Web-Programming-Package-Online/.eslintrc.js".
If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.
airbnb-base 를 찾지못해 나오는 에러이다.
- .eslintrc.js
module.exports = {
extends: ['airbnb-base'],
}
npm run 을 할 때는 node_modules/.bin 안을 먼저 검사한다.
- package.json
{
"scripts": {
"lint": "eslint src/**/*"
},
"devDependencies": {
"eslint": "^7.25.0"
}
}
yarn 은 npm 의 대체격이다.
yarn: https://classic.yarnpkg.com/lang/en/
- yarn 설치
npm install -g yarn@1.22.10
-g 는 글로벌이다.
- 설치 확인
which yarn
만약 이미 설치되있다면
cd /opt/homebrew/bin
rm -rf yarn
rm -rf yarnpkg
두 파일을 지우고 다시 설치해보자.
이제 npm 대신 yarn 으로 설치해보자.
node-modules 폴더를 지우고
yarn add decamelize@5.0.0
yarn add -D eslint@7.25.0
-D: 개발버전으로 설치
yarn eslint
yarn 은 스크립트 작성 없이 바로 실행할 수 있다.
- package.json
{
"scripts": {
"lint": "eslint src/**/*"
},
"dependencies": {
"decamelize": "5.0.0"
},
"devDependencies": {
"eslint": "7.25.0"
}
}
yarn lint 명령어로 스크립트를 실행할 수 있다.
03. Node.js 컨벤션
- 파일이름을 짓는 방식
- API 에서 에러가 result 어떻게 처리
- API 가 어떻게 돌아가는지
구글 스타일가이드: https://github.com/google/styleguide
구글 자바스크립트 가이드: https://google.github.io/styleguide/jsguide.html
단순히 이쁘게만 작성하는 게 아니다.
모두 소문자이여야 하고 밑줄이나 대쉬가 있어도 된다.
- module.js
console.log('module.js required!')
module.exports = {}
- main.js
// @ts-check
const {log} = console
console.log(require('./module'))
require 을 사용할 때 확장자 js 를 안붙여도 동작한다.
// @ts-check
const {log} = console
const a = require('./module')
const b = require('./module')
const c = require('./module')
console.log(a === b, b === c)
module.js required!
true true
module.js required! 는 한번만 호출된다.
파일이름은 모두 소문자로 쓰고 밑줄이나 대쉬가 포함될 수 있다.
require 시 대소문자를 구분하기 때문이다.
- main.js - 비동기
// @ts-check
const fs = require('fs')
fs.readFile('src/main.js', 'utf-8', (err, result) => {
if (err) {
console.error(err)
} else {
console.log(result)
}
})
- main.js - 동기
const result = fs.readFileSync('src/main.js', 'utf-8')
console.log(result)
sync 로 실행하면 매우 긴 시간을 기다리게 된다.
서버에는 readFileSync 를 쓰면 안된다.
떨어지는 에러는 동일하다.
동기는 try-catch 로 잡아줘야 한다.
강사님은 노드버전이 14.16.1, 나는 14.20.0 이다.
eslint 가 노드 버전을 인식하지 못하면 package.json 에 아래 코드를 추가한다.
"engines": {
"node": "14.16.1"
},
- main.js
// promise-stype
async function main() {
const result = await fs.promises.readFile(FILENAME, 'utf-8')
console.log(result)
}
main()
callback-style 보다는 promise-style 을 지향하자.
node 가 구버전이라면
const util = require('util')
util.promisify(fs.readFile)(FILENAME, 'utf-8')
async 에서의 에러가 발생하는 경우는 try-catch 로 잡아줘야 한다.
04. Node.js 기본 데이터 구조
버퍼와 스트림에 대해 알아보자.
14.20.0 버전의 문서: https://nodejs.org/docs/latest-v14.x/api/index.html
버퍼는 고정된 길이의 바이트 시퀀스를 나타내는 객체이다.
Uint8Array 자바스크립트의 바이트 array 이다.
- main.js
// @ts-check
const fs = require('fs')
const result = fs.readFileSync('src/test')
console.log(result)
1 byte = 8 bit 는 0 이상 255 이하의 값 0~2^8-1
- console
<Buffer 61 62 63 64 65>
버퍼를 만들어 사용할 수도 있다.
- main.js
// @ts-check
const fs = require('fs')
const bufFromFile = fs.readFileSync('src/test')
const buf = Buffer.from([97, 98, 99, 100, 101])
console.log(buf.compare(bufFromFile))
0
두 버퍼가 같은 것을 볼 수 있다.
// bufs.sort(Buffer.compare)
bufs.sort((a, b) => a.compare(b))
- 버퍼 여부 검사
const fs = require('fs')
const buf = Buffer.from([0, 1, 2, 3])
console.log(Buffer.isBuffer(buf))
BE 와 LE 의 차이는 나중에 알아보자.
const buf = Buffer.from([0, 1, 0, 0])
console.log(buf.readInt32LE())
256 이 나온다.
LE: Little Endian
BE: Big Endian
const buf = Buffer.from([0, 0, 1, 0])
console.log(buf.readInt32BE())
똑같이 256 으로 나온다.
앞을 작은 수로 본다면 LE, 뒤를 작은 수로 보면 BE 이다.
const buf = Buffer.from([0, 0, 0, 1, 0, 0, 0, 0])
console.log(buf.readInt32LE(2))
0, 1, 0, 0 을 읽으므로 256이 된다.
const fs = require('fs')
const buf = Buffer.from([20, 23, 1, 5])
/**
*
* @param {*} array
* @returns {number}
*/
function readInt32LE(array) {
return (
array[0]
+ array[1] * 256
+ array[2] * 256 ** 2
+ array[3] * 256 ** 3
)
}
const offset = 0
const {log} = console
log(`our function: `, readInt32LE(buf))
log(`orig function: `, buf.readInt32LE(0))
our function: 83957524
orig function: 83957524
값이 동일한 것을 볼 수 있다.
스트림
const stream = fs.createReadStream('src/test')
stream.pipe(process.stdout)
abcde
stdout 은 standard out 이다.
데이터를 흘러가며 처리하기 때문에 램 상에 올라오는 게 적다.
대용량 처리에 특화된 노드의 방식
05. Node.js 내장 객체들
Node.js 의 documentation 을 보자.
지금은 v.16.16.0 이다.
__dirname /Users/connor/Projects/Node.js-Web-Programming-Package-Online/src
__filename /Users/connor/Projects/Node.js-Web-Programming-Package-Online/src/main.js
파일마다 각각 다른 경로를 가진다.
둘 다 global 처럼 보이지만 아니다.
process
https://nodejs.org/docs/latest-v14.x/api/process.html#process_process
표준 입출력을 담당하는 스트림이 있다.
process.stdin.setEncoding('utf-8')
process.stdin.on('data', data => {
console.log(data, data.length)
})
이렇게 작성 후 다음 명령어를 호출하면
cat .gitignore | node src/main.js
node_modules 12
스트림이라 파이핑할 수 있다.
setTimeout(() => {
console.log('timeout!')
}, 1000);
- setInterval: 1초 간격으로 실행
- setTimeout: 1초 뒤 실행
let count = 0
const handle = setInterval(() => {
console.log('Interval')
count += 1
if (count === 4) {
console.log('done!')
clearInterval(handle)
}
}, 1000);
Interval
Interval
Interval
Interval
done!
- clearInterval
- clearTimeout
console.log 는 standard out 으로 출력한다.
console.log(process.argv)
CLI 프로그램을 만들 수 있다.
유용한 것이 있다면 가져다 쓰면 된다.
문서를 보며 살펴보면 된다.
06. 스탠다드 라이브러리
노드에서 자주 사용하는 스탠다드 라이브러리를 살펴보자.
core module 이라고도 한다.
기본적으로 탑재되어 있다.
자주 사용하는 것과 쓸모 있는 것 위주로 설명한다.
OS
https://nodejs.org/docs/latest-v14.x/api/os.html
운영체제에 대한 정보를 얻어올 수 있다.
const os = require('os')
console.log(
['arch', os.arch()],
['platform', os.platform()],
['cpus', os.cpus()],
)
OS 모듈은 알고있으면 도움이 될 것 같다.
FS
파일시스템에 접근하는 것은 fs 에 다 있다고 보면 된다.
Child processes
https://nodejs.org/docs/latest-v14.x/api/child_process.html
spawn 으로 ls 를 실행한다.
ls 에 명령줄 인자를 넣어준다.
데이터에 따라 콘솔로그를 출력한다.
노드에서 구현되 있지 않은 서드파티 바이너리 실행할 때 유용하게 사용할 수 있다.
dns
https://nodejs.org/docs/latest-v14.x/api/dns.html
도메인 네임 서버
- 예시 코드
const dns = require('dns');
const options = {
family: 6,
hints: dns.ADDRCONFIG | dns.V4MAPPED,
};
dns.lookup('example.com', options, (err, address, family) =>
console.log('address: %j family: IPv%s', address, family));
// address: "2606:2800:220:1:248:1893:25c8:1946" family: IPv6
// When options.all is true, the result will be an Array.
options.all = true;
dns.lookup('example.com', options, (err, addresses) =>
console.log('addresses: %j', addresses));
// addresses: [{"address":"2606:2800:220:1:248:1893:25c8:1946","family":6}]
const dns = require('dns');
dns.lookup('google.com', (err, address, family) =>
console.log('address: %j family: IPv%s', address, family)
)
path
https://nodejs.org/docs/latest-v14.x/api/path.html
const path = require('path')
const fs = require('fs')
const fileContent = fs.readFileSync('./test.txt')
console.log(fileContent)
const path = require('path')
const fs = require('fs')
const filePath = path.resolve(__dirname, './test.txt')
console.log('filePath', filePath)
const fileContent = fs.readFileSync(filePath, 'utf-8')
console.log(fileContent)
이제 어디서 실행하건 모두 파일을 읽어오는 것을 알 수 있다.
HTTP
http2 도 가능하다.
https 또한 가능하다.
NET
https://nodejs.org/docs/latest-v14.x/api/net.html
TCP 프로토콜과 같은 저수준 통신을 할 수 있다.
바이트 단위로 메시지를 보내고 받을 수 있다.
Ch 07. 스트림
01. stream의 개요, 중요한 이유
노드에서 제일 중요한 데이터 타입 중 하나인 스트림에 대해서 알아보자.
stream 은 스트림 가능한 소스로 부터 데이터를 작은 청크로 쪼개 처리할 수 있게 한다.
큰 데이터를 처리해야 하거나, 비동기적으로만 얻을 수 있는 데이터를 처리해야 할 때 유용하다.
TCP 소켓의 경우 언제 데이터가 올지 알 수가 없고, 데이터 간 간격이 클 수도 있고 언제 끝날 수도 알수가 없다.
비동기적으로 청크단위로 떨어지는게 맞다.
버퍼단위로 뿐만 아니라 스트림으로도 읽을 수 있다.
data, error, end 등의 이벤트 핸들러를 달아 처리합니다.
특별히 지정하지 않으면 data 는 Buffer 가 됩니다.
인코딩을 지정하면 string 이 된다.
02. stream의 종류와 스탠다드 라이브러리 구현체들
stream 의 종류
zlib 은 압축 알고리즘이 적용된 스트림이다.
crypto 는 암호화 알고리즘이 적용되어 있다.
03. stream을 사용해 큰 데이터 처리해보기
const fs = require('fs')
const ws = fs.createWriteStream('local/big-file')
ws.write('Hello, world!')
big-file 이 생성되고 Hello world 가 추가된다.
local 폴더가 있어야 합니다.
const ws = fs.createWriteStream('local/big-file')
const NUM_MBYTES = 500
for (let i = 0; i < NUM_MBYTES; i += 1) {
ws.write('a'.repeat(1024 * 1024))
}
- 용량확인
du -h local
500M 인 것을 알 수 있다.
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/big-file')
rs.on('data', (data) => {
log('Event: data', data[0])
})
rs.on('end', () => {
log('Event: data')
})
- 메모리 확인
top -o PID
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/big-file', {
encoding: 'utf-8',
})
let chunkCount = 0
rs.on('data', (data) => {
chunkCount += 1
log('Event: data', data[0])
})
rs.on('end', () => {
log('Event: data')
log('chunkCount', chunkCount)
})
aaaaaaabbbbbbbbbbbbbbaaaaaaabbbbbbbbbbbb
위와 같은 파일에서, a 의 연속구간 (a block) 의 개수와, b 의 연속구간 (b block) 의 개수를 세는 프로그램.
- write-file-stream.js
const fs = require('fs')
const ws = fs.createWriteStream('local/big-file')
const NUM_BLOCKS = 500
/** @type {Object.<string, number>} */
const numBlocksPerCharacter = {
a: 0,
b: 0,
}
for (let i = 0; i < NUM_BLOCKS; i += 1) {
const blockLength = Math.floor(800 + Math.random() * 200)
const character = i % 2 === 0 ? 'a' : 'b'
ws.write(character.repeat(1024 * (blockLength)))
numBlocksPerCharacter[character] += 1
}
console.log(numBlocksPerCharacter)
- read-file-stream.js
// @ts-check
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/big-file', {
encoding: 'utf-8',
})
/** @type {Object.<string, number} */
const numBlockPerCharacter = {
a: 0,
b: 0,
}
/** @type {string | undefined} */
let prevCharacter
rs.on('data', (data) => {
if (typeof data !== 'string') {
return
}
for (let i = 0; i < data.length; i += 1) {
if (data[i] !== prevCharacter) {
const newCharacter = data[i]
if (!newCharacter) {
continue
}
prevCharacter = newCharacter
numBlockPerCharacter[newCharacter] += 1
}
}
})
rs.on('end', () => {
log('Event: data')
log('blockCount', numBlockPerCharacter)
})
chunkCount 를 추가해보자.
// @ts-check
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/big-file', {
encoding: 'utf-8',
// highWaterMark
})
/** @type {Object.<string, number} */
const numBlockPerCharacter = {
a: 0,
b: 0,
}
/** @type {string | undefined} */
let prevCharacter
let chunkCount = 0
rs.on('data', (data) => {
chunkCount += 1
if (typeof data !== 'string') {
return
}
for (let i = 0; i < data.length; i += 1) {
if (data[i] !== prevCharacter) {
const newCharacter = data[i]
if (!newCharacter) {
continue
}
prevCharacter = newCharacter
numBlockPerCharacter[newCharacter] += 1
}
}
})
rs.on('end', () => {
log('Event: data')
log('blockCount', numBlockPerCharacter)
log('chunkCount', chunkCount)
})
highWaterMark 의 디폴트는 65536 이다.
const rs = fs.createReadStream('local/big-file', {
encoding: 'utf-8',
highWaterMark: 65536 * 2
})
04. stream을 사용할 때와 아닐 때의 퍼포먼스 차이 확인해보기
// @ts-check
const { log } = console
const fs = require('fs')
const data = fs.readFileSync('local/big-file','utf-8')
/** @type {Object.<string, number} */
const numBlockPerCharacter = {
a: 0,
b: 0,
}
/** @type {string | undefined} */
let prevCharacter
for (let i = 0; i < data.length; i += 1) {
if (data[i] !== prevCharacter) {
const newCharacter = data[i]
if (!newCharacter) {
continue
}
prevCharacter = newCharacter
numBlockPerCharacter[newCharacter] += 1
}
}
log('blockCount', numBlockPerCharacter)
stream 이 지고있는 것 같다.
하지만, buffer 는 메모리를 23%, stream 은 0.9% 를 사용한다.
stream 은 청크단위로 처리하기 때문에 큰 파일은 스트림으로 처리하는 것이 메모리 측면에서 훨씬 유리하다.
05. stream의 사용시와 구현시의 주의점들
JSON 스트림 처리기 예시
// @ts-check
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/jsons')
rs.on('data', () => {
log('Event: data')
})
rs.on('end', () => {
log('Event: end')
})
- process-jsons.js
// @ts-check
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/jsons', {
encoding: 'utf-8',
})
let totalSum = 0
rs.on('data', data => {
log('Event: data')
if (typeof data !== 'string') {
return
}
totalSum += data
.split('\n')
.map(jsonLine => {
try {
return JSON.parse(jsonLine)
} catch (error) {
return undefined
}
})
.filter(json => json)
.map((json) => json.data)
.reduce((sum, curr) => sum + curr, 0)
})
rs.on('end', () => {
log('Event: end')
log(`totalSum`, totalSum)
})
- jsons
{"data": 4}
{"data": 63}
{"data": 22}
{"data": }
{"data": 34}
다음 명령어로 계산을 할 수 있다.
node
.exit 명령어로 종료한다.
highWaterMark 를 임의로 지정해보자.
// @ts-check
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/jsons', {
encoding: 'utf-8',
highWaterMark: 6,
})
let totalSum = 0
rs.on('data', chunk => {
log('Event: data', chunk)
if (typeof chunk !== 'string') {
return
}
totalSum += chunk
.split('\n')
.map(jsonLine => {
try {
return JSON.parse(jsonLine)
} catch (error) {
return undefined
}
})
.filter(json => json)
.map((json) => json.data)
.reduce((sum, curr) => sum + curr, 0)
})
rs.on('end', () => {
log('Event: end')
log(`totalSum`, totalSum)
})
// @ts-check
const { log } = console
const fs = require('fs')
const rs = fs.createReadStream('local/jsons', {
encoding: 'utf-8',
highWaterMark: 20,
})
let totalSum = 0
let accumulatedJsonStr = ''
rs.on('data', (chunk) => {
log('Event: data', chunk)
if (typeof chunk !== 'string') {
return
}
accumulatedJsonStr += chunk
const lastNewlineIdx = accumulatedJsonStr.lastIndexOf('\n')
const jsonLinesStr = accumulatedJsonStr.substring(0, lastNewlineIdx)
accumulatedJsonStr = accumulatedJsonStr.substring(lastNewlineIdx)
totalSum += jsonLinesStr
.split('\n')
.map((jsonLine) => {
try {
return JSON.parse(jsonLine)
} catch (error) {
return undefined
}
})
.filter((json) => json)
.map((json) => json.data)
.reduce((sum, curr) => sum + curr, 0)
})
rs.on('end', () => {
log('Event: end')
log(`totalSum`, totalSum)
})
// @ts-check
const { log } = console
const fs = require('fs')
/**
* @param {number} highWaterMark
*/
function processJSON(highWaterMark) {
const rs = fs.createReadStream('local/jsons', {
encoding: 'utf-8',
highWaterMark,
})
let totalSum = 0
let accumulatedJsonStr = ''
rs.on('data', (chunk) => {
if (typeof chunk !== 'string') {
return
}
accumulatedJsonStr += chunk
const lastNewlineIdx = accumulatedJsonStr.lastIndexOf('\n')
const jsonLinesStr = accumulatedJsonStr.substring(0, lastNewlineIdx)
accumulatedJsonStr = accumulatedJsonStr.substring(lastNewlineIdx)
totalSum += jsonLinesStr
.split('\n')
.map((jsonLine) => {
try {
return JSON.parse(jsonLine)
} catch (error) {
return undefined
}
})
.filter((json) => json)
.map((json) => json.data)
.reduce((sum, curr) => sum + curr, 0)
})
rs.on('end', () => {
log('Event: end')
log(`totalSum (highWatermark: ${highWaterMark})`, totalSum)
})
}
for (let watermark = 1; watermark < 50; watermark += 1) {
processJSON(watermark)
}
pipeline 과 promise
// @ts-check
const { log, error } = console
const fs = require('fs')
const stream = require('stream')
const zlib = require('zlib')
stream.pipeline(
fs.createReadStream('local/big-file'),
zlib.createGzip(),
fs.createWriteStream('local/big-file.gz'),
(err) => {
if (err) {
error(`Pipeline failed.`, err)
} else {
log(`Pipeline succeeded.`)
}
}
)
du -h local/*
알집을 풀어보자.
- pipeline.js
// @ts-check
const { log, error } = console
const fs = require('fs')
const stream = require('stream')
const zlib = require('zlib')
stream.pipeline(
fs.createReadStream('local/big-file'),
zlib.createGzip(),
fs.createWriteStream('local/big-file.gz'),
(err) => {
if (err) {
error(`Gunzip failed.`, err)
} else {
log(`Gunzip succeeded.`)
stream.pipeline(
fs.createReadStream('local/big-file.gz'),
zlib.createGunzip(),
fs.createWriteStream('local/big-file.unzipped'),
(_err) => {
if (_err) {
error('Gunzip failed.', _err)
} else {
log(`Gunzip succeeded.`)
}
}
)
}
}
)
promise 로 풀면 더 깔끔하게 코드를 짤 수 있을 것 같다.
gunzip 은 건집이 아니라 g 언집이다.
이제 util.promisify 함수를 활용해보자.
- pipeline.js
// @ts-check
const { log, error } = console
const fs = require('fs')
const stream = require('stream')
const zlib = require('zlib')
const util = require('util')
async function gzip() {
return util.promisify(stream.pipeline) (
fs.createReadStream('local/big-file'),
zlib.createGzip(),
fs.createWriteStream('local/big-file.gz'),
)
}
async function gunzip() {
return util.promisify(stream.pipeline) (
fs.createReadStream('local/big-file.gz'),
zlib.createGunzip(),
fs.createWriteStream('local/big-file.unzipped'),
)
}
async function main() {
await gzip()
await gunzip()
}
main()
스트림은 많이 공부할수록 노드를 공부할 때 좋다.
Ch 08. 리팩토링 프로젝트
01. 안 좋은 코드 예시를 보고 유지보수성 분석하기
모든 왕좌의 게임 가문의 캐릭터들을 놓고 봤을 때 가장 부정적인 가문과 가장 긍정적인 가문을 알아보자.
SENTIM-API
자연어 분석 API 이다.
- before.js
/* eslint-disable */
const https = require('https')
// 모든 가문의 캐릭터들을 놓고 봤을 때 가장 부정적인 가문과 가장 긍정적인 가문을 알아보자.
const resultsByHouseSlugs = {}
https.get(`https://game-of-thrones-quotes.herokuapp.com/v1/houses`, (res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
const houses = JSON.parse(jsonStr)
let numMembersDone = 0
let numTotalMembers = 0
houses.forEach((house) => {
numTotalMembers += house.members.length
})
houses.forEach((house) => {
const houseSlug = house.slug
const members = house.members
members.forEach((member) => {
const characterSlug = member.slug
setTimeout(() => {
https.get(
`https://game-of-thrones-quotes.herokuapp.com/v1/character/${characterSlug}`,
(res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
const json = JSON.parse(jsonStr)
const mergedQuotes = json[0].quotes
.join(' ')
.replace(/[^a-zA-Z0-9., ]/g, '')
const body = JSON.stringify({
text: mergedQuotes,
})
const postReq = https.request(
{
hostname: 'sentim-api.herokuapp.com',
method: 'POST',
path: '/api/v1/',
headers: {
Accept: 'application/json; encoding=utf-8',
'Content-Type': 'application/json; encoding=utf-8',
'Content-Length': body.length,
},
},
(res) => {
let jsonStr = ''
console.log(body, res.statusCode, res.statusMessage)
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
const result = JSON.parse(jsonStr)
resultsByHouseSlugs[houseSlug] =
resultsByHouseSlugs[houseSlug] || []
resultsByHouseSlugs[houseSlug].push({
character: characterSlug,
polarity: result.result.polarity,
})
numMembersDone += 1
if (numMembersDone === numTotalMembers) {
const resultSlugs = Object.keys(resultsByHouseSlugs)
const finalResult = resultSlugs
.map((slug) => {
let sum = 0
resultsByHouseSlugs[slug].forEach(
(value) => (sum += value.polarity)
)
return {
slug,
polarity: sum / resultsByHouseSlugs[slug].length,
}
})
.sort((a, b) => a.polarity - b.polarity)
console.log('sorted', finalResult)
}
})
}
)
postReq.write(body)
})
}
)
}, Math.random() * 10000)
})
})
})
})
- after.js
/**
* @typedef Character
* @property {string} slug
* @property {number} polarity
* @property {house} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]>}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitizeQuote(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return sanitizeQuote(character[0].quotes.join(' '))
}
/**
* @param {string} quote
*/
async function getSentimAPIResult(quote) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
text: quote,
})
const postReq = https.request(
{
hostname: 'sentim-api.herokuapp.com',
method: 'POST',
path: '/api/v1/',
headers: {
Accept: 'application/json; encoding=utf-8',
'Content-Type': 'application/json; encoding=utf-8',
'Content-Length': body.length,
},
},
(res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
resolve(JSON.parse(jsonStr))
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
}
)
postReq.write(body)
})
}
/**
* @param {number[]} numbers
* @returns {number}
*/
function sum(numbers) {
return numbers.reduce((memo, curr) => memo + curr, 0)
}
async function main() {
const houses = await getHouses()
const characters = await Promise.all(
houses
.map((house) =>
house.members.map((member) =>
getMergedQuotesOfCharacter(member.slug).then((quote) => ({
house: house.slug,
charater: member.slug,
quote,
}))
)
)
.flat()
)
console.log('houses:', houses)
const charactersWithPolarity = await Promise.all(
characters.map(async (character) => {
const result = await getSentimAPIResult(character.quote)
return {
...character,
polarity: result.result.polarity,
}
})
)
console.log('charactersWithPolarity:', charactersWithPolarity)
/** @type {Object.<string, Character[]>} */
const charactersByHouseSlugs = {}
charactersWithPolarity.forEach((character) => {
charactersByHouseSlugs[character.house] =
charactersByHouseSlugs[character.house] || []
charactersByHouseSlugs[character.house].push(character)
})
console.log('charactersByHouseSlugs:', charactersByHouseSlugs)
const houseSlugs = Object.keys(charactersByHouseSlugs)
const result = houseSlugs
.map((houseSlug) => {
const charactersOfHouse = charactersByHouseSlugs[houseSlug]
if (!charactersOfHouse) {
return undefined
}
const sumPolarity = sum(
charactersOfHouse.map((character) => character.polarity)
)
const averagePolarity = sumPolarity / charactersOfHouse.length
return [houseSlug, averagePolarity]
})
.sort((a, b) => a[1] - b[1])
console.log('result:', result)
}
main()
02. Promise로 콜백 구조 함수를 async 함수로 바꾸기
비동기 함수이므로 콜백으로 돌려줄 수는 있어도 바로 하우스 배열이 돌아갈 수 없다.
그래서 async function 을 쓰자.
https 는 promises 함수가 없으므로 직접 구현해야 한다.
- after.js
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return new Promise(resolve => {
https.get(`${GOTAPI_PREFIX}/houses`, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
resolve(JSON.parse(jsonStr))
})
})
})
}
async function main() {
const houses = await getHouses()
console.log(houses)
}
main()
03. async, await 등 모던 패턴을 적용해 개선하기(1)
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return new Promise(resolve => {
https.get(`${GOTAPI_PREFIX}/houses`, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
resolve(JSON.parse(jsonStr))
})
})
})
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
return new Promise(resolve => {
https.get(`${GOTAPI_PREFIX}/character/${slug}`, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
const json = JSON.parse(jsonStr)
const mergedQuotes = json[0].quotes
.join(' ')
.replace(/[^a-zA-Z0-9., ]/g, '')
resolve(mergedQuotes)
})
})
})
}
async function main() {
const houses = await getHouses()
houses.forEach(house => {
house.members.forEach(member => {
getMergedQuotesOfCharacter(member.slug)
.then(quotes => console.log(house.slug, member.slug, quotes))
})
})
console.log(houses)
}
main()
async function 은 promise 를 돌려주기 때문에 await 가 return 에서 필요없다.
- after.js
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise(resolve => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
resolve(JSON.parse(jsonStr))
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
// return new Promise(resolve => {
// https.get(`${GOTAPI_PREFIX}/character/${slug}`, res => {
// let jsonStr = ''
// res.setEncoding('utf-8')
// res.on('data', (data) => {
// jsonStr += data
// })
// res.on('end', () => {
// const json = JSON.parse(jsonStr)
// const mergedQuotes = json[0].quotes
// .join(' ')
// .replace(/[^a-zA-Z0-9., ]/g, '')
// resolve(mergedQuotes)
// })
// })
// })
}
async function main() {
const houses = await getHouses()
houses.forEach(house => {
house.members.forEach(member => {
getMergedQuotesOfCharacter(member.slug)
.then(quotes => console.log(house.slug, member.slug, quotes))
})
})
console.log(houses)
}
main()
수정 후에 똑같은 결과를 반환하는 것을 볼 수 있다.
sanitize 는 걸러준다는 의미이다.
03. async, await 등 모던 패턴을 적용해 개선하기(2)
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitize(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
}
async function main() {
const houses = await getHouses()
const results = await Promise.all(
houses
.map(house =>
house.members.map(member => getMergedQuotesOfCharacter(member.slug))
)
.flat()
)
console.log(results)
}
main()
하나의 배열로 async 로 묶어냈다.
하우스와 캐릭터 정보가 있으면 좋을 것 같다.
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitize(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
}
async function main() {
const houses = await getHouses()
const results = await Promise.all(
houses
.map(house =>
house.members.map(member => ({
house: house.slug,
member: member.slug,
quote: getMergedQuotesOfCharacter(member.slug),
}))
)
.flat()
)
console.log(results)
}
main()
promise 를 돌려줘야 하는데 object 를 돌려줘서 그렇다.
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitize(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
}
async function main() {
const houses = await getHouses()
const results = await Promise.all(
houses
.map(house =>
house.members.map(member => getMergedQuotesOfCharacter(member.slug).then(quote => ({
house: house.slug,
character: member.slug,
quote,
})))
)
.flat()
)
console.log(results)
}
main()
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const https = require('https')
const { resolve } = require('path')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitize(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} quote
*/
async function getSentimAPIResult(quote) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
text: quote,
})
const postReq = https.request(
{
hostname: 'sentim-api.herokuapp.com',
method: 'POST',
path: '/api/v1/',
headers: {
Accept: 'application/json; encoding=utf-8',
'Content-Type': 'application/json; encoding=utf-8',
'Content-Length': body.length,
},
}, (res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
resolve(JSON.parse(jsonStr))
} catch {
reject(new Error('The server response was not a valid JSON document.'))
}
})
})
postReq.write(body)
})
}
async function main() {
const houses = await getHouses()
const characters = await Promise.all(
houses
.map(house =>
house.members.map(member =>
getMergedQuotesOfCharacter(member.slug).then(quote => ({
house: house.slug,
character: member.slug,
quote,
}))
)
)
.flat()
)
const charactersWithPolarity = await Promise.all(
characters.map(async (character) => {
const result = await getSentimAPIResult(character.quote)
return ({
...character,
polarity: result.result.polarity,
})
})
)
console.log(charactersWithPolarity)
}
main()
가문별로 리스트에 담아보자.
- after.js
/**
* @typedef Character
* @property {string} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const { captureRejectionSymbol } = require('events')
const https = require('https')
const { resolve } = require('path')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitize(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} quote
*/
async function getSentimAPIResult(quote) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
text: quote,
})
const postReq = https.request(
{
hostname: 'sentim-api.herokuapp.com',
method: 'POST',
path: '/api/v1/',
headers: {
Accept: 'application/json; encoding=utf-8',
'Content-Type': 'application/json; encoding=utf-8',
'Content-Length': body.length,
},
}, (res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
resolve(JSON.parse(jsonStr))
} catch {
reject(new Error('The server response was not a valid JSON document.'))
}
})
})
postReq.write(body)
})
}
async function main() {
const houses = await getHouses()
const characters = await Promise.all(
houses
.map(house =>
house.members.map(member =>
getMergedQuotesOfCharacter(member.slug).then(quote => ({
house: house.slug,
character: member.slug,
quote,
}))
)
)
.flat()
)
const charactersWithPolarity = await Promise.all(
characters.map(async (character) => {
const result = await getSentimAPIResult(character.quote)
return ({
...character,
polarity: result.result.polarity,
})
})
)
// const houseSlug = houses.map(house => house.slug)
const charactersBuHouseSlugs = {}
characters.forEach(character => {
charactersBuHouseSlugs[character.house] =
charactersBuHouseSlugs[character.house] || []
charactersBuHouseSlugs[character.house].push(character)
})
console.log(charactersBuHouseSlugs)
}
main()
/**
* @typedef Character
* @property {string} slug
* @property {number} polarity
* @property {House} slug
*/
/**
* @typedef House
* @property {string} slug
* @property {Character[]} members
*/
const { captureRejectionSymbol } = require('events')
const https = require('https')
const { resolve } = require('path')
const GOTAPI_PREFIX = 'https://game-of-thrones-quotes.herokuapp.com/v1'
/**
*
* @param {string} url
* @returns {*}
*/
async function getHttpsJSON(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
const parsed = JSON.parse(jsonStr)
resolve(parsed)
} catch {
reject(
new Error('The server response was not a valid JSON document.')
)
}
})
})
})
}
/**
* @returns {Promise<House[]}
*/
async function getHouses() {
return getHttpsJSON(`${GOTAPI_PREFIX}/houses`)
}
/**
* @param {string} quote
* @returns {string}
*/
function sanitize(quote) {
return quote.replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} slug
* @returns {Promise<string>}
*/
async function getMergedQuotesOfCharacter(slug) {
const character = await getHttpsJSON(`${GOTAPI_PREFIX}/character/${slug}`)
return character[0].quotes.join(' ').replace(/[^a-zA-Z0-9., ]/g, '')
}
/**
*
* @param {string} quote
*/
async function getSentimAPIResult(quote) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
text: quote,
})
const postReq = https.request(
{
hostname: 'sentim-api.herokuapp.com',
method: 'POST',
path: '/api/v1/',
headers: {
Accept: 'application/json; encoding=utf-8',
'Content-Type': 'application/json; encoding=utf-8',
'Content-Length': body.length,
},
}, (res) => {
let jsonStr = ''
res.setEncoding('utf-8')
res.on('data', (data) => {
jsonStr += data
})
res.on('end', () => {
try {
resolve(JSON.parse(jsonStr))
} catch {
reject(new Error('The server response was not a valid JSON document.'))
}
})
})
postReq.write(body)
})
}
/**
*
* @param {number[]} numbers
* @returns {number}
*/
function sum(numbers) {
return numbers.reduce((memo, curr) => memo + curr, 0)
}
async function main() {
const houses = await getHouses()
const characters = await Promise.all(
houses
.map(house =>
house.members.map(member =>
getMergedQuotesOfCharacter(member.slug).then(quote => ({
house: house.slug,
character: member.slug,
quote,
}))
)
)
.flat()
)
const charactersWithPolarity = await Promise.all(
characters.map(async (character) => {
const result = await getSentimAPIResult(character.quote)
return {
...character,
polarity: result.result.polarity,
}
})
)
/** @type {Object.<string, Character[]>} */
const charactersBuHouseSlugs = {}
charactersWithPolarity.forEach(character => {
charactersBuHouseSlugs[character.house] =
charactersBuHouseSlugs[character.house] || []
charactersBuHouseSlugs[character.house].push(character)
})
const houseSlugs = Object.keys(charactersBuHouseSlugs)
const result = houseSlugs.map(houseSlug => {
const charactersOfHouse = charactersBuHouseSlugs[houseSlug]
if (!charactersOfHouse) {
return undefined
}
const sumPolarity = sum(
charactersOfHouse.map(character => character.polarity)
)
const averagePolarity = sumPolarity / charactersOfHouse.length
return [houseSlug, averagePolarity]
})
console.log(result)
}
main()
Ch 09. Express로 웹 사이트 만들기
01. Express 소개
Express 는 노드에서 제일 유명한 웹 프레임워크이다.
npm install express
환경을 다음과 같이 설정해준다.
- package.json
{
"devDependencies": {
"@types/node": "^15.0.0",
"eslint": "7.25.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"prettier": "^2.2.1",
"typescript": "^4.2.4"
},
"dependencies": {
"express": "^4.17.1"
}
}
express 를 쓰면서 타입 도움을 받기 위해서 라이브러리를 하나 더 설치해보자.
npm install --save-dev @types/express@4.17.11
express 객체를 만든다.
- 이미 사용중인 포트라고 나온다면
events.js:377
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use :::5000
at Server.setupListenHandle [as _listen2] (net.js:1331:16)
at listenInCluster (net.js:1379:12)
at Server.listen (net.js:1465:7)
- 다음과 같은 방법으로 포트를 죽인다.
lsof -i :5000
kill -9 PID번호
하지만 애플유저는 5000번 포트가 죽지 않을 것이다.
찾아보니 AirPlay 가 5000번 포트를 이용한다고 한다.
개발을 위해서는 잠시 죽이자.
02. 미들웨어 개념 이해하고 만들어보기
미들웨어가 어떻게 동작하는 지 잘 이해하는 것이 express 를 잘 활용하는 첫 걸음이다.
미들웨어가 무엇인지 예시를 보면서 이해해보자.
- 노드몬을 설치하자.
"nodemon": "^2.0.7",
- package.json
"scripts": {
"server": "nodemon src/main.js"
},
"engines": {
"node": "14.20.0"
},
- 미들웨어 및 시간표시
// @ts-check
const express = require('express')
const app = express()
const PORT = 5000
app.use('/', (req, res, next) => {
console.log('Middleware 1')
const requestedAt = new Date()
// @ts-ignore
req.requestedAt = requestedAt
next()
})
app.use((req, res) => {
console.log('Middleware 2')
// @ts-ignore
res.send(`Hello, express!: Requested at ${req.requestedAt}`)
res.send('Hello, express!')
})
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
03. REST API 라우팅하기
- 라우터 변경 전
// @ts-check
const express = require('express')
const fs = require('fs')
const userRouter = express.Router()
const app = express()
const PORT = 5000
app.get('/users', (req, res) => {
res.send('User list')
})
app.get('/users/:id', (req, res) => {
res.send('User info with ID')
})
app.post('/users', (req, res) => {
// Register user
})
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
- 라우터 변경 후
// @ts-check
const express = require('express')
const fs = require('fs')
const userRouter = express.Router()
const app = express()
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
userRouter.get('/:id', (req, res) => {
res.send('User info with ID')
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
app.use('/users', userRouter)
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
// @ts-check
const express = require('express')
const fs = require('fs')
const userRouter = express.Router()
const app = express()
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
userRouter.param('id', (req, res, next, value) => {
console.log(value)
next()
})
userRouter.get('/:id', (req, res) => {
res.send('User info with ID')
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
app.use('/users', userRouter)
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
userRouter.param('id', (req, res, next, value) => {
console.log(`id parameter`, value)
next()
})
userRouter.get('/:id', (req, res) => {
console.log('userRouter get ID')
res.send('User info with ID')
})
// @ts-check
const express = require('express')
const fs = require('fs')
const userRouter = express.Router()
const app = express()
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
const USERS = {
15: {
nickname: 'foo',
}
}
userRouter.param('id', (req, res, next, value) => {
console.log(`id parameter`, value)
// @ts-ignore
req.user = USERS[value]
next()
})
userRouter.get('/:id', (req, res) => {
console.log('userRouter get ID')
// @ts-ignore
res.send(req.user)
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
userRouter.post('/:id/nickname', (req, res) => {
})
app.use('/users', userRouter)
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
// @ts-check
const express = require('express')
const fs = require('fs')
const userRouter = express.Router()
const app = express()
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
const USERS = {
15: {
nickname: 'foo',
}
}
userRouter.param('id', (req, res, next, value) => {
console.log(`id parameter`, value)
// @ts-ignore
req.user = USERS[value]
next()
})
userRouter.get('/:id', (req, res) => {
console.log('userRouter get ID')
// @ts-ignore
res.send(req.user)
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
userRouter.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
app.use('/users', userRouter)
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
body parser 를 설치해보자.
npm install body-parser@1.19.0
// @ts-check
const express = require('express')
const bodyParser = require('body-parser')
const userRouter = express.Router()
const app = express()
app.use(bodyParser.json())
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
const USERS = {
15: {
nickname: 'foo',
}
}
userRouter.param('id', (req, res, next, value) => {
console.log(`id parameter`, value)
// @ts-ignore
req.user = USERS[value]
next()
})
userRouter.get('/:id', (req, res) => {
console.log('userRouter get ID')
// @ts-ignore
res.send(req.user)
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
userRouter.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
app.use('/users', userRouter)
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
express 4.16.0 이후의 버전에서는 bodyParser 가 deprecated 인 모양이다.
app.use(express.json())
bodyParser 없이 express.json() 만으로도 json response 를 받을 수 있다.
04. Pug로 템플릿 그려보기
이번에는 템플릿엔진을 사용해서 웹사이트처럼 만들어볼 것이다.
PUG, EJS 등 템플릿이 있다.
syntax 를 새로 배워야 한다.
템플릿 엔진은 퍼그언어로 쓰여진 것을 html 언어로 바꿔준다.
- 퍼그 설치
npm install pug@3.0.2
app.set('view engine', 'pug')
app.get('/', (req, res) => {
res.render('index')
})
두 줄의 코드를 추가한다.
- view/index.pug
html
head
body
div Hello, Pug!
- 전체코드
// @ts-check
const express = require('express')
const userRouter = express.Router()
const app = express()
app.use(express.json())
app.set('view engine', 'pug')
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
const USERS = {
15: {
nickname: 'foo',
}
}
userRouter.param('id', (req, res, next, value) => {
console.log(`id parameter`, value)
// @ts-ignore
req.user = USERS[value]
next()
})
userRouter.get('/:id', (req, res) => {
console.log('userRouter get ID')
// @ts-ignore
res.send(req.user)
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
userRouter.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
app.use('/users', userRouter)
app.get('/', (req, res) => {
res.render('index')
})
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
변수도 넣을 수 있다.
app.get('/', (req, res) => {
res.render('index', {
message: 'Hello, pug!!!',
})
})
html
head
body
div=message
지금은 views 폴더를 src 밖에 두고있지만 강사님은 src 폴더 내에 두는 것을 선호한다.
app.set('views', 'src/views')
이 코드를 추가하면 설정을 바꿀 수 있다.
요청자가 자기가 받기를 원하는 타입을 accept 에 담는다.
req.headers.accept
// /users/15
userRouter.get('/:id', (req, res) => {
const resMimeType = req.accepts(['json', 'html'])
if (resMimeType === 'json') {
// @ts-ignore
res.send(req.user)
} else if (resMimeType === 'html') {
res.render('user-profile')
}
})
http 호출 시 accept 로 소문자로 작성해도 된다.
헤더를 꼭 --print 옵션 앞에 적어야 한다.
- user-profile.pug
html
head
body
h1 User profile page
h2 Nickname
div= nickname
html
head
body
h1 User profile page
h2 Nickname
div= nickname
05. 스태틱 파일 서빙
스태틱 파일 서빙을 하는 방법은 간단하다.
- src/main.js
app.use(express.static('src/public'))
- user-profile.pug
html
head
link(rel="stylesheet" href="/index.css")
body
h1 User profile page
h2 Nickname
div= nickname
- src/public/index.css
body {
background-color: gray;
}
- user-profile.pug
html
head
link(rel="stylesheet" href="/index.css")
body
h1 User profile page
h2.gold Nickname
div.green.big= nickname
- index.css
body {
background-color: gray;
}
.gold {
color: gold;
}
.green {
color: green;
}
.big {
font-size: 24px2;
}
.gold 는 클래스 gold 를 의미한다.
app.use(express.static('src/public'))
이 코드를 아래로 내리면 다운로드가 되지 않는다.
- 전체코드
// @ts-check
const express = require('express')
const userRouter = express.Router()
const app = express()
app.use(express.json())
app.set('views', 'src/views')
app.set('view engine', 'pug')
const PORT = 5000
userRouter.get('/', (req, res) => {
res.send('User list')
})
const USERS = {
15: {
nickname: 'foo',
},
16: {
nickname: 'bar',
},
}
userRouter.param('id', (req, res, next, value) => {
console.log(`id parameter`, value)
// @ts-ignore
req.user = USERS[value]
next()
})
// /users/15
userRouter.get('/:id', (req, res) => {
const resMimeType = req.accepts(['json', 'html'])
if (resMimeType === 'json') {
// @ts-ignore
res.send(req.user)
} else if (resMimeType === 'html') {
res.render('user-profile', {
// @ts-ignore
nickname: req.user.nickname,
})
}
})
userRouter.post('/', (req, res) => {
// Register user
res.send('User registered.')
})
userRouter.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
app.use('/users', userRouter)
app.use(express.static('src/public'))
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
이렇게 하면 보안 상 문제가 생길 수 있다.
다음과 같이 바꿔보자.
- main.js
app.use('/public', express.static('src/public'))
- user-profile.pug
html
head
link(rel="stylesheet" href="/public/index.css")
body
h1 User profile page
h2.gold Nickname
div.green.big= nickname
static 미들웨어가 동작 한 것을 볼 수 있다.
06. 에러 핸들링
없는 유저에 접근할 때 에러를 핸들링해보자.
router.param('id', (req, res, next, value) => {
// @ts-ignore
const user = USERS[value]
if (!user) {
throw new Error('User not found.')
}
// @ts-ignore
req.user = user
next()
})
인자를 네 개를 받으면 에러 핸들링 미들웨어라고 인식한다.
app.use((err, req, res, next) => {
res.send((err.message))
})
시멘틱에는 잘 맞지 않다.
- main.js
app.use((err, req, res, next) => {
res.statusCode = err.statusCode || 500
res.send((err.message))
})
- user.js
router.param('id', (req, res, next, value) => {
// @ts-ignore
const user = USERS[value]
if (!user) {
const err = new Error('User not found.')
err.statusCode = 404
throw err
}
// @ts-ignore
req.user = user
next()
})
router.param 을 async 함수로 바꾼다면
async function 은 무조건 try catch 로 감싸줘야 한다.
router.param('id', async (req, res, next, value) => {
try {
// @ts-ignore
const user = USERS[value]
if (!user) {
const err = new Error('User not found.')
err.statusCode = 404
throw err
}
// @ts-ignore
req.user = user
next()
} catch (err) {
next(err)
}
})
next 에 err 를 담으면 에러이다.
async function 으로 미들웨어를 작성할 때에는 이 점을 주의해야 한다.
07. Jest를 활용한 API 테스팅
자바스크립트 테스트 프레임워크 Jest: https://jestjs.io/
supertest: https://github.com/visionmedia/supertest
npm install --save-dev jest@26.6.3 @types/jest@26.0.23 supertest@6.1.3 @types/supertest@2.0.11
앱 동작의 규격이다.
테스트의 다른 말로 스펙으로 쓴다.
- app.spec.js
test('our first test', () => {
expect(1 + 2).toBe(3)
})
"scripts": {
"test": "jest",
"server": "nodemon src/main.js"
},
test('our first test', () => {
expect(1 + 3).toBe(3)
})
jest 와 supertest 를 연동해보자.
/* eslint-disable no-undef */
/* eslint-disable node/no-unpublished-require */
const supertest = require('supertest')
const app = require('./app')
const request = supertest(app)
test('our first test', async () => {
const result = await await request.get('/users/15').accept('application/json')
console.log(result)
})
포트가 열려있어 테스트가 끝나지 않는다.
- app.js
// @ts-check
const express = require('express')
const app = express()
app.use(express.json())
app.set('views', 'src/views')
app.set('view engine', 'pug')
const userRouter = require('./routers/user')
app.use('/users', userRouter)
app.use('/public', express.static('src/public'))
app.use((err, req, res, next) => {
res.statusCode = err.statusCode || 500
res.send((err.message))
})
module.exports = app
- main.js
// @ts-check
const app = require('./app')
const PORT = 5000
app.listen(PORT, () => {
console.log(`The Express server is listening at port: ${PORT}`)
})
nickname 이 문자열인지 테스트 해보자.
test('our first test', async () => {
const result = await await request.get('/users/15').accept('application/json')
console.log(result.body)
expect(result.body).toMatchObject({
nickname: expect.any(String),
})
})
test('retrieve user page', async () => {
const result = await await request.get('/users/15').accept('text/html')
console.log(result.text)
})
test('retrieve user page', async () => {
const result = await await request.get('/users/15').accept('text/html')
expect(result.text).toMatch(/^<html>.*<\/html>$/)
})
만약 html 에 다른 것을 입력한다면
test('update nickname', async () => {
const newNickname = 'newNickname'
const res = await request
.post('/users/15/nickname')
.send({ nickname : newNickname})
expect(res.status).toBe(200)
const userResult = await request.get('/users/15').accept('application/json')
expect(userResult.status).toBe(200)
expect(userResult.body).toMatchObject({
nickname: newNickname,
})
})
- 전체코드
/* eslint-disable no-undef */
/* eslint-disable node/no-unpublished-require */
const supertest = require('supertest')
const app = require('./app')
const request = supertest(app)
test('retrieve user json', async () => {
const result = await request.get('/users/15').accept('application/json')
expect(result.body).toMatchObject({
nickname: expect.any(String),
})
})
test('retrieve user page', async () => {
const result = await request.get('/users/15').accept('text/html')
expect(result.text).toMatch(/^<html>.*<\/html>$/)
})
test('update nickname', async () => {
const newNickname = 'newNickname'
const res = await request
.post('/users/15/nickname')
.send({ nickname : newNickname})
expect(res.status).toBe(200)
const userResult = await request.get('/users/15').accept('application/json')
expect(userResult.status).toBe(200)
expect(userResult.body).toMatchObject({
nickname: newNickname,
})
})
08. 이미지 업로드 핸들링해보기
- user-profile.pug
html
head
link(rel="stylesheet" href="/public/index.css")
body
h1 User profile page
h2.gold Nickname
div.green.big= nickname
h2 Profile
form(action=`/user/${userId}/profile` method="post" enctype="multipart/form-data")
input(type="file" name="profile")
button Upload Profile Picture
이미지는 보통 이렇게 보낸다.
멀터라는 이미지 핸들링 라이브러리가 있다.
npm install multer@1.4.2
// @ts-check
const express = require('express')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
const router = express.Router()
const USERS = {
15: {
nickname: 'foo',
profileImage: undefined,
},
16: {
nickname: 'bar',
profileImage: undefined,
},
}
router.get('/', (req, res) => {
res.send('User list')
})
router.param('id', async (req, res, next, value) => {
try {
// @ts-ignore
const user = USERS[value]
if (!user) {
const err = new Error('User not found.')
err.statusCode = 404
throw err
}
// @ts-ignore
req.user = user
next()
} catch (err) {
next(err)
}
})
// /users/15
router.get('/:id', (req, res) => {
const resMimeType = req.accepts(['json', 'html'])
if (resMimeType === 'json') {
// @ts-ignore
res.send(req.user)
} else if (resMimeType === 'html') {
res.render('user-profile', {
// @ts-ignore
nickname: req.user.nickname,
userId: req.params.id,
})
}
})
router.post('/', (req, res) => {
res.send('User registered.')
})
router.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
router.post('/:id/profile', upload.single('profile'), (req, res, next) => {
// const { user } = req
// user.profileImage
res.send('User profile image uploaded.')
})
module.exports = router
upload.single('profile')
로 미들웨어를 추가해준다.
single 은 파일 하나만 업로드하겠다는 의미이다.
profile 은 html 태그에서 필드명이다.
router.post('/:id/profile', upload.single('profile'), (req, res, next) => {
console.log(req.file)
// const { user } = req
// user.profileImage
res.send('User profile image uploaded.')
})
파일명만 잘 저장해두었다가 static 미들웨어만 잘 내려주면 될 것 같다.
- user.js
// @ts-check
const express = require('express')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
const router = express.Router()
const USERS = {
15: {
nickname: 'foo',
profileImage: undefined,
},
16: {
nickname: 'bar',
profileImage: undefined,
},
}
router.get('/', (req, res) => {
res.send('User list')
})
router.param('id', async (req, res, next, value) => {
try {
// @ts-ignore
const user = USERS[value]
if (!user) {
const err = new Error('User not found.')
err.statusCode = 404
throw err
}
// @ts-ignore
req.user = user
next()
} catch (err) {
next(err)
}
})
// /users/15
router.get('/:id', (req, res) => {
const resMimeType = req.accepts(['json', 'html'])
if (resMimeType === 'json') {
// @ts-ignore
res.send(req.user)
} else if (resMimeType === 'html') {
res.render('user-profile', {
// @ts-ignore
nickname: req.user.nickname,
userId: req.params.id,
profileImageURL: '/uploads/ed0dcb88284de748529cd638e9193bc4',
})
}
})
router.post('/', (req, res) => {
res.send('User registered.')
})
router.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
router.post('/:id/profile', upload.single('profile'), (req, res, next) => {
console.log(req.file)
const { user } = req
const {filename} = req.file
user.profileImage = filename
res.send(`User profile image uploaded: ${filename}`)
})
module.exports = router
- app.js
// @ts-check
const express = require('express')
const app = express()
app.use(express.json())
app.set('views', 'src/views')
app.set('view engine', 'pug')
const userRouter = require('./routers/user')
app.use('/users', userRouter)
app.use('/public', express.static('src/public'))
app.use('/uploads', express.static('uploads'))
app.use((err, req, res, next) => {
res.statusCode = err.statusCode || 500
res.send((err.message))
})
module.exports = app
- user-profile.pug
html
head
link(rel="stylesheet" href="/public/index.css")
body
h1 User profile page
h2 Nickname
div= nickname
h2 Profile
img(src=profileImageURL)
form(action=`/users/${userId}/profile` method="post" enctype="multipart/form-data")
input(type="file" name="profile")
button Upload Profile Picture
- user-profile.pug
img(src=profileImageURL).profileImage
- index.css
.profileImage {
width: 300px;
}
- user.js
// @ts-check
const express = require('express')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
const router = express.Router()
const USERS = {
15: {
nickname: 'foo',
profileImageKey: undefined,
},
16: {
nickname: 'bar',
profileImageKey: undefined,
},
}
router.get('/', (req, res) => {
res.send('User list')
})
router.param('id', async (req, res, next, value) => {
try {
// @ts-ignore
const user = USERS[value]
if (!user) {
const err = new Error('User not found.')
err.statusCode = 404
throw err
}
// @ts-ignore
req.user = user
next()
} catch (err) {
next(err)
}
})
// /users/15
router.get('/:id', (req, res) => {
const resMimeType = req.accepts(['json', 'html'])
if (resMimeType === 'json') {
// @ts-ignore
res.send(req.user)
} else if (resMimeType === 'html') {
res.render('user-profile', {
// @ts-ignore
nickname: req.user.nickname,
userId: req.params.id,
// profileImageURL: '/uploads/ed0dcb88284de748529cd638e9193bc4',
profileImageURL: `/uploads/${req.user.profileImageKey}`,
})
}
})
router.post('/', (req, res) => {
res.send('User registered.')
})
router.post('/:id/nickname', (req, res) => {
// req.body: {"nickname": "bar"}
// @ts-ignore
const { user } = req
const { nickname } = req.body
user.nickname = nickname
res.send(`User nickname updated: ${nickname}`)
})
router.post('/:id/profile', upload.single('profile'), (req, res, next) => {
console.log(req.file)
const { user } = req
const {filename} = req.file
user.profileImageKey = filename
res.send(`User profile image uploaded: ${filename}`)
})
module.exports = router
express 는 더 많은 기능들이 있으므로 레퍼런스를 더 보면 된다.
'Backend > 노트' 카테고리의 다른 글
한 번에 끝내는 Node.js 웹 프로그래밍 초격차 패키지 Online - 2 (1) | 2022.09.20 |
---|---|
Tucker 의 Go 언어 프로그래밍 (0) | 2022.08.14 |
🍀스프링부트 (0) | 2022.03.29 |
DB 연결 (0) | 2022.03.19 |
따라하며 배우는 도커와 CI환경 (0) | 2022.03.11 |