JS/Node – 비동기
비동기 쉽게 이해하기
햄버거 주문을 예로 들어 생각해보자
햄버거 가게에서 손님들이 햄버거를 주문하려고 왔다. 그런데 먼저 주문한 A가 주문한 햄버거를 받을 때까지 그 뒤에 있는 B가 햄버거를 주문조차 할 수 없다고 생각해보자
→ “blocking”(하나의 작업이 끝날 때까지 이어지는 작업을 막는 것)
손님 B는 손님 A가 주문한 햄버거가 나오고 나서야 원하는 햄버거를 주문할 수 있다. 즉 손님 A의 햄버거 주문 완료 시점과 손님 B의 햄버거 주문 시작 지점이 같다
→ “동기적(synchronous)” 이다(완료 시점과 시작 시점이 같은 상황)
효율적인 햄버거 가게 운영을 위해 커피 주문 과정을 다음과 같이 변경해 보자
* 햄버거 주문이 blocking 되지 않고, 언제든지 주문을 받을 수 있다
* 햄버거가 완성되는 즉시 햄버거를 제공한다
- 손님 A의 주문 완료 시점과 손님 B의 주문 완료 시점이 같을 필요가 없다
Node.js 의 경우도 효율적인 햄버거 가게라고 생각하면 된다.
→ non-blocking하고 비동기적(asynchronous)으로 작동하는 런타임으로 개발됨
→ 특히 JS의 비동기적 실행은 웹 개발에서 특히 유용
* 백그라운드 실행, 로딩 창 작업
* 인터넷에서 서버로 요청, 응답 받는 과정
* 큰 용량의 파일을 로딩
학습 목표
* 중첩된 callback이 발생하는 케이스
* 중첩된 callback의 단점, Promise의 장점
* Promise 사용 패턴의 이해
- `resolve`, `reject`, `then`, `catch`
- Promise에서 인자를 넘기는 방법
- Promise의 3가지 형태
- `Promise.all`
* `async/await` 키워드와 작동 원리 이해
* Node.js의 `fs` 모듈 사용
고차 함수와 Callback
고차 함수(HoF) 개념에 대해 다시 집고 넘어가자
* 고차함수는 다른 함수를 인자(argument)로 전달받을 수 있다
- 고차 함수의 인자로 전달되는 함수; 콜백 함수(callback function). 작업 중에 호출하는 경우가 많은 함수
* 고차함수는 다른 함수를 리턴할 수 있다(리턴 값이 함수인 형태)
- `함수를 리턴하는 함수`; `커리 함수`라고도 한다.
* `함수를 인자로 받는 함수`와 `함수를 리턴하는 함수` 모두 고차함수
- 즉, 고차함수는 콜백 함수와 커리 함수의 상위 개념
callback 함수를 전달 받은 caller
함수는 함수 내부에서 이 callback 함수를 호출(invoke)할 수 있다.caller
는 “조건”에 따라 callback 함수의 실행 여부를 결정
→ 조건에 따라서 아예 호출 안 할 수도, 여러 번 호출하여 실행할 수도 있는 것
(보통, 특정 작업의 완료 후에 호출하는 경우가 많다)
비동기 호출(Asychronous Call)
비동기를 다뤄보기 전에 callback 함수를 잘 집고 넘어가야 한다
콜백 함수
콜백 함수가 사용되는 경우들을 생각해 보자
* callback in action: 반복 실행하는 함수(iterator)
반복문 같은 곳 안에서 콜백 함수가 사용되는 경우를 생각
* callback in action: 이벤트에 따른 함수(event handler)
DOM에서 특정 이벤트에 의해 콜백 함수가 사용되는 경우
→ 헷갈리면 안되는 점: 함수 실행을 연결하는 것이 아니라 함수 자체를 연결해야 하는것
함수명(); ← 함수 실행( 틀린 케이스)
함수명; ← 함수 자체
blocking vs. non-blocking
blocking은 전화 통화, non-blocking은 문자라고 생각하면 이해가 편하다
* 전화: 하던 일을 멈추고 받아야 한다(blocking), 요청에 대한 결과가 동시에 일어남(동기적)
* 문자: 확인 후 나중에 답장 가능하다(non-blocking), 요청에 대한 결과가 동시에 일어나지 않는다(비동기적)
동기 vs. 비동기
동기: 요청에 대한 결과가 동시에 일어난다
→ 즉, 그 요청에 대한 응답이 끝나야 다음 요청을 받을 수 있는 것
비동기: 요청에 대한 결과가 동시에 일어나지 않는다
→ 그 요청에 대한 응답과 무관하게 다음 요청을 받을 수 있는 것(요청에 blocking이 없다)
→ 비동기의 경우 그 내부 코드에서 callback 함수를 많이 사용한다
비동기 함수 전달 패턴
비동기 함수의 전달 패턴들은 크게 2가지로 구분 가능하다
1. callback 패턴
일반적인 콜백 함수 사용한 경우
ex)
let request= ‘caffelatte’;
orderCoffeeAsync(request, function(response){
//response → 주문한 커피의 결과
drink(response);
});
2. 이벤트 등록 패턴
DOM에서 특정 이벤트에 사용한 경우
ex)
let request= ‘caffelatte’;
orderCoffeeAsync(request).onready = function(reponse){
//response → 주문한 커피의 결과
drink(response);
};
비동기의 주요 사례
* DOM Element의 이벤트 핸들러
- 마우스, 키보드 입력(click, keydown, ...)
- 페이지 로딩(DOMContentLoaded, …)
* 타이머
- 타이머 API(setTimeout, …)
- 애니메이션 API(requestAnimationFrame); 시간 흐름에 따라서 모양이 바뀌는?
* 서버에 자원 요청 및 응답; 가장 많이 사용하는 사례!
- fetch API
- AJAX(XHR)
브라우저의 비동기 함수 작동 원리를 알고 싶다면 “event loop” 키워드로 검색해 보자
비동기 자바 스크립트
왜 Async 자바스크립트 여야 하느냐?(Why Async)
동기적일 때보다 비동기적인 경우가 더 효율적이니깐(시간적으로 볼 때)
→ 비동기적인 경우, 어떤 특정 요청을 해 둔 상태에서 그 요청을 기다리지 않고 다른 요청이나 task를 처리할 수 있다
(유튜브 동영상 로딩을 걸어놓고 다른 댓글들을 보고 댓글을 단다거나 다른 리스트 찾아본다거나 하는 경우를 생각)
Callback
Async가 좋은 것은 알겠는데 그러면 순서를 제어하고 싶으면 어떻게 해야 하는가?
→ Callback 을 이용하자
가령, A , B, C의 순서대로 출력되도록 하고 싶은데?
→ A가 실행되는 구조 안에 Callback으로 B, C가 실행되게 받고, B가 실행되는 구조 안에 Callback으로
C가 실행되게 받는다면, A, B, C의 순서대로 로직이 동작한다.
Callback을 이용한 에러 핸들링 디자인 예
const isErrCheck = (callback) => {
waitingUntilSomethingHappens(); // 뭔가 일어 날때까지 기다림
if(isSomethingGood //어떤 조건){
callback(null, something); // 에러가 없고, data는 something
}
if(isSomethingBad //조건){
callback(something, null); // 어떤 오류메시지( something)와 데이터는 없
}
}
isErrCheck((err, data) => {
if (err) {
console.log(“에러 발생”);
return;
}
)}
그런데 콜백이 너무 많이 반복(callback chain)되면 callback Hell에 빠지게 된다 (코드 가독성이 떨어지게 됨)
→ 그렇다면 어떻게 해야 할까?
→ Promise 를 사용!
Promise, async/await
Promise
기본형태: new Promise( (resolve, reject) => {});
단순화해서 살펴 보면,
new Promise()
resolve() // ← 다음 행동으로 넘어 가게 됨
reject() // ← 에러 핸들링
resolve: 다음 행동(task)으로 넘어가게 함
→ 밖에서 호출해서 사용할때 .then 으로 받는다(.then과 한 쌍)
reject: 에러 핸들링하는 부분
→ 밖에서 호출해서 사용할 때 .catch로 받는다(.catch와 한 쌍)
아래의 콜백 함수 체인이 사용된 것을 보자
const printString = (string, callback) => {
setTimeout(
() => {
console.log(string)
callback()
},
Math.floor(Math.random() * 100)+1
)
}
const printAll = () => {
printString(‘A’ , () => {
printString(‘B’, () => {
printString(‘C’ , () => {
})
})
}
printAll();
이를 Promise를 이용해 바꿔보면,
const printString = (string) => {
return new Promise((resolve, reject) => { // resolve와 reject라는 콜백을 씀
setTimeout(
() => {
console.log(string)
resolve() // callback 대신 resolve 가 사용됨
//callback()
},
Math.floor(Math.random() * 100)+1
)
})
}
const printAll = () => {
printString(‘A’ )
.then(() => { // .then을 사용 ; “이 끝나면” 의 뜻으로 이해
return printString(‘B’)
})
.then(() => {
return printString(‘C’)
})
// reject가 있었다면 .catch를 이용한다
}
printAll();
이를 도식화 해서 보면
[Promise]
1st task → .then () → success task → .then () → success task
→ .catch() → failure task
그런데 Promise 도 너무 과도하게 사용되면 Promise Hell 에 빠질 수 있다
그래서 Promise Chaining 작업을 할 때 `return 처리`를 잘해야 한다!
Promise에 대한 추가 보충 내용
Q. new Promise()를 통해 생성한 Promise 인스턴스에는 어떤 메소드가 존재하나요? 각각은 어떤 용도인가요?
Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다.
다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서
미래의 어떤 시점에 결과를 제공합니다.
Q. Promise.prototype.then 메소드는 무엇을 리턴하나요?
→ 프로미스를 리턴한다
Q. Promise.prototype.catch 메소드는 무엇을 리턴하나요?
→ 값을 반환하는 경우: Promise를 리턴
→ 값을 반환하지 않는 경우: undefined를 리턴
Promise.prototype.catch 메소드 = Promise.prototype.then(undefined, onRejected)와 같은 뜻
Q. Promise의 세 가지 상태는 각각 무엇이며, 어떤 의미를 가지나요?
Promise는 다음 중 하나의 상태를 가집니다.
대기(pending): 이행하거나 거부되지 않은 초기 상태. -> 초기값.
이행이나 거부가 완료가 되었다면 다시 Promise를 반환해주면서 대기 상태로 돌아온다
이행(fulfilled): 연산이 성공적으로 완료됨. .then 이 된거
거부(rejected): 연산이 실패함. .catch 된거
Promise를 사용한 Best Practice
function getData(callback) {
// new Promise() 추가
return new Promise(function(resolve, reject) {
$.get('url 주소/products/1', function(response) {
// 데이터를 받으면 resolve() 호출
resolve(response);
});
});
}
// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
// resolve()의 결과 값이 여기로 전달됨
console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});
async/await
비동기 함수들을 마치 동기적인 프로그램인 것처럼 쓸 수 가 있다.
→ 일반 함수 실행하듯이 `평평하게` 코드 작성 가능
특정 함수 코드 작성 부분
...
const result = async () => { // async 함수다 라는 뜻
const one = await 함수A();
const two = await 함수B();
const three = await 함수C();
const four = await 함수D();
// A, B, C , D의 순서대로 실행됨을 알 수 있음
// 실제로 돌아가는 것은 Promise와 동일하나 표현에 있어서 동기적으로 표현 가능하게 한 것!
}
result();
타이머 API
비동기식 처리에 주로 사용된다
setTimeout(callback, millisecond)
일정 시간 후에 함수를 실행
// * arguments(인자): 실행할 callback 함수, millisecond(콜백 함수 실행 전 기다려야 할 시간; 밀리초)
// * return값(리턴값): 임의의 타이머 ID
setTimeout(function () {
console.log('1초 후 실행');
}, 1000);
// 123
setInterval(callback, millisecond)
일정 시간의 간격을 가지고 함수를 반복적으로 실행
// * 인자: 실행할 callback 함수, millisecond(반복적으로 함수를 실행시키기 위한 간격; 밀리초)
// * 리턴 값: 임의의 타이머 ID
setInterval(function () {
console.log('1초마다 실행');
}, 1000);
// 345
clearInterval(timeId)
반복 실행중인 타이머를 종료
// 인자: 타이머 ID(timeId)
// 리턴값: 없음
const timer = setInterval(function () {
console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// 더 이상 반복 실행되지 않음
setTimeout
에 대응하는 clearTimeout
도 존재한다
Node.js 모듈 사용하기
Node.js는 많은 API가 비동기로 작성되어 있다
→ Node.js: 비동기 이벤트 기반 자바스크립트 런타임 이기 때문
→ “로컬 환경”에서 자바스크립트를 실행할 수 있는 자바스크립트 런타임
브라우저에서 불가능한 몇가지 기능도 가능하다
Node.js 내장 모듈을 사용하는 방법
`fs(file system)` 모듈과 관련해서 살펴 보자
→ 파일을 읽거나 저장하는 기능을 구현하도록 돕는 모듈
파일을 읽을 때: readFile 메소드
파일을 저장할 때: writeFile 메소드
모든 모듈을 사용할 때는 모듈을 사용하기 위해 불러오는 과정
이 필요!
ex) 브라우저에서 다른 파일을 불러올 때는 HTML의 `<script> `태그 이용했었음
<script src=”불러오고자하는_스크립트명.js”></script>
Node.js 에서는 자바스크립트 코드 “가장 상단”에 require
구문을 이용하여 다른 파일을 불러온다
ex) Node.js 에서 다른 파일을 불러오는 require 구문
const fs = require('fs'); // 파일 시스템 모듈을 불러옵니다
const dns = require('dns'); // DNS 모듈을 불러옵니다
// 이제 fs.readFile 메소드 등을 사용할 수 있습니다!
3rd-party 모듈을 사용하는 방법
써드 파티 모듈이란 해당 프로그래밍 언어에서 공식적으로 제공하는 빌트인(built-in) 모듈이 아닌 모든 외부 모듈을 통칭한다.
예를 들면, underscore
의 경우 Node.js 공식문서에 없는 모듈이다
→ 써드 파티 모듈
이러한 써드 파티 모듈을 다운로드 받기 위해선?
→ npm install 명령어를 사용
ex) npm install 명령어를 통한 써드 파티 모듈인 `underscore` 설치
npm install underscore
이제 이 써드 파티 모듈을 사용하려면 위에서 배운 바와 같이 require 구문을 사용하면 된다
const 변수명A = require(‘underscore’);
fs.readFile을 통한 Node.js 공식 문서 탐구
fs.readFile: 로컬에 존재하는 파일을 불러오는(읽어오는) 메서드
→ 이 메서드를 Node.js를 통해 보는 법을 알아보자
fs.readFile(path[, options], callback)
메소드 fs.readFile은 비동기적으로 파일 내용 전체를 읽는데, 이 메소드를 실행할 때는 인자 3개를 넘길 수가 있다
→ 그것이 path , options(생략가능; 말그대로 옵션), callback함수
path <string> | <Buffer> | <URL> | <integer>
path 에는 파일 이름을 인자로 넘길 수가 있다. 위의 4가지 종류의 타입을 인자로 넘길 수 있지만, 일반적으로 문자열(<string>
) 타입으로 넘긴다
ex) '/etc/passwd' 파일을 불러오는 예시
fs.readFile(‘/etc/passwd’, … , …)
options <Object> | <string>
대괄호로 감싼 뜻은 두번째 인자인 options
는 넣을 수도 있고, 안 넣을 수 도 있다는 뜻
→ 대괄호는 선택적 인자
를 의미함
options는 객체 형태(object) 또는 문자열(string)로 인자를 넘길 수 있다. 문자열로 전달할 경우 인코딩을 넘긴다.
ex) 두번째 인자 options에 encoding 정보를 전달함
let options = {
encoding: 'utf8', // UTF-8이라는 인코딩 방식으로 연다는 뜻
flag: 'r' // 읽기 위해 엽니다 // 파일시스템과 관련된 flag. fs.readFile의 기본값은 :r
}
// /etc/passed 파일을 옵션을 사용하여 읽습니다.
fs.readFile('/etc/passwd', options, ...)
fs와 관련한 flag를 몇개 살펴 보면
r: 읽기
w: 쓰기
f: 파일이 존재함
정도로 요약 가능
callback <Function>
- `err` <Error>
- `data` <string> | <Buffer>
콜백 함수를 전달한다. 이는 파일을 읽고 난 “후”에 비동기적
으로 실행되는 함수이다
콜백 함수에는 2가지 파라미터(err, data)가 존재하는데,
에러가 발생하지 않으면 err
는 null
이 되며, data
에 문자열이나 buffer
라는 객체가 전달된다
`data`는 파일의 내용을 뜻함
Q: data에는 문자열이나 buffer가 전달되는데, 어떤 경우에 문자열로 전달이 될까?
A: options가 string이고 그것이 encoding 정보를 담고 있는 경우에 문자열(string)로 전달 된다
import { readFile } from 'fs';
readFile('/etc/passwd', 'utf8', callback);
// options가 utf8로 명시되었으므로, callback 함수에서 data에 string이 전달 된다
'SE Bootcamp 내용 정리' 카테고리의 다른 글
http/네트워크 - 기초 (0) | 2021.10.18 |
---|---|
js/node - 비동기 2 (0) | 2021.10.15 |
자료구조/알고리즘 - 자료구조 기초 2(Graph, Tree Search Algorithm) (0) | 2021.10.13 |
자료구조/알고리즘 - 자료구조 기초(Stack, Queue, Graph, Tree, BST) (0) | 2021.10.11 |
자료구조/알고리즘 - 재귀 (0) | 2021.10.11 |