2022.09.07 경일 메타버스 23주차 3일 수업내용. Node.js - 자바스크립트 ES2015+, 노드 기능
p. 75 ~ 78
ES2015부터 자바스크립트와 노드의 API들이 콜백 대신 프로미스 기반으로 재구성됨
콜백 지옥 (Callback Hell) 현상 극복
콜백 지옥 :
콜백 지옥은 JavaScript를 이용한 비동기 프로그래밍시 발생하는 문제로서, 함수의 매개 변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상을 말한다.
예시 코드 1
const condition = true; // true면 resolve, false면 reject
const promise = new Promise((resolve, reject) => {
if (condition) {
resolve('성공');
} else {
reject('실패');
}
});
// 다른 코드가 들어갈 수 있음
promise
.then((message) => {
console.log(message); // 성공(resolve)한 경우 실행
})
.catch((error) => {
console.error(error); // 실패(reject)한 경우 실행
})
.finally(() => { // 끝나고 무조건 실행
console.log('무조건');
});
new Promise로 프로미스를 생성할 수 있다.
프로미스의 내부에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣는다.
프로미스인 객체 promise에 then과 catch 메소드를 붙일 수 있다.
프로미스 내부에서 resolve가 호출되면 then이 실행되고, reject가 호출되면 catch가 실행된다.
finally 부분은 성공 / 실패 여부와 상관없이 실행된다.
프로미스를 간단히 설명하자면, 실행은 바로 하되 결괏값은 나중에 받는 객체
결괏값은 실행이 완료된 후 then이나 catch 메소드를 통해 받는다.
위 예제에서 new Promise는 바로 실행 → 결괏값은 then을 붙였을 때 받는다
⇒ 비동기
then이나 catch에 다시 다른 then이나 catch를 붙일 수 있다.
이전 then의 return 값은 다음 then의 매개변수로 넘긴다.
예시 코드 2
const condition = true; // true면 resolve, false면 reject
const promise = new Promise((resolve, reject) => {
if (condition) {
resolve('성공');
} else {
reject('실패');
}
});
// then이나 catch에서 다시 다른 then이나 catch를 붙일 수 있다.
promise
.then((message) => {
return new Promise((resolve, reject) => {
resolve(message); // 다음 then의 매개변수로 리턴 값을 넘긴다.
});
})
.then((message2) => {
console.log(message2);
return new Promise((resolve, reject) => {
resolve(message2); // 다음 then의 매개변수로 리턴 값을 넘긴다.
});
})
.then((message3) => {
console.log(message3); // 성공(resolve)한 경우 실행
})
.catch((error) => {
console.error(error); // 3개의 promise 어디서든 실패(reject)하면 실행
});
then에서 new Promise를 return해야 다음 then에서 받을 수 있다.
이를 활용해 콜백을 프로미스로 바꿀 수 있다.
예시 코드 3 - 1
// 콜백 패턴
function findAndSaveUser(Users) {
Users.findOne({}, (err, user) => { // 첫 번째 콜백
if (err) {
return console.error(err);
}
user.name = 'zero';
user.save((err) => { // 두 번째 콜백
if (err) {
return console.error(err);
}
Users.findOne({ gender: 'm' }, (err, user) => { // 세 번째 콜백
// 생략
});
});
});
}
예시 코드의 위는 콜백 함수가 세 번 중첩되어 있다.
콜백 함수가 나올 때마다 코드의 깊이가 깊어진다.
각 콜백 함수마다 에러를 따로 처리해야 한다.
이를 아래 코드처럼 프로미스로 바꾸었다.
예시 코드 3 - 2
// 프로미스 사용
function findAndSaveUser(Users) {
Users.findOne({})
.then((user) => {
user.name = 'zero';
return user.save();
})
.then((user) => {
return Users.findOne({ gender: 'm' });
})
.then((user) => {
// 생략
})
.catch(err => {
console.error(err);
});
}
코드의 깊이가 깊어지지 않는다.
마지막 catch에서 에러를 한 번에 처리한다.
프로미스의 사용은 new Promise가 함수 내부에 구현되어 있어야 한다.
프로미스 여러 개를 한 번에 실행할 수 있다.
예시 코드 4
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
Promise.all([promise1, promise2])
.then((result) => {
console.log(result); // ['성공1', '성공2'];
})
.catch((error) => {
console.error(error);
});
Promise.resolve : 즉시 resolve하는 프로미스를 생성
프로미스가 여러 개 있을 때 Promise.all에 넣으면 모두 resolve 될 때까지 기다렸다가 then으로 넘어간다.
result 매개변수에 각각의 프로미스 결괏값이 배열로 들어간다.
하나라도 reject 되면 catch로 넘어간다.
p. 78 ~ 81
노드 7.6 버전, ES2017부터 추가되고 지원되는 기능
비동기 위주 프로그래밍에서 도움이 많이 된다.
프로미스를 사용한 코드를 한 번 더 줄일 수 있다.
예시 코드 5 - 1
// 예시 코드 3 - 2
// 프로미스 사용
function findAndSaveUser(Users) {
Users.findOne({})
.then((user) => {
user.name = 'zero';
return user.save();
})
.then((user) => {
return Users.findOne({ gender: 'm' });
})
.then((user) => {
// 생략
})
.catch(err => {
console.error(err);
});
}
async/await 문법을 사용해 더 줄일 수 있다.
예시 코드 5 - 2
async function findAndSaveUser(Users) {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await Users.findOne({ gender: 'm' });
// 생략
}
함수 선언부를 async function으로 교체
프로미스 앞에 await
함수는 해당 프로미스가 resolve될 때까지 기다린 후 다음 로직으로 넘어간다.
ex. await Users.findOne({})이 resolve될 때까지 기다린 후 user 변수를 초기화
위 예시에는 에러를 처리하는 부분이 없다. (프로미스가 reject된 경우)
예시 코드 5 - 3
async function findAndSaveUser(Users) {
try {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await Users.findOne({ gender: 'm' });
// 생략
} catch (error) {
console.error(error);
}
}
try/catch 문으로 로직을 감싸,
프로미스의 catch 메소드처럼 try/catch 문의 catch가 에러를 처리
화살표 함수도 async와 같이 사용할 수 있다.
예시 코드 5 - 4
const findAndSaveUser = async (Users) => {
try {
let user = await Users.findOne({});
user.name = 'zero';
user = await user.save();
user = await Users.findOne({ gender: 'm' });
// 생략
} catch (error) {
console.error(error);
}
};
프로미스를 순차적으로 실행하는 방법
노드 10부터 지원하는 ES2018 문법
for문과 async/await을 사용
for await of 문을 사용해 프로미스 배열을 순회한다.
예시 코드 5 - 5
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
(async () => {
for await (promise of [promise1, promise2]) {
console.log(promise);
}
})();
async 함수의 반환값은 항상 Promise로 감싸진다.
new Promise로 then 메소드의 반환값을 프로미스로 만들듯이
반환값이 항상 프로미스 객체라는 뜻
따라서 실행 후 then을 붙이거나 또 다른 async 함수 안에서 await을 붙여 처리할 수 있다.
예시 코드 5 - 6
async function findAndSaveUser(Users) {
// 생략
}
findAndSaveUser().then(() => { /* 생략 */ });
// 또는
async function other() {
const result = await findAndSaveUser();
}
p. 90 ~ 91
자바스크립트는 스크립트 언어이다.
⇒ 미리 컴파일을 하지 않아도 즉석에 코드 실행 가능
REPL (Rad Eval Print Loop) :
노드 제공 콘솔.
입력한 코드를 읽고 (Read), 해석하고 (Eval), 결과물을 반환하고 (Print), 종료할 때까지 반복한다 (Loop)
p. 92 ~ 96
노드는 코드를 모듈로 만들 수 있다.
모듈 :
특정한 기능을 하는 함수나 변수들의 집합
하나의 프로그램이면서 다른 프로그램의 부품
재사용할 수 있다.
모듈 만들기
module.exports에 변수들을 담은 객체를 대입하여 파일을 모듈로 만든다.
require 함수 안에 불러올 모듈의 경로를 적어 참조한다.
예시 코드 6
// var.js
const odd = '홀수입니다';
const even = '짝수입니다';
module.exports = {
odd,
even,
};
// func.js
const { odd, even } = require('./3_3_var');
function checkOddOrEven(num) {
if (num % 2) { // 홀수면
return odd;
}
return even;
}
module.exports = checkOddOrEven;
**// index.js
const { odd, even } = require('./3_3_var');
const checkNumber = require('./3_3_func');
function checkStringOddOrEven(str) {
if (str.length % 2) { // 홀수면
return odd;
}
return even;
}
console.log(checkNumber(10));
console.log(checkStringOddOrEven('hello'));
모듈의 장단점
장점 :
여러 파일에 걸쳐 재사용되는 함수나 변수를 모듈로 만들어두면 편리하다.
단점 :
모듈이 많아지고 모듈 간의 관계가 얽히게 되면 구조를 파악하기 어렵다.
p. 96 ~ 98
전역 객체
모든 파일에서 접근 가능
생략 가능
파일 간에 간단한 데이터를 공유할 때 사용하기도 한다.
남용에 주의
p. 98 ~ 100
로깅 함수
console.time(레이블) :
console.timeEnd(레이블)과 대응되어 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정
console.log(내용) :
로그를 콘솔에 표시.
console.log(내용, 내용, 내용, …)처럼 사용해 여러 내용을 동시에 표시할 수도 있다.
console.error(에러 내용) :
에러를 콘솔에 표시.
console.table(배열) :
배열의 요소로 객체 리터럴을 넣으면 객체의 속성들을 테이블 형식으로 표시.
console.dir(객체, 옵션 객체) :
객체를 콘솔에 표시.
옵션의 colors는 콘솔에 색을 추가할지 결정.
옵션의 depth는 객체 안의 객체를 몇 단계까지 보일지를 결정. 기본값은 2.
console.trace(레이블) :
에러가 어디서 발생했는지 추적.
에러의 위치가 에러와 함께 나오지 않는다면 사용
p. 101 ~ 103
노드에서 타이머 기능을 제공하는 함수는 global 객체 안에 들어있다.
타이머 기능 함수
setTimeout(콜백 함수, 밀리초) :
주어진 밀리초 이후에 콜백 함수를 실행
setInterval(콜백 함수, 밀리초) :
주어진 밀리초 마다 콜백 함수를 반복 실행
setImmediate(콜백 함수) :
콜백 함수를 즉시 실행
clearTimeout(아이디) :
setTimeout 취소
clearInterval(아이디) :
setInterval 취소
clearImmediate(아이디) :
setImmediate 취소
예시 코드 7
const timeout = setTimeout(() => {
console.log('1.5초 후 실행');
}, 1500);
const interval = setInterval(() => {
console.log('1초마다 실행');
}, 1000);
const timeout2 = setTimeout(() => {
console.log('실행되지 않습니다');
}, 3000);
setTimeout(() => {
clearTimeout(timeout2);
clearInterval(interval);
}, 2500);
const immediate = setImmediate(() => {
console.log('즉시 실행');
});
const immediate2 = setImmediate(() => {
console.log('실행되지 않습니다');
});
clearImmediate(immediate2);
결과 :
즉시 실행
1초마다 실행
1.5초 후 실행
1초마다 실행
setImmediate(Callback)과 setTimeout(Callback, 0)에 담긴 콜백 함수는 이벤트 루프를 거친 뒤 즉시 실행된다.
I/O 작업의 콜백 함수 안에서 타이머를 호출하는 경우,
setImmediate가 setTimeout(콜백, 0)보다 먼저 실행된다.
p. 103
filename, dirname 키워드는 경로 정보를 제공한다.
경로 구분자 문제 등을 해결하기 위해 보통 path 모듈과 함께 쓴다.
p. 103 ~ 108
모듈을 만들 때, module.exports 말고 exports 객체도 사용 가능하다.
각각의 변수를 exports 객체에 하나씩 넣을 수 있다.
module.exports와 exports가 같은 객체를 참조하기 때문
노드에서의 this
최상위 스코프의 this ⇒ module.exports (또는 exports)
함수 선언문 내부의 this ⇒ global 객체
자바스크립트에서 함수는 객체
require의 속성
require.cache :
파일 이름이 속성명으로 들어있다.
속성값으로는 각 파일의 모듈 객체
require.main :
노드 실행 시 첫 모듈
노드를 실행한 파일
require.main 객체의 모양은 require.cache의 모듈 객체와 같다.
현재 파일이 첫 모듈인지 알아보는 방법
require.main === module
// 첫 모듈이면 true
두 모듈이 서로 참조(require)하면 발생
순환 참조되는 대상을 빈 객체(null)로 만든다.