반응형

# Node.js 학습_npm(패키지 매니저)

1. npm이란

  • npm이란 Node Package Manager의 약자로, 노드의 패키지 매니저이다.
  • 다른 사람들이 만든 소스 코드들을 모아둔 저장소.
  • 남의 코드를 사용하여 프로그래밍 가능
  • 이미 있는 기능을 다시 구현할 필요가 없어 효율적이다.
  • 오픈 소스 생태계를 구성중
  • 패키지 : npm에 업로드된 노드 모듈
  • 모듈이 다른 모듈을 사용할 수 있듯, 패키지도 다른 패키지를 사용할 수 있다.
  • 의존 관계라고 부른다.

### package.json

  • 현재 프로젝트에 대한 정보와 사용 중인 패키지에 대한 정보를 담은 파일.
  • 같은 패키지라도 버전별로 기능이 다를 수 있으므로 버전을 기록해두어야 함
  • 동일한 버전을 설치하지 않으면 문제가 생길 수 있다.
  • 노드 프로젝트 시작 전 package.json부터 만들고 시작함 (package.json 생성 명령어 : npm init)
1. npm init
입력 시 아래와 같이 나옴.
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.

2. npm init 후 아래와 같은 문구 나오면 항목에 맞게 작성.
package name: (publish) npmtest
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author: lhyeonho
license: (ISC) MIT
About to write to C:\work\work_http_server_c\publish\package.json:

{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "lhyeonho",
  "license": "MIT"
}


Is this OK? (yes) yes

### package.json 속성들

  • package name : 패키지의 이름. package.json의 name 속성에 저장된다.
  • version : 패키지의 버전. npm의 버전은 다소 엄격하게 관리된다.
  • entry point : 자바스크립트 실행 파일 진입점. 보통 마지막으로 module.exports를 하는 파일을 지정한다. package.json의 main 속성에 저장된다.
  • test command : 코드를 테스트할 때 입력할 명령어를 의미. package.json scripts 속성 안의 test 속성에 저장된다.
  • git repository : 코드를 저장해둔 Git 저장소 주소를 의미한다. 나중에 소스에 문제가 생겼을 떄 사용자들이 이 저장소에 방문해 문제를 제기할 수도 있고, 코드 수정본을 올릴 수도 있다. package.json의 repository 속성에 저장된다.
  • keywords : 키워드는 npm 공식 홈페이지(https://npmjs.com)에서 패키지를 쉽게 찾을 수 있게 해준다. package.json의 keywords 속성에 저장된다.
  • license : 해당 패키지의 라이선스를 넣어주면 된다.

### npm 스크립트 (=터미널에 입력하는 명령어에 별명을 붙여주는 것)

  • npm init이 완료되면 폴더에 package.json이 생성된다.
{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "lhyeonho",
  "license": "MIT"
}
  • npm run [스크립트명]으로 스크립트 실행
1. 스크립트에 설정해놓은 test를 run 할 경우 아래와 같이 나옴.
npm run test

> npmtest@1.0.0 test C:\work\work_http_server_c\publish
> echo "Error: no test specified" && exit 1

"Error: no test specified" 
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! npmtest@1.0.0 test: `echo "Error: no test specified" && exit 1`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the npmtest@1.0.0 test script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\Hyeonho\AppData\Roaming\npm-cache\_logs\2021-10-05T12_07_03_187Z-debug.log

C:\work\work_http_server_c\publish>
  • 아래와 같이 스크립트에 "start"를 넣어 실행 가능. (해당 스크립트는 run이 생략된 npm start로 실행 가능)
{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index"
  },
  "author": "lhyeonho",
  "license": "MIT"
}

### 패키지 설치하기

  • express 설치하기 (아래와 같이 입력하여 설치하게 되면 package.json에 dependencies 추가됨.
npm i express
{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index"
  },
  "author": "lhyeonho",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  }
}
  • cookie-parser body-parser 동시에 설치도 가능.
npm i cookie-parser body-parser
{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index"
  },
  "author": "lhyeonho",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cookie-parser": "^1.4.5",
    "express": "^4.17.1"
  }
}
  • npm i -D : devDependencies는 개발 시에만 쓰이는 패키지를 저장해 두는 것.
npm i -D nodemon
{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index"
  },
  "author": "lhyeonho",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cookie-parser": "^1.4.5",
    "express": "^4.17.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.13"
  }
}

 

2. node_modules와 npx, SenVer

  • 배포 시에는 보통 node_modules를 지우고 배포 함. (용량을 많이 차지함)
  • package.json 이 있는 폴더에서 npm i 입력 시 package.json에 있는 dependencies 항목들이 다시 설치됨.
npm i
  • npm i -g : g는 global로 전역 설치. 이렇게 설치하면 dependencies에 기록되지 않음. (요즘엔 -g 설치 기피함 -> package.json에 없는 항목은 사용여부 추측이 어려움)
npm i -g rimraf
  • 여즘엔 -g 설치 기피하여 아래와 같이 설치.
npm i -D rimraf 

npx rimraf node_modules
  • npx : node package excute 로 node 패키지 실행 한다는 것.
  • package-lock.json 의 경우 버전 문제를 막아주는 파일 임.

### SemVer 버저닝

  • 노드 패키지의 버전은 SemVer(유의적 버저닝) 방식을 따름
  • Major(주 버전) , Minor(부 버전), Patch(수 버전)
  • 노드에서는 배포를 할 때 항상 버전을 올려야 함
  • Major : 하위 버전과 호환되지 않은 수정 사항이 생겼을 때 올림
  • Minor : 하위 버전과 호환되는 수정 사항이 생겼을 때 올림
  • Patch : 기능에 버그를 해결했을 때 올림
1.0.0

첫 번째 숫자 : Major	// 하위 호환되지 않는 변경 사항
두 번째 숫자 : Minor	// 하위 호환되는 변경 사항
세 번째 숫자 : Patch	// 간단한 버그 수정
  • 버전 기호 사용하기
버전 앞에 기호를 붙여 의미를 더함

^1.1.1 : 패키지 업데이트 시 minor 버전까지만 업데이트 됨(2.0.0버전은 안됨)

~1.1.1 : 패키지 업데이트 시 patch 버전까지만 업데이트 됨(1.2.0버전은 안됨)

>=, <=, >, <는 이상, 이하, 초과, 미만.

@latest : 최신을 의미

@next : 가장 최신 배포판 사용 가능(불안정함)

알파/베타/RC 버전이 존재할 수도 있음
(1.1.1-alpha.0 / 2.0.0-beta.1 / 2.0.0-rc.0)
  • 예시
1. 첫 번째 자리만 고정
^1.1.1


2. 두 번째 자리만 고정
~1.1.1


3. 세 번째 자리까지 모두 고정
1.1.1

 

3. npm 명령어

### 기타 명령어

  • npm outdated : 어떤 패키지에 기능 변화가 생겼는지 알 수 있음
  • npm uninstall 패키지명 : 패키지 삭제(npm rm 패키지명 으로 사용 가능)
  • npm search 검색어 : npm 패키지를 검색할 수 있다. (npmjs.com에서도 가능)
  • npm info 패키지명 : 패키지의 세부 정보 파악 가능
  • npm adduser : npm에 로그인을 하기 위한 명령어(npmjs.com에서 회원가입)
  • npm whoami : 현재 사용자가 누구인지 알려줌
  • npm logout : 로그인한 계정을 로그아웃
  • npm version 버전 : package.json의 버전을 올림 (이렇게 하면 git에 commit도 됨)
  • npm deprecate [패키지명][버전] [메시지] : 패키지를 설치할 때 경고 메시지를 띄우게 함 (오류가 있는 패키지에 적용)
  • npm publish : 자신이 만든 패키지를 배포
  • npm unpublish : 자신이 만든 패키지를 배포 중단(배포 후 24시간 내에만 가능). 다른 사람이 내 패키지를 사용하고 있는데 배포가 중단되면 문제가 생기기 때문.
  • npm ls 패키지명 : 내 프로젝트가 어떤 패키지를 사용하고 있는지 찾고 싶을 때 사용.
  • 그외 명령어는 https://docs.npmjs.com 의 CLI Commands에서 확인 가능. (https://docs.npmjs.com)
 

npm Docs

Documentation for the npm registry, website, and command-line interface

docs.npmjs.com

 

4. npm 배포

  • package.json이 있는 위치로 이동.
  • npm publish 입력하여 npm 배포.
C:\work\work_http_server_c>cd publish

C:\work\work_http_server_c\publish>npm publish 
npm notice 
npm notice package: npmtest35303011-3046@1.0.0
npm notice === Tarball Contents === 
npm notice 439B package.json
npm notice === Tarball Details === 
npm notice name:          npmtest35303011-3046
npm notice version:       1.0.0
npm notice package size:  345 B
npm notice unpacked size: 439 B
npm notice shasum:        9630f88ba8cd0fc537f32dd8ce6ed539b635c352
npm notice integrity:     sha512-ocG3QNKpShdyK[...]565CrPYe3ywTQ==
npm notice total files:   1
npm notice 
npm ERR! code E403
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/npmtest35303011-3046 - Package name triggered spam detection; if you believe this is in error, please contact support at https://npmjs.com/support
npm ERR! 403 In most cases, you or one of your dependencies are requesting
npm ERR! 403 a package version that is forbidden by your security policy.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\Hyeonho\AppData\Roaming\npm-cache\_logs\2021-10-05T12_53_36_119Z-debug.log

C:\work\work_http_server_c\publish>
  • npm info 설정한 패키지명(예)npmtest35303011-3046) 로 확인 가능.
  • npm unpublish 패키지명 으로 삭제 가능.
반응형
반응형

# http 모듈로 서버 만들기

1. HTTP 서버 만들기

### 서버와 클라이언트

  • 클라이언트가 서버로 요청(request)을 보냄
  • 서버는 요청을 처리
  • 처리 후 클라이언트로 응답(response)을 보냄.

### 노드로 http 서버 만들기

  • http 요청에 응답하는 노드 서버
  • createServer로 요청 이벤트에 대기
  • req 객체는 요청에 관한 정보가 / res 객체는 응답에 관한 정보가 담겨 있다.
# createServer.js
const http = require('http');

http.createServer((req, res) => {
    // 여기에 어떻게 응답할지 작성.
});

### 8080 포트 연결하기.

  • res 메서드로 응답 보냄 : write로 응답 내용을 적고, end로 응답 마무리(내용을 넣어도 됨)
  • listen(포트) 메서드로 특정 포트에 연결
  • 8080포트 연결 후 로컬에서 http://localhost:8080/ 으로 접속하여 확인 가능.
const http = require('http');

const server = http.createServer((req, res) => {
    // 요청에 대한 응답 작성. (응답 거부 가능.)
    res.write('<h1>Hello Node!</h1>');
    res.write('<p>Hello server</p>');
    res.end('<p>Hello Zero</p>');
})
    .listen(8080);
server.on('listening', () => {
    console.log('8080번 포트에서 서버 대기 중입니다.');
});

server.on('error', (error) => {
    console.error(error);
});

### localhost와 포트

  • localhost는 컴퓨터 내부 주소로, 외부에서는 접근 불가능
  • 포트는 서버 내에서 프로세스를 구분하는 번호이다.
  • 기본적으로 http 서버는 80번 포트 사용(생략 가능, https는 443)
  • 예) www.gilbut.com:80  -> www.gilbut.com  
  • 다른 포트로 데이터베이스나 다른 서버 동시에 연결 가능함.

 

2. fs로 HTML 읽어 제공하기

  • 수정 후에는 Ctrl + C 입력하여 서버를 내렸다가 node 파일명으로 다시 실행시켜줘야 정상적으로 반영 됨.
# server1.js
const http = require('http');

const server = http.createServer((req, res) => {
    // 요청에 대한 응답 작성. (응답 거부 가능.)
    res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8'});
    res.write('<h1>Hello Node!</h1>');
    res.write('<p>Hello server</p>');
    res.end('<p>Hello Zero</p>');
})
    .listen(8080);
server.on('listening', () => {
    console.log('8080번 포트에서 서버 대기 중입니다.');
});

server.on('error', (error) => {
    console.error(error);
});
  • 아래와 같이 포트 추가하여 서버 2개, 3개도 돌릴수 있음
# server1.js
const http = require('http');

// 8080 포트 서버
const server = http.createServer((req, res) => {
    // 요청에 대한 응답 작성. (응답 거부 가능.)
    res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8'});
    res.write('<h1>Hello Node!</h1>');
    res.write('<p>Hello server</p>');
    res.end('<p>Hello Zero</p>');
})
    .listen(8080);
server.on('listening', () => {
    console.log('8080번 포트에서 서버 대기 중입니다.');
});

server.on('error', (error) => {
    console.error(error);
});

// 8081 포트 서버
const server1 = http.createServer((req, res) => {
    // 요청에 대한 응답 작성. (응답 거부 가능.)
    res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8'});
    res.write('<h1>Hello Node!</h1>');
    res.write('<p>Hello server</p>');
    res.end('<p>Hello Zero</p>');
})
    .listen(8081);
    
server.on('listening', () => {
    console.log('8081 포트 대기 중');
});
  • 아래와 같이 html 파일을 별도로 생성하여 연결할 수 있다.
# server2.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Node.js 웹 서버</title>
    </head>
    <body>
        <h1>Node.js 웹 서버</h1>
        <p>만들 준비 됨?</p>
    </body>
</html>




# server2.js
const http = require('http');
const fs = require('fs').promises;

const server = http.createServer(async (req, res) => {
    res.writeHead(200, { 'Context-Type' : 'text/html; charset=utf-8' });
    const data = await fs.readFile('./server2.html');
    res.end(data);
}).listen(8080);

server.on('listening', () => {
    console.log('8080번 포트에서 서버 대기 중.');
});

server.on('error', (error) => {
    console.error(error);
});

 

3. REST API 서버 만들기 / POST, PUT, DELETE 요청 보내기

  • 서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현.
/index.html이면 index.html을 보내달라는 뜻.

항상 html 요구할 필요는 없음

서버가 이해하기 쉬운 주소가 좋음
  • REST API (Represenational State Transfer)
서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법

/user이면 사용자 정보에 관한 정보를 요청하는 것

/post면 게시글에 관련된 자원을 요청하는 것
  • HTTP 요청 메서드
GET : 서버 자원을 가져오라고 할 때 사용

POST : 서버에 새로 등록하고자 할 때 사용(또는 뭘 써야할 지 애매할 때)

PUT : 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할 때 사용 (전체 수정)

PATCH : 서버 자원의 일부만 수정하고자 할 때 사용 (부분 수정)

DELETE : 서버의 자원을 삭제하고 할 때 사용

### HTTP 프로토콜

  • 클라이언트가 누구든 서버와 HTTP 프로토콜로 소통 가능
IOS, 안드로이드, 웹이 모두 같은 주소로 요청 보낼 수 있음

서버와 클라이언트의 분리
  • RESTful
REST API를 사용한 주소 체계를 이용하는 서버

GET /user는 사용자를 조회하는 요청, POST / user는 사용자를 등록하는 요청

### REST API 서버 만들기

  • 개발자 도구(F12) Network 탭에서 REST 요청 내용을 실시간으로 확인 가능하다.
Name는 요청 주소, Method는 요청 메서드, Status는 HTTP 응답 코드

Protocol은 HTTP 프로토콜, Type은 요청 종류(xhr은 AJAX 요청)
  • restServer.js
const http = require('http');
const fs = require('fs').promises;

const users = {}; // 데이터 저장용

http.createServer(async (req, res) => {
  try {
    if (req.method === 'GET') {
      if (req.url === '/') {
        const data = await fs.readFile('./restFront.html');
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        return res.end(data);
      } else if (req.url === '/about') {
        const data = await fs.readFile('./about.html');
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        return res.end(data);
      } else if (req.url === '/users') {
        res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
        return res.end(JSON.stringify(users));
      }
      // /도 /about도 /users도 아니면
      try {
        const data = await fs.readFile(`.${req.url}`);
        return res.end(data);
      } catch (err) {
        // 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
      }
    } else if (req.method === 'POST') {
      if (req.url === '/user') {
        let body = '';
        // 요청의 body를 stream 형식으로 받음
        req.on('data', (data) => {
          body += data;
        });
        // 요청의 body를 다 받은 후 실행됨
        return req.on('end', () => {
          console.log('POST 본문(Body):', body);
          const { name } = JSON.parse(body);
          const id = Date.now();
          users[id] = name;
          res.writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' });
          res.end('ok');
        });
      }
    } else if (req.method === 'PUT') {
      if (req.url.startsWith('/user/')) {
        const key = req.url.split('/')[2];
        let body = '';
        req.on('data', (data) => {
          body += data;
        });
        return req.on('end', () => {
          console.log('PUT 본문(Body):', body);
          users[key] = JSON.parse(body).name;
          res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
          return res.end('ok');
        });
      }
    } else if (req.method === 'DELETE') {
      if (req.url.startsWith('/user/')) {
        const key = req.url.split('/')[2];
        delete users[key];
        res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
        return res.end('ok');
      }
    }
    res.writeHead(404);
    return res.end('NOT FOUND');
  } catch (err) {
    console.error(err);
    res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(err.message);
  }
})
  .listen(8082, () => {
    console.log('8082번 포트에서 서버 대기 중입니다');
  });
  • restFront.js
async function getUser() { // 로딩 시 사용자 가져오는 함수
  try {
    const res = await axios.get('/users');
    const users = res.data;
    const list = document.getElementById('list');
    list.innerHTML = '';
    // 사용자마다 반복적으로 화면 표시 및 이벤트 연결
    Object.keys(users).map(function (key) {
      const userDiv = document.createElement('div');
      const span = document.createElement('span');
      span.textContent = users[key];
      const edit = document.createElement('button');
      edit.textContent = '수정';
      edit.addEventListener('click', async () => { // 수정 버튼 클릭
        const name = prompt('바꿀 이름을 입력하세요');
        if (!name) {
          return alert('이름을 반드시 입력하셔야 합니다');
        }
        try {
          await axios.put('/user/' + key, { name });
          getUser();
        } catch (err) {
          console.error(err);
        }
      });
      const remove = document.createElement('button');
      remove.textContent = '삭제';
      remove.addEventListener('click', async () => { // 삭제 버튼 클릭
        try {
          await axios.delete('/user/' + key);
          getUser();
        } catch (err) {
          console.error(err);
        }
      });
      userDiv.appendChild(span);
      userDiv.appendChild(edit);
      userDiv.appendChild(remove);
      list.appendChild(userDiv);
      console.log(res.data);
    });
  } catch (err) {
    console.error(err);
  }
}

window.onload = getUser; // 화면 로딩 시 getUser 호출
// 폼 제출(submit) 시 실행
document.getElementById('form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const name = e.target.username.value;
  if (!name) {
    return alert('이름을 입력하세요');
  }
  try {
    await axios.post('/user', { name });
    getUser();
  } catch (err) {
    console.error(err);
  }
  e.target.username.value = '';
});
  • restFront.html
<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="utf-8" />
    <title>RESTful SERVER</title>
    <link rel="stylesheet" href="./restFront.css" />
</head>

<body>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
    </nav>
    <div>
        <form id="form">
            <input type="text" id="username">
            <button type="submit">등록</button>
        </form>
    </div>
    <div id="list"></div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="./restFront.js"></script>
</body>

</html>
  • about.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>RESTful SERVER</title>
    <link rel="stylesheet" href="./restFront.css" />
</head>

<body>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
    </nav>
    <div>
        <h2>소개 페이지입니다.</h2>
        <p>사용자 이름을 등록하세요!</p>
    </div>
</body>

</html>

 

4. 쿠키 이해하기

### 쿠키의 필요성

  • 요청에는 한 가지 단점이 있다.
누가 요청을 보냈는지 모름(IP 주소와 브라우저 정보 정도만 앎)

이때 로그인을 구현해서 누가 요청을 보냈는지 알아야하는데, 쿠키와 세션이 필요.
  • 쿠키 : 키=값의 쌍
name=zero 처럼 대상이 누군지 매 요청마다 서버에 동봉해서 보냄

서버는 쿠키를 읽어 누구인지 파악

### 쿠키 서버 만들기

  • 쿠키 넣는 것을 직접 구현
  • writeHead : 요청 헤더에 입력하는 메서드
  • Set-Cookie : 브라우저에게 쿠키를 설정하라고 명령
# cookie.js
const http = require('http');

http.createServer((req, res) => {
    console.log(req.url, req.headers.cookie);
    res.writeHead(200, { 'Set-Cookie' : 'mycookie=test' });
    res.end('Hello Cookie');
})
    .listen(8083, () => {
        console.log('8083번 포트에서 서버 대기 중');
    });

### 헤더와 본문

  • http 요청과 응답은 헤더와 본문을 가진다.
헤더 : 요청 또는 응답에 대한 정보를 가진다.

본문 : 주고받는 실제 데이터.

쿠키 : 부가적인  정보이므로 헤더에 저장

### 쿠키로 나 식별하기

  • 쿠키에 내 정보를 입력
  • parseCookies : 쿠키 문자열을 객체로 변환
  • 주소가 /login인 경우와 /인 경우로 나뉨
  • /login인 경우 쿼리스트링으로 온 이름을 쿠키로 저장
  • 그 외의 경우 쿠기가 있는지 없는지 판단 (쿠키가 있으면 환영 인사 / 없으면 로그인 페이지로 리다이렉트)
  • 쿠키에 만료 시간 등 설정해주지 않는 경우 세션쿠키가 되어 브라우저를 종료하면 쿠키가 사라짐.
  • 쿠키 옵션
쿠키명 = 쿠키값; 기본적인 쿠키의 값. mycookie=test 또는 name=zero 같이 설정한다.

Expires = 날짜; 만료 기한. 해당 기한이 지나면 쿠키가 제거된다. 기본값은 클라이언트가 종료될 때 까지.

Max-age = 초; Expires와 비슷하지만 날짜 대신 초를 입력할 수 있다. 해당 초가 지나면 쿠키가 제거된다.
(Expires 보다 우선됨)

Domain = 도메인명; 쿠키가 전송될 도메인을 특정할 수 있다. 기본값은 현재 도메인.

Path=URL; 쿠기가 전송될 URL을 특정할 수 있다. 기본값은 '/'이고 이 경우 모든 URL에서 쿠키를 전송할 수 있다.

Secure : HTTPS 일 경우에만 쿠키가 전송된다.

HttpOnly; 설정 시 자바스크립트에서 쿠키에 접근할 수 없다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋다.
  • cookie2.js
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
    cookie
        .split(';')
        .map(v => v.split('='))
        .reduce((acc, [k, v]) => {
            acc[k.trim()] = decodeURIComponent(v);
            return acc;
        }, {});

http.createServer(async (req, res) => {
    const cookies = parseCookies(req.headers.cookie);   // { mycookie : 'test' }

    // 주소가 로그인으로 시작하는 경우
    if (req.url.startsWith('/login')) {
        const { query } = url.parse(req.url);
        const { name } = qs.parse(query);
        const expires = new Date();

        // 쿠키 유효 시간을 현재 시간 + 5분으로 설정
        expires.setMinutes(expires.getMinutes() + 5);
        res.writeHead(302, {
            Location: '/',
            'Set-Cookie' : `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
        });
        res.end();

    // name 이라는 쿠기가 있는 경우
    } else if (cookies.name) {
        res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8' });
        res.end(`${cookies.name}님 안녕하세요.`);
    } else {
        try {
            const data = await fs.readFile('./cookie2.html');
            res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8' });
            res.end(data);
        } catch {
            res.writeHead(500, { 'Content-Type' : 'text/plain; charset=utf-8' });
            res.end(err.message);
        }
    }
})
    .listen(8084, () => {
        console.log('8084번 포트 대기 중');
    });
  • cookie2.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>쿠키&세션 이해하기</title>
</head>

<body>
<form action="/login">
    <input id="name" name="name" placeholder="이름 입력하세요.">
    <button id="login">로그인</button>
</form>
</body>

</html>

 

5. 세션 사용하기

### 세션 사용하기.

  • 쿠키의 정보는 노출되고 수정되는 위험이 있음(개발자 도구 통해서)
  • 중요한 정보는 서버에서 관리하고 클라이언트에는 세션 키만 제공
  • 서버에 저장 객체(session) 생성 후, uniqueInt(키)를 만들어서 속성명으로 사용.
  • 속성 값에 정보를 저장하고, uniqueInt를 클라이언트에게 보냄
  • session.js
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
    cookie
        .split(';')
        .map(v => v.split('='))
        .reduce((acc, [k, v]) => {
            acc[k.trim()] = decodeURIComponent(v);
            return acc;
        }, {});

const session = {};

http.createServer(async (req, res) => {
    const cookies = parseCookies(req.headers.cookie);
    
    if (req.url.startsWith('/login')) {
        const { query } = url.parse(req.url);
        const { name } = qs.parse(query);
        const expires = new Date();
        expires.setMinutes(expires.getMinutes() + 5);
        
        const uniqueInt = Date.now();
        session[uniqueInt] = {
            name,
            expires,
        };
        res.writeHead(302, {
            Location: '/',
            'Set-Cookie' : `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
        });
        res.end();
    
    // 세션쿠키가 존재하고, 만료 기간이 지나지 않았다면
    } else if (cookies.session && session[cookies.session].expires > new Date()) {
        res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8' });
        res.end(`${session[cookies.session].name}님 안녕하세요.`);  
    } else {
        try {
            const data = await fs.readFile('./cookie2.html');
            res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8' });
            res.end(data);
        } catch (err) {
            res.writeHead(500, { 'Content-Type' : 'text/plain; charset=utf-8' });
            res.end(err.message);
        }
    }
})
    .listen(8085, () => {
        console.log('8085번 포트 대기 중');
    });

 

6. https, http2

### https

  • 웹 서버에 SSL 암호화를 추가하는 모듈
  • 오고 가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없음
  • 요즘에는 https 적용이 필수(개인 정보가 있는 곳은 특히 심함)
  • https 서버
http 서버를 https 서버로 (암호화를 위해 인증서가 필요한데 발급받아야 함)

createServer가 인자를 두 개 받음
첫 번째 인자는 인증서와 관련된 옵션 객체
pem, crt, key 등 인증서를 구입할 때 얻을 수 있는 파일 넣기
두 번째 인자는 서버 로직

### http2

  • SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용하는 모듈
요청 및 응답 방식이 기존 http/1.1보다 개선됨

웹의 속도도 개선됨
 

Let's Encrypt - 무료 SSL/TLS 인증서

 

letsencrypt.org

  • server1-4.js
const http2 = require('http2');
const fs = require('fs');

http2.createSecureServer({
    cert : fs.readFileSync('도메인 인증서 경로'),
    key : fs.readFileSync('도메인 비밀키 경로'),
    ca : [
        fs.readFileSync('상위 인증서 경로'),
        fs.readFileSync('상위 인증서 경로')
    ],
}, (req, res) => {
    res.writeBead(200, { 'Content-Type' : 'text/html; charset=utf-8' });
    res.write('<h1>Hello node!!!!</h1>');
    res.end('<p>Hello Server!!!!!!</p>');
})
    .listen(443, () => {
        console.log('443번 포트에서 서버 대기 중')
    });
  • 개발환경에서는 http 를, 배포 시에는 https로 작성하여 사용.

 

7. cluster

  • 기본적으로 싱글 스레드인 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈
  • 포트를 공유하는 노드 프로세스를 여러 개 둘 수 있음
  • 요청이 많이 들어 왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산됨
  • 서버에 무리가 덜 감
  • 코어가 8개인 서버가 있을 때 보통은 하나만 활용
  • cluster로 코어 하나당 노드 프로세스 하나를 배정 가능
  • 성능이 8배가 되는 것은 아니지만 개선됨
  • 컴퓨터 자원(메모리, 세션 등)을 공유하지 못한다는 단점 존재.
  • Redis 등 별도 서버로 해결.

### 서버 클러스터링

  • 마스터 프로세스와 워커 프로세스
  • 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만듦(worker_threads랑 구조 비슷)
  • 요청이 들어오면 워커 프로세스에 고르게 분배.
  • 클러스터를 이용하면 하나의 포트에서 여러 서버를 동시에 사용 가능.
  • cluster.js
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    console.log(`마스터 프로세스 아이디 : ${process.pid}`);

    // CPU 개수만큼 워커 생산
    for (let i = 0; 1 < numCPUs; i += 1) {
        cluster.fork();
    }

    // 워커가 종료되었을 때.
    cluster.on('exit', (worker, code, signal) => {
        console.log(`${worker.process.pid}번 워커가 종료됨.`);
        console.log('code', code, 'signal', signal);
        cluster.fork();
    });
} else {
    // 워커들이 포트에서 대기
    http.createServer((req, res) => {
        res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8' });
        res.write('<h1>Hello Node!!!!!!!</h1>');
        res.end('<p>Hello Cluster!!!!!</p>');
        setTimeout(() => {  // 워커 존재 확인 위해 1초마다 강제 종료.
            process.exit(1);
        }, 1000);
    }).listen(8086);

    console.log(`${process.pid}번 워커 실행`)
}
반응형
반응형

# 스레드풀

  • fs, crypto, zlib 모듈의 메서드를 실행할 때는 백그라운드에서 동시에 실행됨. (스레드 풀이 동시에 처리해줌)
  • 노드는 기본적으로 백그라운드에서 4개씩 동시에 돌림. 
  • SET UV_THREADPOOL_SIZE=8 명령어를 이용하여 컴퓨터 사양에 맞게 변경 가능. (맥/리눅스의 경우 UV_THREADPOOL_SIZE=8 로 변경 가능)
  • const crypto = require('crypto'); const pass = 'pass'; const salt = 'salt'; const start = Date.now(); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('1 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('2 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('3 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('4 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('5 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('6 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('7 ', Date.now() - start); }); crypto.pbkdf2(pass, salt, 1_000_000, 128, 'sha512', () => { console.log('8 ', Date.now() - start); });

 

# 커스텀 이벤트

const EventEmitter = require('events');

const myEvent = new EventEmitter(); // 커스텀 이벤트
myEvent.addListener('event1', () => {
    console.log('이벤트 1');
});
myEvent.on('event2', () => {
    console.log('이벤트 2');
});
myEvent.on('event2', () => {
    console.log('이벤트 2 추가');
});
myEvent.once('event3', () => {
    console.log('이벤트 3');
}); // once : 한 번만 실행됨.
myEvent.emit('event2', () => {
    console.log('이벤트 2');
});

myEvent.emit('event1'); // 이벤트 호출
myEvent.emit('event2'); // 이벤트 호출

myEvent.emit('event3'); // 이벤트 호출
myEvent.emit('event2'); // 실행 안 됨.

myEvent.on('event4', () => {
    console.log('이벤트 4');
});
myEvent.removeAllListeners('event4');   // 관련된 모든 콜백함수 지움
myEvent.emit('event4'); // 실행 안 됨.

const listener = () => {
    console.log('이벤트 5');
};

myEvent.on('event5', listener);
myEvent.removeListener('event5', listener); //
myEvent.emit('event5'); // 실행 안 됨.

console.log(myEvent.listenerCount('event2'));   // 등록된 콜백함수 개수 확인

 

# 에러 처리(예외 처리)

  • 예외(Exception) : 처리하지 못한 에러
  • 노드 스레드를 멈춤
  • 노드는 기본적으로 싱글 스레드라 스레드가 멈춘다는 것은 프로세스가 멈추는 것.
  • 에러 처리는 필수이다.
  • 기본적으로 try ~ catch문으로 예외를 처리한다. (에러가 발생할 만한 곳을 try ~ catch로 감쌈)
  • 노드가 기본적으로 제공하는 비 동기 함수들의 콜백 에러는 노드 프로세스를 멈추지는 않음.
# error1.js
setInterval(() => {
    console.log('시작');
    try {
        throw new Error('서버를 고장내주마!!!!!');
    } catch (err) {
        console.error(err);
    }
}, 1000);



# error2.js
const fs = require('fs');

setInterval(() => {
    fs.unlink('./abcdefg.js', (err) => {
        if (err) {
            console.error(err);
        }
    });
}, 1000);



# error3.js
const fs = require('fs').promises;

setInterval(() => {
    fs.unlink('./abcdefg.js')
}, 1000);


출력 결과 (프로미스에 catch 붙이지 않은 경우 출력되는 문구.)
(node:29168) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, unlink 'C:\work\abcdefg.js'
(Use `node --trace-warnings ...` to show where the warning was created)
(node:29168) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async 
function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)



# error4.js
process.on('uncaughtException', (err) => {
    console.error('예기치 못한 에러', err);
});

setInterval(() => {
    throw new Error('서버를 고장내주마!!!!');
}, 1000);

setTimeout(() => {
    console.log('실행됩니다.')
}, 2000);
  • 최후의 수단으로 사용해야 함. (콜백 함수의 동작이 보장되지 않음, 따라서 복구 작업용으로 쓰는 것은 부적합하다. 에러 내용 기록 용으로만 사용 하는게 좋음)
  • 프로세스 종료 방법은 아래와 같다.
# Windows
$ netstat -ano | findstr 포트		// 노드 서버가 몇번 포트를 사용하고 있는지 알아내는 명령어.
$ taskkill /pid 프로세스아이디 /f



# 맥/리눅스
$ lsof -i tcp:포트
$ kill -9 프로세스아이디
반응형
반응형

# fs (파일 시스템 모듈)

 

File system | Node.js v14.18.0 Documentation

 

nodejs.org

  • 파일 시스템에 접근하는 모듈
  • 파일/폴더 생성, 삭제, 읽기, 쓰기 가능
  • 웹 브라우저에서는 제한적이었으나 노드는 권한을 가지고 있음
  • 아래는 파일을 읽는 예제들.
# readfile.js
const fs = require('fs');

fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log(data);
    console.log(data.toString());
});


# readme.txt
저를 읽어 주세요.


출력 결과
<Buffer ec a0 80 eb a5 bc 20 ec 9d bd ec 96 b4 ec a3 bc ec 84 b8 ec 9a 94 2e>
저를 읽어주세요.
  • fs.promise 사용 예
const fs = require('fs').promises;

fs.readFile('./readme.txt')
    .then((data) => {
        console.log(data);
        console.log(data.toString()); 
    })
    .catch((err) => {
        throw err;
    });
    
    
출력 결과
<Buffer ec a0 80 eb a5 bc 20 ec 9d bd ec 96 b4 ec a3 bc ec 84 b8 ec 9a 94 2e>
저를 읽어주세요.
  • 아래는 파일을 생성하는 예제들.
# writefile.js
const fs = require('fs').promises;

fs.writeFile('./writeme.txt', '글이 입력됩니다.')
    .then(() => {
        return fs.readFile('./writeme.txt');
    })
    .then((data) => {
        console.log(data.toString());
    })
    .catch((err) => {
        throw err;
    });
    
    
출력 결과
writeme.txt 파일 생성과 함께 아래 문구 출력.
글이 입력됩니다.
  • 콜백이나 프로미스는 대부분의 경우 비동기라 순서대로 처리되지 않음. (매 번 순서가 다르게 실행됨.)
# async.js
const fs = require('fs');

fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('1번', data.toString());
});
fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('2번', data.toString());
});
fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('3번', data.toString());
});
fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('4번', data.toString());
});



출력 결과 (변동 됨)
2번 저를 읽어주세요.
3번 저를 읽어주세요.
1번 저를 읽어주세요.
4번 저를 읽어주세요.
  • 아래와 같이 동기 방식으로도 가능. (동시에 돌릴수 없어서 비 효율적_실행 시 다른 작업(다른 사용자)들은 기다려야함. / 딱 한번 실행 or 서버 초기화 작업 시 사용)
# sync.js
const fs = require('fs');

let data = fs.readFileSync('./readme.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./readme.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./readme.txt');
console.log('3번', data.toString());
data = fs.readFileSync('./readme.txt');
console.log('4번', data.toString());

출력 결과
1번 저를 읽어주세요.
2번 저를 읽어주세요.
3번 저를 읽어주세요.
4번 저를 읽어주세요.
  • 가능하면 비 동기작업을 하면서 순서를 유지하는걸 추천함. (콜백 헬 발생 가능성 존재.)
# async.js
const fs = require('fs');

fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('1번', data.toString());
    fs.readFile('./readme.txt', (err, data) => {
        if (err) {
            throw err;
        }
        console.log('2번', data.toString());
        fs.readFile('./readme.txt', (err, data) => {
            if (err) {
                throw err;
            }
            console.log('3번', data.toString());
            fs.readFile('./readme.txt', (err, data) => {
                if (err) {
                    throw err;
                }
                console.log('4번', data.toString());
            });
        });
    });
});


출력 결과
1번 저를 읽어주세요.
2번 저를 읽어주세요.
3번 저를 읽어주세요.
4번 저를 읽어주세요.
  • 콜백 헬 해결을 위해 프로미스 사용.
const fs = require('fs').promises;

async function main() {
    let data = await fs.readFile('./readme.txt');
    console.log('1번', data.toString());
    data = await fs.readFile('./readme.txt');
    console.log('2번', data.toString());
    data = await fs.readFile('./readme.txt');
    console.log('3번', data.toString());
    data = await fs.readFile('./readme.txt');
    console.log('4번', data.toString());
}
main();
  • 비동기로 하되 순서를 지키는게 순서를 지키면서 동시성도 지키는 좋은 방법이다.
  • sync는 편하지만 실제로 서버에서 사용 시 문제 발생 가능성 존재.

 

# 버퍼와 스트림 

  • 버퍼 : 일정한 크기로 모아두는 데이터. 
일정한 크기가 되면 한 번에 처리.
버퍼링 : 버퍼에 데이터가 찰 때까지 모으는 작업.
  • 스트림 : 데이터의 흐름
일정한 크기로 나눠서 여러 번에 걸쳐서 처리.
버퍼(또는 청크)의 크기를 작게 만들어서 주기적으로 데이터를 전달.
스트리밍 : 일정한 크기의 데이터를 지속적으로 전달하는 작업.
  • 노드에서는 Buffer 객체 사용.
# buffer.js
const buffer = Buffer.from('저를 버퍼로 바꿔보세요.');
console.log(buffer);
console.log(buffer.length);
console.log(buffer.toString());


출력 결과
<Buffer ec a0 80 eb a5 bc 20 eb b2 84 ed 8d bc eb a1 9c 20 eb b0 94 ea bf 94 eb b3 b4 ec 84 b8 ec 9a 94 2e>
33
저를 버퍼로 바꿔보세요.
const buffer = Buffer.from('저를 버퍼로 바꿔보세요.');
console.log(buffer);
console.log(buffer.length);
console.log(buffer.toString());

const array = [Buffer.from('띄엄 '), Buffer.from('띄엄 '), Buffer.from('띄어쓰기 ')];
console.log(Buffer.concat(array).toString());


출력 결과
<Buffer ec a0 80 eb a5 bc 20 eb b2 84 ed 8d bc eb a1 9c 20 eb b0 94 ea bf 94 eb b3 b4 ec 84 b8 ec 9a 94 2e>
33
저를 버퍼로 바꿔보세요.
띄엄 띄엄 띄어쓰기
  • 빈 버퍼 생성
console.log('-----Buffer.alloc()-----');
console.log(Buffer.alloc(5));
  • 나눠서 처리 후 최종적으로 모아주기. (Stream은 동시에 오는 것이 아닌, 순서대로 진행 됨)
# createReadStream.js
const fs = require('fs');
const readStream = fs.createReadStream('./readme3.txt');

const data = [];
readStream.on('data', (chunk) => {
    data.push(chunk);
    console.log('data', chunk, chunk.length);
});
readStream.on('end', () => {
    console.log('end : ', Buffer.concat(data).toString())
});


# readme3.txt
저는 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
안녕하세요. 헬로 노드 헬로 스트림 헬로 버퍼.


출력 결과
data <Buffer ec a0 80 eb 8a 94 20 ec a1 b0 ea b8 88 ec 94 a9 20 eb 82 98 eb 88 a0 ec 84 9c 20 ec a0 84 eb 8b ac eb 90 a9 eb 8b 88 eb 8b 
a4 2e 20 eb 82 98 eb 88 a0 ... 103 more bytes> 153
end :  저는 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
안녕하세요. 헬로 노드 헬로 스트림 헬로 버퍼.
  • 비동기는 에러처리 꼭 해줘야 함.
const fs = require('fs');
const readStream = fs.createReadStream('./readme3.txt', { highWaterMark : 16 });

const data = [];
readStream.on('data', (chunk) => {
    data.push(chunk);
    console.log('data', chunk, chunk.length);
});
readStream.on('end', () => {
    console.log('end : ', Buffer.concat(data).toString())
});
readStream.on('error', (err) => {
    console.log('error : ', err)
});



출력 결과
data <Buffer ec a0 80 eb 8a 94 20 ec a1 b0 ea b8 88 ec 94 a9> 16
data <Buffer 20 eb 82 98 eb 88 a0 ec 84 9c 20 ec a0 84 eb 8b> 16
data <Buffer ac eb 90 a9 eb 8b 88 eb 8b a4 2e 20 eb 82 98 eb> 16
data <Buffer 88 a0 ec a7 84 20 ec a1 b0 ea b0 81 ec 9d 84 20> 16
data <Buffer 63 68 75 6e 6b eb 9d bc ea b3 a0 20 eb b6 80 eb> 16
data <Buffer a6 85 eb 8b 88 eb 8b a4 2e 0d 0a ec 95 88 eb 85> 16
data <Buffer 95 ed 95 98 ec 84 b8 ec 9a 94 2e 20 ed 97 ac eb> 16
data <Buffer a1 9c 20 eb 85 b8 eb 93 9c 20 ed 97 ac eb a1 9c> 16
data <Buffer 20 ec 8a a4 ed 8a b8 eb a6 bc 20 ed 97 ac eb a1> 16
data <Buffer 9c 20 eb b2 84 ed 8d bc 2e> 9
end :  저는 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
안녕하세요. 헬로 노드 헬로 스트림 헬로 버퍼.
  • 스트림 방식을 사용하게 되면 메모리를 아낄 수 있어서 좋음.
  • 쓰기 할때도 메모리 효율적.
# createWriteStream.js
const fs = require('fs');

const writeStream = fs.createWriteStream('./writeme2.txt');
writeStream.on('finish', () => {
    console.log('파일 쓰기 완료');
});

writeStream.write('이 글을 씁니다.\n');
writeStream.write('한 번 더 씁니다.');
writeStream.end();

## pipe와 스트림 메모리 효율 확인

  • pipe 이용 파일 복사
const fs = require('fs');

const readStream = fs.createReadStream('./readme3.txt', { highwaterMark : 16 });
const writeStream = fs.createWriteStream('./writeme3.txt');
readStream.pipe(writeStream);
  • pipe 이용 압축 스트리밍
const fs = require('fs');
const zlib = require('zlib');

const readStream = fs.createReadStream('./readme3.txt', { highwaterMark : 16 });
const zlibStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./writeme3.txt');
readStream.pipe(zlibStream).pipe(writeStream);
  • 큰 파일 만드는 예.
const fs = require('fs');
const file = fs.createWriteStream('./big.txt');

for (let i = 0; i <= 10_000_000; i++) {
    file.write('안녕하세요. 엄청나게 큰 파일을 만들어 볼 것입니다. 각오 단단히 하세요!\n');
}
file.end();
  • 큰 파일 만들어서 메모리 비교 (버퍼 방식 / 스트림 방식 비교)
# buffer-memory.js
const fs = require('fs');

console.log('before : ', process.memoryUsage().rss);

const data1 = fs.readFileSync('./big.txt');
fs.writeFileSync('./big2.txt', data1);
console.log('buffer : ', process.memoryUsage().rss);


출력 결과
before :  19705856
buffer :  1020694528




# stream-memory.js
const fs = require('fs');

console.log('before : ', process.memoryUsage().rss);

const readStream = fs.readFileSync('./big.txt');
const writeStream = fs.createWriteStream('./big3.txt');
readStream.pipe(writeStream);
readStream.on('end', () => {
    console.log('stream : ', process.memoryUsage().rss);
});


출력 결과
before :  19677184
stream :  29220864

## 파일 및 폴더 생성

const fs = require('fs').promises;
const constants = require('fs').constants;

fs.access('./folder', constants.F_OK | constants.W_OK | constants.R_OK)
    .then(() => {
        return Promise.reject('이미 폴더가 존재합니다.');
    })
    .catch((err) => {
        if (err.code === 'ENOENT') {
            console.log('폴더 없음');
            return fs.mkdir('./folder');
        }
        return Promise.reject(err);
    })
    .then(() => {
        console.log('폴더 만들기 성공');
        return fs.open('./folder/file.js', 'w');
    })
    .then((fd) => {
        console.log('빈 파일 만들기 성공 ', fd);
        fs.rename('./folder/file.js', './folder/newfile.js');
    })
    .catch((err) => {
        console.error(err);
    });
  • fs.access(경로, 옵션, 콜백) : 폴더나 파일에 접근할 수  있는지 체크함. 두 번째 인자로 상수들을 넣음. F_OK는 파일 존재 여부 / R_OK는 읽기 권한 여부 / W_OK는 쓰기 권한 여부를 체크한다. 파일/폴더나 권한이 없다면 에러가 발생, 파일/폴더가 없을 때의 에러 코드는 ENOENT로 설정.
  • fs.mkdir(경로, 콜백) : 폴더를 만든느 메서드. 이미 폴더가 있다면 에러가 발생하므로 먼저 access() 메서드를 호출해서 확인하는 것이 중요.
  • fs.open(경로, 옵션, 콜백) : 파일의 아이디(fd 변수)를 가져오는 메서드. 파일이 없다면 파일을 생성한 뒤 그 아이디를 가져온다. 가져온 아이디를 사용해 fs.read()나 fs.write()로 읽거나 쓸 수 있다. 두 번째 인자로 어떤 동작을 할 것인지 설정할 수 있다. 쓰려면 w, 읽으려면 r, 기존 파일에 추가하려면 a로 설정. 예제에서는 w로 설정했으므로 파일이 없을 때 새로 만들 수 있었음. r이었다면 에러가 발생하였을 것임.
  • fs.rename(기존 경로, 새 경로, 콜백) : 파일의 이름을 바꾸는 메서드. 기존 파일의 위치와 새로운 파일 위치를 적어주면 된다. 반드시 같은 폴더를 지정할 필요는 없으며 잘라내기 같은 기능을 할 수도 있다.

## 폴더 내용 확인 및 삭제

  • fs.readdir(경로, 콜백) : 폴더 안의 내용물을 확인할 수 있다. 배열 안에 내부 파일과 폴더명이 나온다.
  • fs.unlink(경로, 콜백) : 파일을 지울 수 있다. 파일이 없다면 에러가 발생하므로 먼저 파일이 있는지를 꼭 확인해야 한다.
  • fs.rmdir(경로, 콜백) : 폴더를 지울 수 있다. 폴더 안에 파일이 있다면 에러가 발생하므로 먼저 내부 파일을 모두 지우고 호출해야 한다.
const fs = require('fs').promises;

fs.readdir('./folder')
    .then((dir) => {
        console.log('폴더 내용 확인 ', dir);
        return fs.unlink('./folder/newFile.js');
    })
    .then(() => {
        console.log('파일 삭제 성공');
        return fs.rmdir('./folder');
    })
    .then(() => {
        console.log('폴더 삭제 성공');
    })
    .catch((err) => {
        console.error(err);
    });

## 파일을 복사하는 방법

const fs = require('fs').promises;

fs.copyFile('readme4.txt', 'writeme4.txt')
    .then(() => {
        console.log('복사 완료');
    })
    .catch((error) => {
        console.error(error);
    })

## 파일을 감시하는 방법(변경 사항 발생 시 이벤트 호출)

  • target.txt 파일 바뀔 때 이벤트 호출하는 예.
const fs = require('fs');

fs.watch('./target.txt', (eventType, filename) => {
    console.log(eventType, filename);
});
반응형

+ Recent posts