관리 메뉴

정골라코딩

[JS] Node.js (2) 본문

JavaScript

[JS] Node.js (2)

정골라 2022. 10. 13. 23:07

Node.js 모듈 (Common JS) vs  ES6 모듈

노드에서 각 파일은 비공개 네임스페이스를 가진 독립적인 모듈입니다. (각 파일은 모두 모듈)

노드의 전역 객체 exports는 항상 정의 되어있습니다. (module.exports의 기본값은 exports가 참조하는 것과 같은 객체입니다.)

ES6의 모듈성은 노드의 모듈성과 같은 개념이다. 각 파일이 하나의 모듈이며 파일에서 정의한 상수, 변수, 함수, 클래스는 명시적으로 내보내지 않는 한 해당 모듈에서만 사용됩니다. 모듈에서 값을 내보내면 다른 모듈에서 명시적으로 가져와 사용할 수 있습니다.

 

Node 서버 생성

const http = require('http');

const server=http.createServer((request,response) => {
  //여기서 작업이 진행됩니다!
  });

node 웹 애플리케이션은 웹 서버 객체를 만들어야 합니다. 이 때 createServer를 사용합니다.

이 서버로 오는 HTTP 요청마다 createServer에 전달된 함수가 한번씩 호출됩니다.

const server=http.createServer();
server.on('request', (request, response) => {
 //여기서 작업이 진행됩니다!
 });

readFile, readFileSync

fs.readFile() 메서드는 파일을 읽는 데 사용되는 내장 메서드 입니다. 이 메서드는 전체 파일을 버퍼로 읽습니다.

  • filename: 읽을 파일의 이름이나 다른 위치에 저장된 경우 전체 경로
  • encoding: 파일의 인코딩, 기본값은 'utf8'
  • callback: 파일을 읽은 후 호출되는 콜백 함수
    • err: 작업에 실패하면 반환되는 오류
    • data: 파일의 내용
const fs=require('fs');
const fname='may_or_may_not_exist.txt';

fs.readFile(fname, function(err, data) {
   if(err) return console.error('error reading file ${fname}: ${err.message}`);
   console.log(`${fname} contents: ${data}`);
 });
 
 //콜백의 첫번째 매개변수에 에러 객체를 쓰는 것을 (error-first callback) 이라고 합니다.
 //에러 우선 콜백을 다룰 때 가장 먼저 생각할 것은 에러 매개변수를 체크하고 그에 맞게 로직을 구성하는 것 입니다.
 //err가 있으면 파일을 읽는 데 문제가 생겼다는 뜻으로 오류를 출력하고 해당 로직을 빠져나옵니다.

readFileSync

const fs=require('fs')
const data=fs.readFileSync('example.txt', {encoding: 'utf8', flag: 'r'})

Single Thread, Multi Thread

node.js는 싱글 스레드로 동작하면서 동시에 여러 요청을 처리할 수 있어야 합니다.

  • Non Blocking I/O (하나의 요청을 처리하는 동안 다른 요청을 블로킹하지 않는다는 의미)

node의 싱글 스레드는 클라이언트로부터 요청을 받으면 그 요청을 다른 일꾼에게 전달합니다. 따라서 싱글 스레드는 자유로워지고 두번째 요청을 받을 수 있습니다. 두번째 요청을 받을 경우 다른 일꾼에게 전달합니다. 그리고 여기서 일꾼은 다른 서버 또는 데이터베이스에 쿼리를 날려 요청을 처리합니다.

일꾼이 응답을 가져왔다면 (서버 또는 데이터베이스의 응답을 받아옴) 콜백 함수를 실행하게 됩니다.

node.js는 싱글 스레드라고 했는데 어떻게 멀티 스레드처럼 일꾼들을 데리고 동작할 수 있는걸까?

-node.js는 V8이라는 자바스크립트 엔진과 비동기 작업을 처리하는 libuv라는 라이브러리로 이루어져 있습니다.

비동기 작업을 가능하게 하는 것은 libuv 라는 라이브러리에서 non-blocking IO라는 기능을 가능하게 하는 이벤트 루프를 제공하기 때문입니다. libuv는 c언어로 개발되었고 시스템 커널을 이용하는데, 커널은 멀티 스레드를 이용합니다. 따라서 node.js는 libuv가 멀티스레드로 동작하기 때문에 비동기 처리를 할 수 있습니다.

 

요청의 흐름을 나타내보면

  1. node.js로 요청이 들어오면 들어온 요청은 event queue에 추가됩니다.

  2. node.js의 이벤트 루프는 event queue를 살펴 들어온 요청이 있다면 선착순 (first come first served) 요청이 처리됩니다.

  3. 요청은 internal C++ Thread Pool로 보내집니다. 이것은 libuv 에서 개발된 이벤트 루프의 일부로 여러 요청을 수행할 수 있습니다.       동시에 이벤트 루프는 event queue에 요청이 있는지 계속해서 확인하면서 요청이 있다면 thread pool로 가져옵니다.

  4. thread pool은 db 또는 file 또는 다른 서버 등에 보낸 요청을 수행합니다.

  5. 수행을 마치면 콜백 함수를 실행시켜 이벤트 루프로 응답을 전달 합니다.

  6. 이벤트 루프는 응답을 클라이언트에 보냅니다.

 

용어 정리

V8엔진: 자바스크립트 코드를 평가하여 객체 등의 동적 데이터가 할당되는 memory heap, context의 실행을 담당하는 하나의 call stack(=execution context stack) 을 가진다.

event loop: pending 상태인 자바스크립트 tack와 microtask를 execution context stack 에 푸시하여 실행시킨다.

Fetch API

fetch()는 URL 을 받고 프라미스를 반환합니다. 그 프라미스는 HTTP 응답이 도착하기 시작하여 HTTP 상태와 헤더를 읽으면 이행됩니다. 

fetch ("/api/user/profile").then(response=>{
  //프라미스가 해석되면 상태와 헤더가 존재합니다.
  if (response.ok && response.headers.get("Content-Type")==="application/json"){
  //아직 응답 바디는 받지 못했습니다.
  }
 })

fetch() 가 반환하는 프라미스가 이해오디면 프라미스는 then() 메서드에 전달한 함수에 응답 객체를 전달합니다. 이 응답 객체는 요청 상태와 헤더에 접근을 허용하며, 응답 바디에 각각 텍스트와 JSON 형태로 접근할 수 있는 text()와 json() 메서드도 가지고 있습니다.

 

프라미스가 이행되긴 했지만 응답 바디는 아직 도착하지 않았을 수도 있습니다. 따라서 응답 바디에 접근하는 text()와 json() 메서드 역시 프라미스를 반환합니다.

 

fetch("/api/user/profile").then(response => {
 response.json().then(profile=>{ //JSON으로 분석된 바디를 요청합니다.
   //응답 바디를 받으면 자동으로 JSON으로 전달하고 이 함수에 전달합니다.
   displayUserProfile(profile)
   })
 })

하지만 위와 같이 프라미스를 콜백처럼 중첩해서 사용하는 것은 프라미스의 설계 목적에 부합하지 않습니다. 아래와 같이 프라미스를 연속적인 체인으로 사용하는 것이 낫습니다. 

fetch("/api/user/profile")
  .then(response => {
    return response.json()
  })
  .then(profile => {
    displayUserProfile(profile)
  })
// fetch().then().then()

이렇게 표현식 하나에서 메서드를 하나 이상 호출하는 것을 메서드 체인이라고 부릅니다.

then, catch, finally는 Promise를 반환한다.
프라미스가 프라미스가 아닌 값으로 해석되면 그 프라미스는 그 값으로 즉시 이행됩니다.

axios vs fetch

axios와 fetch 모두 promise 기반의 HTTP 클라이언트 입니다. 즉 이 클라이언트를 이용해 네트워크 요청을 하면 resolve 혹은 reject 할 수 있는 promise가 반환됩니다.

먼저 fetch를 알아보겠습니다. fetch는 두 개의 인자를 받습니다. 첫번째 인자는 가져오고자 하는 리소스의 URL 입니다. 두 번째 인자는 요청의 설정 옵션을 포함하는 객체로 선택적 인자입니다.

 

두 번째 인자로 설정 옵션을 넘기지 않을 경우, 기본적으로 GET 요청을 생성합니다.

fetch(url);

설정 옵션을 넘기면 다음과 같이 요청에 대해 커스텀 설정을 할 수 있습니다.

fetch (url, {
method: "GET", // 다른 옵션도 가능합니다.(POST, PUT, DELETE, etc.)
headers:{
  "Content-Type": "application/json",
   },
   body: JSON.stringify({}),
 });

axios의 문법도 비슷하나, 다양한 방법으로 요청할 수 있습니다.

axios(url, {
//설정 옵션
});

아래와 같이 HTTP 메서드를 붙일 수도 있습니다.

axios.get(url, {
 //설정 옵션
});

fetch 메서드처럼 HTTP 메서드 없이 요청할 경우 기본적으로 GET 요청을 생성합니다. 또한 두번째 인자를 사용해서 커스텀 설정하는 것도 가능합니다.

axios (url, {
  method: "get", //다른 옵션도 가능합니다 (POST, PUT, DELETE, etc.)
  headers:{},
  data: {},
});

//아래처럼도 가능합니다.
axios({
  method: "get",
  url: url,
  header: {},
  data: {},
});

이제 axios 와 fetch의 응답 처리 시 어떤 차이가 있는지 알아보겠습니다.

아래 예제에서는 JSONPlaceholder 라는 REST API에 GET 요청을 통해 투두 리스트의 아이템을 가져오며 fetch와 axios의 차이점을 알아봅니다.

fetch API 를 사용하면 코드는 다음과 같습니다.

const url = "https://jsonplaceholder.typicode.com/todos";
fetch(url)
  .then((response) => response.json())
  .then(console.log);

fetch()는 위에 설명했기 때문에 넘어가고 axios를 보겠습니다.

const url = "https://jsonplaceholder.typicode.com/todos";
axios.get(url).then((response) => console.log(response.data));

axios를 사용하면 응답 데이터를 기본적으로 JSON 타입으로 사용할 수 있습니다. 응답 데이터는 언제나 응답 객체의 data 프로퍼티에서 사용할 수 있습니다.

다음과 같이 설정 옵션을 통해 responseType을 지정하여 기본 JSON 데이터 타입을 재정의할 수도 있습니다.

axios.get(url, {
  responseType: "json", // options: 'arraybuffer', 'document', 'blob', 'text', 'stream'
});

 POST로 데이터를 전송할 때 JavaScript 객체를 API로 전송하면 axios가 자동으로 데이터를 문자열로 변환해줍니다.

const url = "https://jsonplaceholder.typicode.com/todos";
const todo = {
  title: "A new todo",
  completed: false,
};
axios
  .post(url, {
    headers: {
      "Content-Type": "application/json",
},
    data: todo,
  })
  .then(console.log);

 만약 fetch api를 사용하면 JSON.stringify()를 사용해 객체를 문자열로 변환한 뒤 body에 할당해야 합니다.

기본적으로 axios는 Content-Type을 application/json으로 설정합니다.

const url = "https://jsonplaceholder.typicode.com/todos";
const todo = {
  title: "A new todo",
  completed: false,
};
fetch(url, {
  method: "post",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(todo),
})
  .then((response) => response.json())
  .then((data) => console.log(data));

fetch를 사용하면 명시적으로 Content-Type을 application/json으로 설정해야 합니다.

 

Promise가 reject되면, catch()를 사용해 에러를 처리할 수 있습니다. axios부터 살펴보겠습니다.

const url = "https://jsonplaceholder.typicode.com/todos";
axios
  .get(url)
  .then((response) => console.log(response.data))
  .catch((err) => {
    console.log(err.message);
  });

axios의 promise는 상태코드가 2xx의 범위를 넘어가면 거부(reject)합니다.

*에러 객체에 응답(response) 또는 요청(request) 프로퍼티가 포함되어 있는지 확인하여 에러에 대한 자세한 정보를 확인할 수 있습니다.

.catch ((err)=>{
 //에러 처리
 if(err.response){
 //요청이 이루어졌고 서버가 응답했을 경우
 
   const {status, config}=err.response;
   if(status===404){
     console.log(`${config.url} not found`);
   
   if(status===500){
     console.log("Server error");
   }
  
 } else if (err.request) {
   //요청이 이루어졌으나, 서버에서 응답이 없었을 경우
   console.log("Error", err.message);
   } else {
   //그 외 다른 에러
     console.log("Error", err.message);
     }
  });

에러 객체의 response 프로퍼티는 클라이언트가 2xx 범위를 벗어나는 상태 코드를 가진 에러 응답을 받았음을 나타냅니다. 에러 객체의 request 프로퍼티는 요청이 수행되었지만 클라이언트가 응답을 받지 못했음을 나타냅니다. 요청 또는 응답 속성이 모두 없는 경우는 네트워크 요청을 설정하는 동안 오류가 발생한 경우 입니다.

 

fetch는 404 에러나 다른 HTTP 에러 응답을 받았다고 해서 promise를 거부(reject) 하지 않습니다. Fetch는 네트워크 장애가 발생한 경우에만 promise를 거부(reject) 합니다. 따라서 .then 절을 사용해 수동으로 HTTP 에러를 처리해야 합니다.

const url = "https://jsonplaceholder.typicode.com/todos";
fetch(url)
  .then((response) => {
    if (!response.ok) {
      throw new Error(
        `This is an HTTP error: The status is ${response.status}`
      );
}
    return response.json();
  })
  .then(console.log)
  .catch((err) => {
    console.log(err.message);
  });

응답 블록에서 응답의 ok 상태가 false 인 경우 .catch 블록에서 처리되는 커스텀 에러를 발생시킵니다.

위 사진은 fetch가 성공했을 때의 스크린샷 입니다. 만약 잘못된 URL 엔트포인트를 요청했을 경우 ok와 status 속성은 각각 false 와 404 값을 가지게 됩니다. 이에 에러를 발생시키고 .catch() 절에서 커스텀 에러 메세지를 출력합니다.

 

각각의 HTTP 클라이언트에서 HTTP 요청이 시간 초과될 경우 어떻게 처리하는지 살펴봅시다. axios에서는 timeoue 속성을 설정 객체에 추가하여 요청이 종료될 때까지의 시간을 밀리 초로 지정할 수 있습니다.

 

다음 코드 스니펫에서는 만약 요청이 4초 이상 걸릴 경우에 종료하고 console 창에 error를 로깅하고 있습니다.

const url = "https://jsonplaceholder.typicode.com/todos";

axios
 .get(url, {
  timeout: 4000, // 기본 설정은 '0'입니다. (타임아웃 없음)
 })
 .then((response)=>console.log(response.data))
 .catch((err)=>{
  console.log(err.message);
 });

fetch를 통한 요청을 취소하기 위해서는 AbortController 인터페이스를 사용할 수 있습니다.

다음과 같이 사용할 수 있습니다.

const url = "https://jsonplaceholder.typicode.com/todos";
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 4000);
fetch(url, {
  signal: signal,
})
  .then((response) => response.json())
  .then(console.log)
  .catch((err) => {
    console.error(err.message);
  });

controller 객체를 생성하고 나서 signal 객체와 abort() 메서드에 접근했습니다. 이 signal 객체를 설정 옵션을 통해 fetch()에 넘깁니다. 이렇게 하면 abort 메서드가 호출될 때마다 fetch 요청이 종료됩니다. 보시다시피 setTimeout 기능을 사용하여 서버가 4초 이내에 응답하지 않으면 작업이 종료됩니다.

 

에러 처리

비동기 프로그래밍에서는 에러 처리가 중요합니다. 동기적 코드에서는 에러 처리 코드가 없으면 예외가 발생하고 스택 추적을 통해 어디서 무엇이 잘못됐는지 파악할 수 있습니다.

비동기 코드에서는 처리하지 않은 예외가 아무런 경고 없이 사라질 때가 많고 에러도 조용히 일어날 때가 많으므로 디버그하기 무척 어렵습니다.

 

프라미스의 .catch() 메서드는 비동기 코드의 한계를 보완하는 대안입니다. 동기적 코드에서는 뭔가 잘못됐을 때 예외가 catch 블록을 만날 때까지 '콜 스택을 따라 올라간다 (bubbling up the call stack)' 라고 합니다. 프라미스의 비동기 체인에서는 '.catch를 만날 때까지 에러가 체인을 따라 내려간다. (trickling down the chain)') 고 표현할 수 있습니다.

 

localStorage 와 sessionStorage

localStorage 와 sessionStorage를 웹 스토리지 객체 (web storage object) 라고 합니다.

localStorage 와 sessionStorage는 브라우저 내에 키-값 쌍을 저장할 수 있게 해줍니다. 이 둘을 사용하면 페이지를 새로 고침하고 (sessionStorage의 경우) 심지어 브라우저를 다시 실행해도 (localStorage의 경우) 데이터가 사라지지 않고 남아있습니다.

 

두 스토리지 객체는 동일한 메서드와 프로퍼티를 제공합니다.

  • setItem(key, value)-키-값 쌍으로 보관합니다.
  • getItem(key) - 키에 해당하는 값을 받아옵니다.
  • removeItem(key) - 키와 해당 값을 삭제합니다.
  • clear()- 모든 것을 삭제합니다.
  • key(index) -인덱스 (index) 에 해당하는 키를 받아옵니다.
  • length- 저장된 항목의 개수를 얻습니다.

두 스토리지 객체는 Map 과 유사합니다. setItem/getItem/removeItem 을 지원하죠. 하지만 인덱스를 사용해 키에 접근할 수 있다는 점(key(index)) 에서 차이가 있습니다.

 

localStorage 의 주요 기능은 다음과 같습니다.

  • 오리진이 같은 경우 데이터는 모든 탭과 창에서 공유됩니다.
  • 브라우저나 OS가 재시작하더라고 데이터가 파기되지 않습니다.
localStorage.setItem('test', 1);
alert( localStorage.getItem('test') ); // 1

sesstionStorage 객체는 localStorage 에 비해 자주 사용되진 않습니다.

제공하는 프로퍼티와 메서드는 같지만, 훨씬 제한적이기 때문입니다.

  • sessionStorage 는 현재 떠 있는 탭 내에서만 유지됩니다.
  • 같은 페이지라도 다른 탭에 있으면 다른 곳에 저장되기 때문입니다.
sessionStorage.setItem('test', 1);
alert(sessionStorage.getItem('test');

더 큰 저장공간을 원한다면, IndexedDB를 활용!

'JavaScript' 카테고리의 다른 글

[JS] Node.js와 MongoDB III  (0) 2022.10.21
[JS] Node.js 와 MongoDB II  (0) 2022.10.20
[JS] Node.js 와 MongDB I  (0) 2022.10.10
[JavaScript] 비동기 (2)  (0) 2022.10.06
[JS] 자료구조와 에러처리  (0) 2022.10.05