바르고 뜨겁게

[NodeJS] 노드JS로 서버 만들기 본문

자바스크립트/Node Js

[NodeJS] 노드JS로 서버 만들기

RightHot 2018. 12. 27. 00:14

Node JS - 노드 서버 만들기

http 모듈, localhost, 포트

http 모듈은 선언된 js를 서버프로그램으로 만든다.

http 모듈이 서버 역활을 하고 node는 이걸 실행한다.

const http = require('http');

// http.createServer(요청/응답 콜백);
http.createServer((req, res)=>{
   console.log('server start');
   res.write('<h1>server1.js PAGE</h1>');
   res.write('<h3>write used multiple times</h3>');
   res.write('<h3>write used multiple times</h3>');
   res.end('<p>end</p>');

}).listen(8080, ()=>{
   console.log('8080 포트에서 서버 실행');
});

PS D:\포트폴리오\_Study\Inflearn_nodeJS-Textbook> node server1
8080 포트에서 서버 실행
server start



응답을 이용해 클라이언트로 HTML 파일 보내기

server2.html

<!DOCTYPE html>
<html>

<head>
   <meta charset="utf-8" />
   <title>Node.js 서버</title>
</head>
<body>
   <h1>노드JS 웹서버</h1>
   <p>HTML 삽입</p>
</body>

</html>

server1.js

const http = require('http');
const fs = require('fs');


// http.createServer(요청/응답 콜백);
const server = http.createServer((req, res)=>{
   console.log('server start');
   fs.readFile('./server2.html',(err,data)=>{
       if(err){
           throw err;
      }
       res.end(data); //data에 담긴 버퍼를 브라우저(클라이언트)로 요청을 보냄
  });

}).listen(8080);

// http 모듈에 이벤트 추가 가능
server.on('listening', ()=>{
   console.log('8080 포트에서 서버 실행');
});

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




클라이언트-서버 데이터 주고받기 (쿠키 사용)
// 쿠키 응답 (크롬 f12 - Application - Cookies 확인가능)

const http = require('http');
const fs = require('fs');

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


// http.createServer(요청/응답 콜백);
const server = http.createServer((req, res) => {
   console.log('요청한곳 : ',req.url);
   // 쿠키 정보 받기
   console.log(req.headers.cookie);
   // 문자열을 객체로 파싱
   console.log(parseCookies(req.headers.cookie));

   // res.writeHead(요청코드, 객체)
   // 객체 내용 : 'Set-Cookie' 쿠키사용설정 , '키=값' 으로 이루어져있음
   res.writeHead(200, { 'Set-Cookie': 'mycookie=test' });
   res.end('cooke end');

}).listen(8080);

// http 모듈에 이벤트 추가 가능
server.on('listening', () => {
   console.log('8080 포트에서 서버 실행');
});

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

// 로컬호스트 요청이라서 / 로 뜸
요청한곳 :  /      
_ga=GA1.1.517848530.1529337539; mycookie=test
{ _ga: 'GA1.1.517848530.1529337539', mycookie: 'test' }

// 브라우저가 파비콘 받아오기 위해한 요청
요청한곳 :  /favicon.ico
_ga=GA1.1.517848530.1529337539; mycookie=test
{ _ga: 'GA1.1.517848530.1529337539', mycookie: 'test' }

헤더에 Set-Cookie를 보내면 브라우저는 쿠키를 저장한다.

라우터 분기 처리와 쿠키

server1.js

const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');


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


// http.createServer(요청/응답 콜백);
const server = http.createServer((req, res) => {
   console.log('요청한곳 : ', req.url);
   console.log('쿠키정보 : ', 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);  // Expires 쿠키유효시간

       res.writeHead(302, {
           Location: '/',
           'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; path=/`
      });
       console.log(name);
       res.end(name);

  } else if(cookies.name){
       res.writeHead(200,{ 'Content-Type' : 'text/html; charset-utf-8'});
       res.end(`${cookies.name}님 안녕하세요`)

  }else {
       fs.readFile('./server4.html', (err, data) => {
           res.end(data);
      });
  }

}).listen(8080);

// http 모듈에 이벤트 추가 가능
server.on('listening', () => {
   console.log('8080 포트에서 서버 실행');
});

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

server4.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>


요청한곳 :  /login?name=RightHot
쿠키정보 : { _ga: 'GA1.1.517848530.1529337539', mycookie: 'test' }
RightHot
요청한곳 : /favicon.ico
쿠키정보 : { _ga: 'GA1.1.517848530.1529337539', mycookie: 'test' }



다른 페이지로 이동

        res.writeHead(302, {
           Location: '/',
           'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; path=/`
      });


메모리 세션

쿠키는 쿠키안에 데이터를 그대로 사용하지만

아래 예제는 세션의 아이디만 쿠키에 저장하고 데이터는 브라우저의 정보를 사용한다.

const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');


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

const session = {

};

// http.createServer(요청/응답 콜백);
const server = http.createServer((req, res) => {
   const cookies = parseCookies(req.headers.cookie);
   console.log('요청한곳 : ', req.url);
   console.log('쿠키정보 : ', cookies);

   // 요청 주소에 따라 분기
   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);  // Expires 쿠키유효시간
       const randomInt = +new Date(); // 세션 객체에 랜덤값을 넣는다
       session[randomInt] ={
           name,
           expires
      }

       res.writeHead(302, {
           Location: '/',
           'Set-Cookie': `session=${randomInt}; Expires=${expires.toGMTString()}; HttpOnly; path=/`
      });
       console.log(name);
       res.end(name);

  } else if(cookies.session && session[cookies.session] && session[cookies.session].expires > new Date()){ // 쿠키확인, 쿠키의 유효기간 확인
       res.writeHead(200,{ 'Content-Type' : 'text/html; charset=utf-8'});
       res.end(`${session[cookies.session].name}님 안녕하세요`)

  }else {
       fs.readFile('./server4.html', (err, data) => {
           res.end(data);
      });
  }

}).listen(8080);

// http 모듈에 이벤트 추가 가능
server.on('listening', () => {
   console.log('8080 포트에서 서버 실행');
});

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

REST API 개념 - 간단한 노드 서버 만들기

서버의 자원을 주소를 통해서 가져오는 구조적인 방법

GET 가져오기 POST 등록하기 PUT 전체 변경 PATCH 일부 변경 DELETE 삭제

위와 같은 메서드는 req.method에 담겨있다.

server1.js

const http = require('http');
const fs = require('fs');


// DB가 없어서 임시로 메모리에 저장
const users = {

};

http.createServer((req,res)=>{
   if(req.method === 'GET'){
       if(req.url === '/'){
           return fs.readFile('./restFront.html', (err,data)=>{
               if(err){
                   throw err;
              }
               res.end(data);
          })

      }else if(req.url === '/users'){
           return res.end(JSON.stringify(users)); // 객체라서 객체형식을 못보내기때문에 JSON 형태로 변경
      }

       // 요청한 url을 보내준다 (css, js 등등... -정적파일 처리)
       return fs.readFile(`.${req.url}`,(err,data)=>{
           return res.end(data);
      });

  }else if(req.method === 'POST'){
       if(req.url === '/'){

      }else if(req.url === '/users'){
           let body ='';
           req.on('data', (chunk)=>{ // 데이터가 버퍼로 들어옴
               body += chunk
          });
           return req.on('end', ()=>{
               console.log('POST 본문(body)', body);
               //const name = JSON.parse(body).name; 아래는 비구조화 할당
               const { name } = JSON.parse(body);
               
               const id = +new Date(); // 랜덤 키 생성
               users[id] = name;
               res.writeHead(201, { 'Content-Type': 'text/html; charset=utf-8'});
               res.end('사용자 등록 완료')

          });

      }

  }else if(req.method === 'PATCH'){
       if(req.url === '/'){

      }else if(req.url === '/users'){
   
      }

  }else if(req.method === 'PUT'){
       if(req.url === '/'){

      }else if(req.url.startsWith('/users/')){ // (/users/67) URL안에 값이 있어서 뒤에 나오는 값은 바뀔수도있으니 이렇게 모두 받는다
           const id = req.url.split('/')[2];
           let body ='';
           req.on('data', (chunk)=>{ // 데이터가 버퍼로 들어옴
               body += chunk
          });
           return req.on('end', ()=>{
               console.log('put ', body);
               users[id] = JSON.parse(body).name;
               return res.end(JSON.stringify(users)); // 클라이언트로 응답
          });
      }

  }else if(req.method === 'DELETE'){
       if(req.url === '/'){

      }else if(req.url.startsWith('/users/')){ // (/users/67) URL안에 값이 있어서 뒤에 나오는 값은 바뀔수도있으니 이렇게 모두 받는다
           const id = req.url.split('/')[2];
           let body ='';
           req.on('data', (chunk)=>{ // 데이터가 버퍼로 들어옴
               body += chunk
          });
           return req.on('end', ()=>{
               console.log('delete', body);
               delete users[id];
               return res.end(JSON.stringify(users)); // 클라이언트로 응답
          });
      }

  }

}).listen(8080, ()=>{
   console.log('8080 포트에서 서버 실행중')
});

restFront.js

function getUser() { // 로딩 시 사용자 가져오는 함수
   var xhr = new XMLHttpRequest();
   xhr.onload = function () {
     if (xhr.status === 200) {
       var users = JSON.parse(xhr.responseText);
       var list = document.getElementById('list');
       list.innerHTML = '';
       Object.keys(users).map(function (key) {
         var userDiv = document.createElement('div');
         var span = document.createElement('span');
         span.textContent = users[key];
       
         var edit = document.createElement('button');
         edit.textContent = '수정';
         edit.addEventListener('click', function () { // 수정 버튼 클릭
           var name = prompt('바꿀 이름을 입력하세요');
           if (!name) {
             return alert('이름을 반드시 입력하셔야 합니다');
          }
           var xhr = new XMLHttpRequest();
           xhr.onload = function () {
             if (xhr.status === 200) {
               console.log(xhr.responseText);
               getUser();
            } else {
               console.error(xhr.responseText);
            }
          };
           xhr.open('PUT', '/users/' + key);
           xhr.setRequestHeader('Content-Type', 'application/json');
           xhr.send(JSON.stringify({ name: name }));
        });
         
         var remove = document.createElement('button');
         remove.textContent = '삭제';
         remove.addEventListener('click', function () { // 삭제 버튼 클릭
           var xhr = new XMLHttpRequest();
           xhr.onload = function () {
             if (xhr.status === 200) {
               console.log(xhr.responseText);
               getUser();
            } else {
               console.error(xhr.responseText);
            }
          };
           xhr.open('DELETE', '/users/' + key);
           xhr.send();
        });
         userDiv.appendChild(span);
         userDiv.appendChild(edit);
         userDiv.appendChild(remove);
         list.appendChild(userDiv);
      });
    } else {
       console.error(xhr.responseText);
    }
  };
   xhr.open('GET', '/users');
   xhr.send();
}
 window.onload = getUser; // 로딩 시 getUser 호출
 // 폼 제출
 document.getElementById('form').addEventListener('submit', function (e) {
   e.preventDefault();
   var name = e.target.username.value;
   if (!name) {
     return alert('이름을 입력하세요');
  }

   // AJAX 요청
   var xhr = new XMLHttpRequest();

   // 서버 응답 콜백
   xhr.onload = function () {
     if (xhr.status === 201) {
       console.log(xhr.responseText);
       getUser();
    } else {
       console.error(xhr.responseText);
    }
  };

   // 서버 요청 방식, 경로
   xhr.open('POST', '/users');
   xhr.setRequestHeader('Content-Type', 'application/json');
   xhr.send(JSON.stringify({ name: name })); // 전송되는 데이터
   e.target.username.value = '';
});

restFront.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>
   <form id="form">
       <input type="text" id="username">
       <button type="submit">등록</button>
   </form>
</div>
<div id="list"></div>
<script src="./restFront.js"></script>
</body>
</html>


위에 코드 리팩토링 (라우터)
  • 보호연산자 : ( 첫번째값 && 두번째값 ) router && router[req.method] && router[req.method][req.url] ... undefind가 아니면 다음걸 계속 실행 아이면 이전값 실행

  • 기본값연산자 : ( 첫번째값 || 두번째값 ) 첫번째값이 undefind면 두번째값이 실행

const http = require('http');
const fs = require('fs');


// DB가 없어서 임시로 메모리에 저장
const users = {

};

// 프레임워크를 사용하지 않을때 리팩토링 방법
const router = {
   GET: {
       '/': (req, res) => {
           fs.readFile('./restFront.html', (err, data) => {
               if (err) {
                   throw err;
              }
               res.end(data);
          });
      },
       '/users': (req, res) => {
           res.end(JSON.stringify(users));
      },
       '*': (req, res) => {
           return fs.readFile(`.${req.url}`, (err, data) => {
               return res.end(data);
          });
      }
  },
   POST: {
       '/users': (req, res) => {
           let body = '';
           req.on('data', (chunk) => { // 데이터가 버퍼로 들어옴
               body += chunk
          });
           return req.on('end', () => {
               console.log('POST 본문(body)', body);
               //const name = JSON.parse(body).name; 아래는 비구조화 할당
               const { name } = JSON.parse(body);

               const id = +new Date(); // 랜덤 키 생성
               users[id] = name;
               res.writeHead(201, { 'Content-Type': 'text/html; charset=utf-8' });
               res.end('사용자 등록 완료')

          });
      }
  },
   PATCH: {

  },
   PUT: {
       '/users': (req, res) => {
           const id = req.url.split('/')[2];
           let body = '';
           req.on('data', (chunk) => { // 데이터가 버퍼로 들어옴
               body += chunk
          });
           return req.on('end', () => {
               console.log('put ', body);
               users[id] = JSON.parse(body).name;
               return res.end(JSON.stringify(users)); // 클라이언트로 응답
          });
      }
  },
   DELETE: {
       'users': (req, res) => {
           const id = req.url.split('/')[2];
           let body = '';
           req.on('data', (chunk) => { // 데이터가 버퍼로 들어옴
               body += chunk
          });
           return req.on('end', () => {
               console.log('delete', body);
               delete users[id];
               return res.end(JSON.stringify(users)); // 클라이언트로 응답
          });
      },
  }
}

http.createServer((req, res) => {
   const matchedUrl = router[req.method][req.url];

   // matchedUrl에 값이 있으면 그대로 사용, req.url에 값이 없어서 undefind면 router[req.method]['*'] 사용
  (matchedUrl || router[req.method]['*'])(req, res);
}).listen(8080, () => {
   console.log('8080 포트에서 서버 실행중')
});


https

https

https는 암호화 통신이 때문에 인증서가 필요하다.

  • letsencrypt (무료)등에서 발급

const http = require('http');
const https = require('https');

https.createServer({
   cert: fs.readFileSync('도메인 인증서 경로'),
   key: fs.readFileSync('도메인 비밀키 경로'),
   ca: [
       fs.readFileSync('상위 인증서 경로'),
       fs.readFileSync('상위 인증서 경로')
  ],
},(req,res)=>{
   res.end('https server');
}).listen(443);


cluster 멀티 프로세싱

노드는 싱글 스레드이기 때문에 CPU 코어를 모두 활용하지 못함.

그래서 서버를 여러개 만드는 개념의 CLUSTER를 사용

cluster에는 masterworker 프로세스가 존재.

master는 cpu개수만큼 워커를 만든다.

worker는 서버를 돌린다.

 const http = require('http');
const os = require('os');
const cluster = require('cluster');
const numCPUs = os.cpus().length;

if(cluster.isMaster){
   console.log('마스터 프로세스 아이디: ', process.pid);
   for(let i=0; i < numCPUs; i += 1){
       cluster.fork(); // 워커 만들기
  }

   // 워커가 종료되면 발생되는 이벤트
   cluster.on('exit',(worker, code, siqnal)=>{
       console.log(worker.process.pid, ' 워커 종료 - 새로 fork로 워커를 만듭니다.');
       cluster.fork();
  })
}else{
   console.log('워커 프로세스 아이디: ', process.pid);
   http.createServer((req,res)=>{
       res.end('http server');
  }).listen(8080);
}
마스터 프로세스 아이디:  6004
워커 프로세스 아이디:  22372
워커 프로세스 아이디:  9676
워커 프로세스 아이디:  31268
워커 프로세스 아이디:  34712
워커 프로세스 아이디:  26344
워커 프로세스 아이디:  9864
워커 프로세스 아이디:  8992
워커 프로세스 아이디:  3740
워커 프로세스 아이디:  2984
워커 프로세스 아이디:  15848
워커 프로세스 아이디:  13304
워커 프로세스 아이디:  37796
워커 프로세스 아이디:  17340
워커 프로세스 아이디:  16108
워커 프로세스 아이디:  32124
워커 프로세스 아이디:  29916

Comments