Unit3 - [JS/Node] 비동기 - 2

강성일·2023년 5월 17일
0
post-thumbnail

✅ TIL


오늘은 어제에 이어서 promiseasync/await을 추가로 학습하겠다.

추가로 Node.js로 파일을 읽고 저장할 수 있는 fs 모듈에 대해서도 배우게 된다.

fs 모듈을 다루는 데에 기본이 되는 파일 읽기 및 쓰기를 배우고,
앞으로 학습할 callback, promise, async/await을 어떻게 적용할 수 있는지 실습한다.


💡 vscode의 전체적인 코드는 비동기적 흐름인데, 왜 비동기 제어를 따로 배울까?

vscode에 개발자들이 작성하는 일반적인 코드는 예측 가능하고 동기적으로 작업을 다루는 경향이 있다.
비동기 제어를 따로 하는 부분은 '네트워크'의 분야라서 얼마나 걸릴지 파악이 되지 않음으로 '비동기 작업을 순차적으로 조율하고 예측 가능한 방식으로 다루기 위해' 배운다.



간단한 복습

먼저 Callback 함수에 대한 개념 복습과, 오늘 다룰 모듈에 대한 개념을 간단히 언급하겠다.


콜백 함수란?

_.reduce = function (arr, iteratee, initVal) {
  // TODO: 여기에 코드를 작성합니다.
  let acc = initVal;
  _.each(arr, function (ele, idx, arr) {
    if (acc === undefined) {
      acc = arr[0];
    } else {
      acc = iteratee(acc, ele, idx, arr);
    }
  });
  return acc;
};

어제 다뤘던 과제에서 콜백 함수는 iteratee이었다.
iteratee는 각 요소와 누적값을 더하는 연산을 수행하는 콜백 함수로 사용되었다.

그리고 과제 Underbar 실습은 실제로 인기있게 사용되고 있는 underscore 라이브러리를 직접 비슷하게 만들어본 것이었다.

실제로 underscore를 설치하는 방법은 다음과 같다.


3rd-party 모듈을 사용하는 방법

서드 파티 모듈(3rd-party module)은 해당 프로그래밍 언어에서 공식적으로 제공하는
빌트인 모듈(built-in module)이 아닌 모든 외부 모듈을 일컫는다.

예를 들어, Node.js에서 underscoreNode.js 공식 문서에 없는 모듈이기 때문에 서드 파티 모듈이다.
따라서, underscore와 같은 서드 파티 모듈을 다운로드하기 위해서는 npm을 사용해야 한다.

터미널에서 다음과 같이 입력해 underscore를 설치할 수 있다.

npm install underscore

이제 node_modulesunderscore가 설치되었으므로,
Node.js 내장 모듈을 사용하듯 require 구문을 통해 underscore를 사용할 수 있습니다.

const _ = require('underscore');



동기(synchronous)


JavaScript의 동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것’을 의미한다.

주문 즉시 붕어빵을 만들어 주는 노점상이 있다고 생각해보자.

동기적으로 운영되는 노점상의 경우 붕어빵을 주문받은 후, 주문받은 붕어빵이 다 만들어지고 난 후에야 다음 손님의 주문을 받고 붕어빵을 제작하게 된다.

이 경우 여러 손님의 주문을 소화하기에는 무리가 있습니다.

  • 위의 그림처럼 하나의 task가 끝났을때 다음 task를 진행할 수 있다.
  • 작업 요청을 했을 때 요청의 결과값(return)을 직접 받는 것이다.
  • 요청의 결과값이 return값과 동일하다.
  • 호출한 함수가 작업 완료를 신경 쓴다.
  • 설계가 매우 간단하고 직관적이라는 장점이 있지만,
    요청에 따른 결과가 반환되기 전까지 아무것도 못하고 대기해야하는 단점도 있다.


비동기(asynchronous)


JavaScript의 비동기 처리는 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것’을 의미한다.

앞선 예시로 든 노점상이 비동기적으로 운영되는 경우 여러 손님의 주문을 받으면서 붕어빵을 제작하게 되고 완성되는 대로 손님에게 붕어빵을 제공하게 된다.

동기적으로 운영하는 경우보다 훨씬 효율적이다.

  • 위의 그림을 보면 동기와는 반대로 여러개의 태스크를 동시에 처리할 수 있다.
  • 작업 요청을 했을 때 요청의 결과값(return)을 간접적으로 받는 것이다.
  • 요청의 결과값이 return값과 다를 수 있다.
  • 해당 요청 작업은 별도의 스레드에서 실행하게 된다.
  • 콜백을 통한 처리가 비동기 처리라고 할 수 있다.
  • 호출된 함수(callback 함수)가 작업 완료를 신경 쓴다.
  • 요청에 따른 결과가 반환되는 시간 동안 다른 작업을 수행할 수 있다는 장점이 있다.
  • 단점은 동기식보다 설계가 복잡하고 논증적이다.



비동기 JavaScript


⏰ 타이머 관련 API


⚙️ setTimeout(callback, millisecond)

일정 시간 후에 함수를 실행

  • 매개변수(parameter): 실행할 콜백 함수, 콜백 함수 실행 전 기다려야 할 시간 (밀리초)
  • return 값: 임의의 타이머 ID
setTimeout(function () {
  console.log('1초 후 실행');
}, 1000);
// 123


⚙️ clearTimeout(timerId)

setTimeout 타이머를 종료

  • 매개변수(parameter): 타이머 ID
  • return 값: 없음
const timer = setTimeout(function () {
  console.log('10초 후 실행');
}, 10000);
clearTimeout(timer);
// setTimeout이 종료됨.


⚙️ setInterval(callback, millisecond)

일정 시간의 간격을 가지고 함수를 반복적으로 실행

  • 매개변수(parameter): 실행할 콜백 함수, 반복적으로 함수를 실행시키기 위한 시간 간격 (밀리초)
  • return 값: 임의의 타이머 ID
setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
// 345


⚙️ clearInterval(timerId)

setInterval 타이머를 종료

  • 매개변수: 타이머 ID
  • return 값: 없음
const timer = setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨.



비동기 제어를 배우는 이유


비동기 코드는 코드가 작성된 순서대로 작동되는 것이 아니라 동작이 완료되는 순서대로 작동하게 된다.
즉, 코드의 순서를 예측할 수 없다. 아래의 코드를 살펴보면서 이해해보자.


const printString = (string) => {
  setTimeout(function () {
    console.log(string);
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A');
  printString('B');
  printString('C');
};

printAll();

console.log(`아래와 같이 비동기 코드는 순서를 보장하지 않습니다!`);

출력 시, A B C 순서가 랜덤하게 찍힌다.

이처럼 프로그래밍을 하면서 개발자가 제어할 수 없는 코드를 작성하는 것은 옳지 않다.

개발자는 언제나 예측가능한 코드를 작성하도록 노력해야 한다.
따라서, 비동기로 작동하는 코드를 제어할 수 있는 방법에 대해 잘 알고 있어야 한다.

이제 그 방법들을 알아보도록 하겠다.



Callback


여러 방법 중 하나는 Callback 함수를 활용하는 방법이다.

Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있다.

즉, 비동기를 동기화할 수 있다는 의미입니다.


// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string, callback) => {
  setTimeout(function () {
    console.log(string);
    callback();
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {});
    });
  });
};

printAll();

console.log(
  `아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
);

Callback Hell


Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만
코드가 길어질수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.


// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string, callback) => {
  setTimeout(function () {
    console.log(string);
    callback();
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {
        printString('D', () => {
          printString('E', () => {
            printString('F', () => {
              printString('G', () => {
                printString('H', () => {
                  printString('I', () => {
                    printString('J', () => {
                      printString('K', () => {
                        printString('L', () => {
                          printString('M', () => {
                            printString('N', () => {
                              printString('O', () => {
                                printString('P', () => {});
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  });
};

printAll();

console.log(
  `아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있습니다.`
);

앞서 확인한 Callback Hell의 현상을 방지하기 위해 Promise가 사용되기 시작했다.

다음은 Promise에 대해 알아보자.



Promise


비동기로 작동하는 코드를 제어할 수 있는 다른 방법은 Promise를 활용하는 방법이다.

또한 Callback Hell을 방지하는 역할도 수행한다.


new Promise


Promise는 class이기 때문에 new 키워드를 통해 Promise 객체를 생성한다.

또한 Promise는 비동기 처리를 수행할 콜백 함수(executor)를 인수로 전달받는데,
이 콜백 함수는 resolve, reject 함수를 인수로 전달받는다.

Promise 객체가 생성되면 executor는 자동으로 실행되고 작성했던 코드들이 작동된다.

코드가 정상적으로 처리가 되었다면 resolve 함수를 호출하고
에러가 발생했을 경우에는 reject 함수를 호출하면 된다.


let promise = new Promise((resolve, reject) => {
	// 1. 정상적으로 처리되는 경우
	// resolve의 인자에 값을 전달할 수도 있습니다.
	resolve(value);

	// 2. 에러가 발생하는 경우
	// reject의 인자에 에러메세지를 전달할 수도 있습니다.
	reject(error);
});


Promise 객체의 내부 프로퍼티


new Promise가 반환하는 Promise 객체는 stateresult 내부 프로퍼티를 갖는다.

하지만 직접 접근할 수 없고 .then, .catch, .finally의 메서드를 사용해야 접근이 가능하다.


State

기본 상태는 pending(대기)이다.

비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동했다면,
fulfilled(이행)로 변경이 되고, 에러가 발생했다면 rejected(거부)가 됩니다.


Result

처음은 undefined이다.

비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동하여,
resolve(value)가 호출되면 value로, 에러가 발생하여 reject(error)가 호출되면 error로 변한다.



then, catch, finally

Then

executor에 작성했던 코드들이 정상적으로 처리가 되었다면
resolve 함수를 호출하고 .then 메서드로 접근할 수 있다.

또한 .then 안에서 리턴한 값이 PromisePromise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아오고,
Promise가 아니라면 리턴한 값을 .then 의 콜백 함수의 인자로 받아올 수 있습니다.

아래의 .then 과 Promise chaining의 예시를 살펴보면서 동작 방식을 확인해 보세요.


let promise = new Promise((resolve, reject) => {
	resolve("성공");
});

promise.then(value => {
	console.log(value);
	// "성공"
})


Catch

executor에 작성했던 코드들이 에러가 발생했을 경우에는 reject 함수를 호출하고 .catch 메서드로 접근할 수 있다.


let promise = new Promise(function(resolve, reject) {
	reject(new Error("에러"))
});

promise.catch(error => {
	console.log(error);
	// Error: 에러
})


Finally

executor에 작성했던 코드들의 정상 처리 여부와 상관없이 .finally 메서드로 접근할 수 있다.


let promise = new Promise(function(resolve, reject) {
	resolve("성공");
});

promise
.then(value => {
	console.log(value);
	// "성공"
})
.catch(error => {
	console.log(error);
})
.finally(() => {
	console.log("성공이든 실패든 작동!");
	// "성공이든 실패든 작동!"
})



Promise chaining


Promise chaining가 필요하는 경우는 비동기 작업을 순차적으로 진행해야 하는 경우다.

Promise chaining이 가능한 이유는 .then, .catch, .finally 의 메서드들은 Promise를 리턴하기 때문이다.

따라서 .then을 통해 연결할 수 있고, 에러가 발생할 경우 .catch 로 처리하면 된다.


Promise chaining 으로 개선된 코드

let promise = new Promise(function(resolve, reject) {
	resolve('성공');
	...
});

promise
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .catch((error) => {
    console.log(error);
    return '실패';
  })
  .finally(() => {
    console.log('성공이든 실패든 작동!');
  });

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
      console.log(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = () => {
  printString('A')
    .then(() => {
      return printString('B');
    })
    .then(() => {
      return printString('C');
    });
};

printAll();

console.log(
  `아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
);


Promise.all()


const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));

Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용한다.

인자로는 배열을 받는다.

해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면
결과를 배열에 저장해 새로운 Promise를 반환해준다.

앞서 배운 Promise chaining을 사용했을 경우는 코드들이 순차적으로 동작되기 때문에
총 6초의 시간이 걸리게 된다. 또한, 같은 코드가 중복되는 현상도 발생하게 된다.


// 기존
const result = [];
promiseOne()
  .then(value => {
    result.push(value);
    return promiseTwo();
  })
  .then(value => {
    result.push(value);
    return promiseThree();
  })
  .then(value => {
    result.push(value);
   console.log(result);
	 // ['1초', '2초', '3초']
  })

이러한 문제들을 Promise.all()을 통해 해결할 수 있다.

Promise.all()은 비동기 작업들을 동시에 처리한다. 따라서 3초 안에 모든 작업이 종료된다.

또한 Promise chaining로 작성한 코드보다 간결해진 것을 확인할 수 있다.


// promise.all
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
  .then((value) => console.log(value))
  // ['1초', '2초', '3초']
  .catch((err) => console.log(err));

추가적으로 Promise.all()은 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면
나머지 Promise의 state와 상관없이 즉시 종료된다.

아래의 예시와 같이 1초 후에 에러가 발생하고 Error: 에러1이 반환된 후로는 더 이상 작동하지 않고 종료된다.


Promise.all([
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
])
	.then((value) => console.log(value))
    .catch((err) => console.log(err));
	// Error: 에러1


Promise Hell


Promise를 통해 비동기 코드의 순서를 제어할 수 있지만,
Callback 함수와 같이 코드가 길어질수록 복잡해지고, 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.


const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = () => {
  printString('A').then((value) => {
    console.log(value);

    printString('B').then((value) => {
      console.log(value);

      printString('C').then((value) => {
        console.log(value);

        printString('D').then((value) => {
          console.log(value);

          printString('E').then((value) => {
            console.log(value);

            printString('F').then((value) => {
              console.log(value);

              printString('G').then((value) => {
                console.log(value);

                printString('H').then((value) => {
                  console.log(value);

                  printString('I').then((value) => {
                    console.log(value);

                    printString('J').then((value) => {
                      console.log(value);

                      printString('K').then((value) => {
                        console.log(value);

                        printString('L').then((value) => {
                          console.log(value);

                          printString('M').then((value) => {
                            console.log(value);

                            printString('N').then((value) => {
                              console.log(value);

                              printString('O').then((value) => {
                                console.log(value);

                                printString('P').then((value) => {
                                  console.log(value);
                                });
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  });
};

printAll();

console.log(
  `아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있습니다.`
);


Async / Await


JavaScript는 ES8에서 async/await키워드를 제공했다.

이를 통해 복잡한 Promise 코드를 간결하게 작성할 수 있게 되었다. 사용법은 간단하다.

함수 앞에 async 키워드를 사용하고 async 함수 내에서만 await 키워드를 사용하면 된다.

이렇게 작성된 코드는 await 키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작하게 된다.


// 함수 선언식
async function funcDeclarations() {
	await 작성하고자 하는 코드
	...
}

// 함수 표현식
const funcExpression = async function () {
	await 작성하고자 하는 코드
	...
}

// 화살표 함수
const ArrowFunc = async () => {
	await 작성하고자 하는 코드
	...
}

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
      console.log(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = async () => {
  await printString('A');
  await printString('B');
  await printString('C');
};

printAll();

console.log(
  `Async/Await을 통해 Promise를 간결한 코드로 작성할 수 있게 되었습니다.`
);


확실히 .then이 줄을 많이 잡아먹던 현상이 사라져서, 가독성이 매우 좋아졌다 😚



💡 요약

비동기 → 동기로 제어(순서가 있도록)

Promise는 비동기 함수(node.js가 추구하는 방식)인데, async와 await는 Promise를 동기함수로 만들어준다.

전체적인 비동기 코드에서 async 함수 내에서만은 동기로 제어할 수 있다. 덕분에 동작순서를 걱정할 일이 없다.


// 이것처럼 차레로 API를 받아와야할 때 !
const getMovies = async () => {
    const json = await (
      await fetch(
        `https://yts.mx/api/v2/list_movies.json?minimum_rating=${
          Math.random() * 10
        }&sort_by=year`
      )
    ).json();
    setMovies(json.data.movies);
    setLoading(false);
  };

async 작동 원리

async 함수 내에서 await를 사용하면 해당 비동기 작업의 완료를 기다리고, 그 사이에 함수의 실행이 일시 중지된다.
이로 인해 async 함수 내에서는 비동기 코드를 동기적으로 작성하고 제어할 수 있다.

async 함수는 비동기 작업을 처리하는 데에 사용되며, 함수 내에서 await 키워드를 사용하여 비동기 작업의 완료를 기다릴 수 있다.

이를 통해 비동기 작업이 완료된 후에만 다음 작업을 수행하거나 결과를 반환할 수 있다.
이로써 비동기 코드의 흐름을 동기적으로 제어할 수 있게 된다.

async 함수를 사용하여 비동기 코드를 동기적으로 제어하면 코드의 가독성이 향상되고 콜백 함수나 프로미스 체인을 사용하는 것보다 코드가 더 간결해진다.
또한 try-catch 문을 사용하여 비동기 작업 중 발생한 예외를 처리할 수 있어 오류 처리가 간편해진다.

따라서 async 함수를 사용하여 전체적인 비동기 코드를 동기적으로 제어할 수 있다.
다만, async 함수 내에서만 비동기 코드를 동기적으로 다룰 수 있고, async 함수 밖에서는 여전히 비동기 코드의 특성을 유지한다.



Node.js 개요


브라우저에서 사용할 수 있는 비동기 흐름은 타이머 혹은 DOM 이벤트와 관련된 상황으로 다소 한정적이지만, Node.js의 경우 많은 API가 비동기로 작성되어 있다.

Node.js 소개 문서의 첫 단락은 Node.js의 정의부터 시작한다.
Node.js는 "비동기 이벤트 기반 JavaScript 런타임"이다.

Node.js 모듈을 사용하는 방법을 먼저 학습하고, 이를 통해 비동기 상황을 연출하고 연습합니다.


모듈이란?

건축으로부터 비롯된 모듈이라는 단어는, 어떤 기능을 조립할 수 있는 형태로 만든 부분입니다.
그중 fs(File System) 모듈은, PC의 파일을 읽거나 저장하는 등의 일을 할 수 있게 도와줍니다.



Node.js 내장 모듈을 사용하는 방법


일을 읽을 때에는 [readFile]이라는 메서드가 적합하다.

파일의 저장은 ?
saveFile 이라는 메서드는 찾을 수 없지만, 비슷해 보이는 메서드가 있다.
파일을 저장할 때에는 [writeFile]을 쓰면 된다.


모든 모듈은 '모듈을 사용하기 위해 불러오는 과정'이 필요하다.

브라우저에서 다른 파일을 불러올 때에는 다음과 같이 <script> 태그를 이용했다.

// HTML에서 JavaScript 파일을 불러오는 script 태그
<script src="불러오고싶은_스크립트.js"></script>

Node.js는 JavaScript 코드 가장 상단에 require 구문을 이용하여 다른 파일을 불러온다.

// Node.js에서 다른 파일을 불러오는 require 구문
const fs = require('fs'); // 파일 시스템 모듈을 불러옵니다
const dns = require('dns'); // DNS 모듈을 불러옵니다

// 이제 fs.readFile 메서드 등을 사용할 수 있습니다!


fs.readFile을 통해 알아보는 Node.js 공식문서 가이드


메서드 fs.readFile은 로컬에 존재하는 파일을 읽어온다.

16.x 버전 기준으로, fs.readFile의 공식 API 문서에 안내되어 있는 항목을 설명하겠다.


fs.readFile(path[, options], callback)

메서드 fs.readFile비동기적으로 파일 내용 전체를 읽는다. 이 메서드를 실행할 때에는 전달인자 세 개를 받는다.


💡 path

\ string | \ Buffer | \ URL | \ integer

path에는 파일 이름을 전달인자로 받는다.
네 가지 종류의 타입을 넘길 수 있지만 일반적으로 문자열(<string>)의 타입을 받는다.

// 'etc/passwd'라는 파일을 불러오는 예제
fs.readFile('/etc/passwd', ..., ...)

💡 options

\ Object | \ string

대괄호로 감싼 두 번째 전달인자 options는 넣을 수도 있고, 넣지 않을 수도 있다.
대괄호는 선택적 전달인자를 의미한다.

options는 문자열 또는 객체 형태로 받을 수 있다. 문자열로 전달할 경우 인코딩을 받는다.
밑의 예제에서는 'utf8'을 두 번째 전달인자로 받는 것을 확인할 수 있다.

// 두 번째 전달인자 options에 '문자열'을 전달한 경우
// /etc/passwd 파일을 'utf8'을 사용하여 읽습니다.
fs.readFile('/etc/passwd', 'utf8', ...);
// 두 번째 전달인자 options에 '객체'를 전달한 경우
let options = {
  encoding: 'utf8', // utf8 인코딩 방식으로 엽니다
  flag: 'r' // 읽기 위해 엽니다
}
// /etc/passwd 파일을 options를 사용하여 읽습니다.
fs.readFile('/etc/passwd', options, ...)

💡 callback \ Function

⚙️ err

\ Error | \ AggregateError

⚙️ data

\ string | \ Buffer

콜백 함수를 전달한다. 파일을 읽고 난 후에 비동기적으로 실행되는 함수다.

콜백 함수에는 두 가지 매개변수가 존재한다.
에러가 발생하지 않으면 errnull 이 되며, data에 문자열이나 Buffer라는 객체가 전달된다.

data는 파일의 내용이다.

// 메서드 fs.readFile로 파일의 데이터를 읽을 수 있습니다.
fs.readFile('test.txt', 'utf8', (err, data) => {
  if (err) {
    throw err; // 에러를 던집니다.
  }
  console.log(data);
});


fetch API 개요


비동기 요청의 가장 대표적인 사례는 단연 네트워크 요청이다.

네트워크를 통해 이루어지는 요청은 그 형태가 다양하고, URL로 요청하는 경우가 가장 흔하다.

URL로 요청하는 것을 가능하게 해 주는 API가 바로 fetch API이다.


fetch API

사이트는 시시각각 변하는 정보와, 늘 고정적인 정보가 따로 분리되어 있는 것을 확인할 수 있다.
이 중에서 최신 뉴스날씨/미세먼지 정보가 바로 동적으로 데이터를 받아야 하는 정보이다.

이럴 때 많은 웹사이트에서는 해당 정보만 업데이트하기 위해 요청 API를 이용한다.
그중 대표적인 fetch API를 이용해 해당 정보를 원격 URL로부터 불러오는 경우를 설명하겠다.

fetch API는 위와 같이, 특정 URL로부터 정보를 받아오는 역할을 한다.
이 과정이 비동기로 이루어지기 때문에, 경우에 따라 다소 시간이 걸릴 수 있다.

이렇게 시간이 소요되는 작업을 요구할 경우에는 blocking이 발생하면 안 되므로,
특정 DOM에 정보가 표시될 때까지 로딩 창을 대신 띄우는 경우도 있다.


let url =
  "https://koreanjson.com/posts/1";
fetch(url)
  .then((response) => response.json())
  .then((json) => console.log(json))
  .catch((error) => console.log(error));


💬 Sprint Review


Part 2는 브라우저 환경과는 다르게 Node.js 환경은 로컬 컴퓨터에서 직접 실행되므로, 파일을 불러오거나 저장하는 등의 액션이 가능하다.

fs(File System) module 사용법을 잘 익힌다면 "파일 열기", "파일 저장하기" 등을 직접 구현할 수 있다.

Node.js에서는 파일을 읽는 비동기 함수를 제공합니다. 이를 통해 callback과 Promise를 구현해 봅니다.

node part-2/01_callback.js 로 part-2/01_callBack.js 파일을 실행할 수 있다.


const fs = require("fs");

const getDataFromFile = function (filePath, callback) {
  // TODO: fs.readFile을 이용해 작성합니다
  fs.readFile(filePath, "utf-8", (err, data) => {
    if (err) callback(err, null); // 에러 -> 콜백의 첫번째인자에 err를 넣어준다.
    else callback(null, data); //에러 X -> 콜백의 두번째 인자에 data를 넣어준다.
  });
};

getDataFromFile("README.md", (err, data) => console.log(data));

module.exports = {
  getDataFromFile,
};

//실행 시, [README.md](http://README.md) 파일 안에 있는 text를 읽어온다.

const fs = require("fs");

const getDataFromFilePromise = (filePath) => {
  // return new Promise()
  // TODO: Promise 및 fs.readFile을 이용해 작성합니다.
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, "utf8", (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
};

// getDataFromFilePromise("README.md").then((data) => console.log(data));

module.exports = {
  getDataFromFilePromise,
};

const path = require("path");
const { getDataFromFilePromise } = require("./02_promiseConstructor");

const user1Path = path.join(__dirname, "files/user1.json");
const user2Path = path.join(__dirname, "files/user2.json");

// HINT: getDataFromFilePromise(user1Path) 및 getDataFromFilePromise(user2Path)를 이용해 작성합니다
const readAllUsersChaining = () => {
  // TODO: 여러개의 Promise를 then으로 연결하여 작성합니다
  return getDataFromFilePromise(user1Path)
    .then((user1) => {
      return getDataFromFilePromise(user2Path).then((user2) => {
        return `[${user1}, ${user2}]`;
      });
    })
    .then((data) => JSON.parse(data));
};

// readAllUsersChaining();

module.exports = {
  readAllUsersChaining,
};

const path = require("path");
const { getDataFromFilePromise } = require("./02_promiseConstructor");

const user1Path = path.join(__dirname, "files/user1.json");
const user2Path = path.join(__dirname, "files/user2.json");

const readAllUsers = () => {
  // TODO: Promise.all을 이용해 작성합니다
  return Promise.all([
    getDataFromFilePromise(user1Path),
    getDataFromFilePromise(user2Path),
  ]).then(([data1, data2]) => {
    return JSON.parse(`[${data1}, ${data2}]`);
  });
};

readAllUsers()
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

module.exports = {
  readAllUsers,
};

const path = require("path");
const { getDataFromFilePromise } = require("./02_promiseConstructor");

const user1Path = path.join(__dirname, "files/user1.json");
const user2Path = path.join(__dirname, "files/user2.json");

const readAllUsersAsyncAwait = async () => {
  // TODO: async/await 키워드를 이용해 작성합니다
  const user1 = await getDataFromFilePromise(user1Path);
  const user2 = await getDataFromFilePromise(user2Path);
  let result = `[${user1}, ${user2}]`;
  return JSON.parse(result);
};

readAllUsersAsyncAwait();

module.exports = {
  readAllUsersAsyncAwait,
};


Part 3는 Node.js 환경을 떠나, 브라우저에서 진행한다.
Node.js 환경에는 fetch API가 내장 모듈로 제공되지 않기 때문이다.

이번에는 fetch를 이용해 HTTP 요청을 보내고, 응답을 받는 연습을 하게 된다.
다만 callback 형태의 요청이 존재하지 않으므로, chaining과 Promise.all 그리고 async/await을 이용한다.


const newsURL = "http://localhost:4999/data/latestNews";
const weatherURL = "http://localhost:4999/data/weather";

function getNewsAndWeather() {
  return fetch(newsURL)
    .then((response) => response.json()) // news.json
    .then((newsData) => {
      return fetch(weatherURL)
        .then((response) => response.json()) //weather.json
        .then((weatherData) => {
          return {
            news: newsData.data,
            weather: weatherData,
          };
        });
    });
}

// return new Promise((resolve, reject) => {
//   fetch(newsURL)
//     .then((JSON) => JSON.json())
//     .then((JSON) => {
//       news = [...JSON.data];
//     })
//     .catch((err) => err);
//   fetch(weatherURL)
//     .then((JSON) => JSON.json())
//     .then((JSON) => (weather = JSON))
//     .then(() => resolve({ news, weather }))
//     .catch((err) => err);
// });

if (typeof window === "undefined") {
  module.exports = {
    getNewsAndWeather,
  };
}

function getNewsAndWeatherAll() {
  // TODO: Promise.all을 이용해 작성합니다

  return Promise.all([
    fetch(newsURL).then((response) => response.json()), // news.json
    fetch(weatherURL).then((response) => response.json()), //weather.json
  ])
    .then(([news, weather]) => {
      return {
        news: news.data,
        weather: weather,
      };
    })
    .catch((error) => {
      console.log(error);
    });
}

getNewsAndWeatherAll()
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

if (typeof window === "undefined") {
  module.exports = {
    getNewsAndWeatherAll,
  };
}

async function getNewsAndWeatherAsync() {
  // TODO: async/await 키워드를 이용해 작성합니다

  const news = await fetch(newsURL).then((response) => response.json());
  const weather = await fetch(weatherURL).then((response) => response.json());
  return {
    news: news.data,
    weather: weather,
  };
}

if (typeof window === "undefined") {
  module.exports = {
    getNewsAndWeatherAsync,
  };
}
profile
아이디어가 넘치는 프론트엔드를 꿈꿉니다 🔥

0개의 댓글