4장 http 모듈로 서버 만들기

마조리카·2021년 4월 8일
0

4.1 요청과 응답 이해하기

클라이언트에서 서버로 요청을 보내고 서버는 처리 뒤 응답합니다.
요청과 응답은 이벤트 방식이라고 생각하면 된다.
서버는 클라이언트로부터 요청이 왔을 떄 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해두어야 합니다.
const http = require('http');

htpp.createSever((req, res) => {
  // 응답
});
http 서버가 있어야 요청을 처리할 수 있으므로 http 모듈 사용
http 모듈에는 createSever 메서드가 있다.
인수로 콜백 함수를 넣을 수 있으며, 요청이 들어올 떄마다 매번 콜백 함수를 실행한다.
매개변수로는 req, res가 있으며 각각 요청에 대한정보, 응답에 대한 정보들을 갖고있다.
const http = require('http');

htpp.createSever((req, res) => {
  res.writeHead(200, { 'Content-type' : 'text/html; charset=utf-8' });
  res.write('<h1>hello Node!</h1>');
  res.end('<p?hello Server!</p>');
  })
	.listen(8080, () => {
  		console.log( '8080번 포트에서 서버 대기 중입니다!'); 
});
createSever 메서드 뒤에 listen 메서드를 붙이고 클라이언트에 공개할 포트번호와 포트 연결 완료 후 실행될 콜백 함수를 넣습니다. 이 파일을 실행하면 서버는 포트에서 요천이 오기를 기다립니다.
res 객체에는 res.writeHead와 res.write, res.end 메서드가 있습니다. res.writeHead는 응답에 대한 정보를 기록하는 메서드 입니다.
첫번째 인수임을 의미하는 200을, 두번째 인수로는 응답에 대한 정보를 내 보내는데 콘텐츠의 형식이 HTML임을 알리고 있습니다. 또한 한글 표시를 위해 charshe을 utf-8로 지정했습니다. 이정보가 기록되는 부분을 헤더라고 부릅니다.

res.write 메서드의 첫 번째 인수는 클라이언트로 보낼 데이터 입니다.
지금은 HTML 모양의 문자열을 보내지만 버퍼를 보낼수도 있습니다. 또한 여러번 호출해서 데이터를 여러개 보내도 됩니다.
데이터가 기록되는 부분은 본문(BODY)라고 부릅니다.

res.end는 응답을 종료하는 메서드입니다. 만약 인수가 있다면 그 데이터도 클라이언트로 보내고 응답을 종료합니다.

listen 메서드에 콜백 함수를 넣는 대신 다음과 같이 서버에 listening이벤트 리스너를 부터여 됩니다.. 추가로 erroe 이벤트 리스너도 붙여봤습니다.
const http = require('http');

htpp.createSever((req, res) => {
	res.writeHead(200, { 'Content-type' : 'text/html; charset=utf-8' });
	res.write('<h1>hello Node!</h1>');
	res.end('<p?hello Server!</p>');
});
server.listen(8080)
 
server.on('listening', () => {
	console. log( '8080번 포트에서 서버 대기 중입니다!'); 
});
sever.on('error', (error) => {
	console.error(error)'
});
한번에 여러 서버를 실행할 수 있습니다(포트는 다르게). createServer를 원하느 만큼 호출하면 됩니다.

res.write와 res.end에 일일이 html을 적는 것은 비효율적이니 미리 html 파일을 만들어 두는면이 좋습니다.
그 html 파일을 fs모듈로 읽어서 전송합니다.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Node.js 웹 서버</title>
  </head>
  <body>
    <h1>Node.js 웹 서버</h1>
    <p>만들 준비 되셨나요?</p>
  </body>
</html>
const http = require('http');
const fs = require('fs').promises;

http.createSever(async (req, res) => {
	try {
		const data = await fs.readFile('/server2.html');
		res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8'});
		res.end(data);
}	catch (err) {
console.error(err);
		ers,writeHeda(500, { ' Content-Type' : 'text/html; charset=utf-8'});
res.end(err.aessage);
}
})
.listne(8081, () => {
	console.log('8081번 포트에서 서버 대기중입니다.
});

4.2 REST와 라우팅 사용하기

REST - 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킵니다. 일족의 약속

GET : 서버 자원을 가져오고자 할 때 사용합니다. 요청의 본문에 데이터를 넣지 않습니다. 데이터를 서버로 보내야 한다면 쿼리 스트링을 사용합니다.
POST : 서버에 자원을 새로 등록하고자 할 때 사용합니다. 요청의 본문에 새로 등록할 데이터를 넣어 보냅니다.
PUT : 서버의 자원을 요청에 들어 있는 자원으로 치원하고자 할 때 사용합니다. 요청의 본문에 치환할 데이터를 넣어 보냅니다.
PATCH : 서버 자원의 일부만 수정하고자 할 때 사용합니다. 요청의 본문에 일부 수정할 데이터를 넣어 보냅니다.
DELETE : 서버의 자원을 삭제하고자 할 때 사용합니다. 요청의 본문에 데이터를 넣지 않습니다.
OPTION : 요청을 하기 전에 통신 옵션을 설명하기 위해 사용합니다. 12장에서 자주 보게 될 것입니다.

주소와 메서드만 보고 요청의 내용을 알아볼 수 있다는 장점이 있다. 또한, get 메서드 같은 경우에는 브라우저에서 캐싱할 수 있으므로 같은 주소로 get요청을 할 때 서버에서 가져오는것이 아니라 캐시에서 가져올 수도 있습니다. 캐싱하면 성능이 좋아집니다.
restFront.css
a { color: blue; text-decoration: none; }
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>
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 = '';
});

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>
 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번 포트에서 서버 대기 중입니다');
 });
restSever.js 가 핵심입니다. 코드를 보면 req.method로 http 요청 메서드를 구분하고 있습니다 . 메서드가 GET이면 다시 req.url로 요청 주소를 구분합니다. 주소가 /일 때 는 restFront.html을 제공하고, 주소가 /about이면 about.html 파일을 제공합니다. 이외의 경우에는 주소에 적힌 restFront.css 파일을 제공합니다. 만약 존재하지 않는 파일을 요청하거나 GET 메서드 요청이 아닌 경우라면 404 NOT FOUND 에러가 응답으로 전송됩니다. 응답 과정 중에 예기치 못한 에러가 발생한 경우에는 500 에러가 응답으로 전송됩니다.( 이경우는 매우 적다.)

왜 res.end 앞에 return은 왜붙을까? - res.end한다고 함수가 종료되는것이 아니다. return해야지 종료된다.

브라우저 F12, Network 텝에서 네트워크 요청 내용을 실시간으로 볼 수 있습니다. REST 방식으로 주소를 만들었으므로 주소와 메서드만 봐도 요청 내용을 유추할 수 있습니다. Name은 요청 주소를, Method는 요청 메서드를, Status는 HTTP 응답 코드를, Protocol은 통신 프로토콜을, Type은 요청의 종류를 의미합니다. xhr은 AJAX 요청입니다.

Network 탭에서 POST /users는 사용자를 등록하는 요청임을 알 수 있습니다. DELETE /users/1505550586127은 해당 키를 가진 사용자를 제거하는 요청입니다. 등록, 수정, 삭제가 발생 할 대마다 GET /users로 갱싱된 사용자 정보를 가져올수 있습니다.

4.3 쿠키와 세션 이해하기

클라이언트에 보내는 요청에는 한 가지 단점이 있다. 요청을 누가 보내는지 모른다는 것이다. 그래서 로그인을 구현해야 하는데 로그인 할때 쿠키와 세션을 사용한다. 새로고침을 해도 로그인이 풀리지 않는것은 이때문이다.

서버는 요청에 대한 응답을 할 때 쿠키라는 것을 같이 보냅니다.
쿠키는 유효기간을 갖고 있으며 name=lee와 같은 단순한 키-값 쌍입니다.
서버로부터 쿠키가 오면 웹 브라우저는 쿠키를 저장해두었다가 다음에 요청을 할때 마다 쿠키를 동봉해서 보냅니다.
서버는 요청에 들어 있는 쿠키를 읽어서 사용자가 누군지 파악할 수 있습니다.
즉, 서버는 미리 클라이언트에 요청자를 추정할 만한 정보를 쿠키로 만들어 보내고, 그 다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악합니다. 쿠키가 여러분이 누구인지 추적하고 있는 것입니다. 개인적보 유출방지를 위해 쿠키를 주기적으로 지우라는 권고가 이때문입니다.

쿠키는 요청에 헤더(Cookie)에 담겨 전송됩니다. 브라우저는 응답의 헤더(Set_Cookie)에 따라 쿠키를 저장합니다
cookie.js

const hppt = require('http');

http.creatSever((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 포트에서 서버 대기중입니다!');
});
쿠키는 name=lee; year=1994처럼 문자열로 존재합니다. 쿠키 간에서 세미클론으로구분됩니다.
createServer 메서드의 콜잭에서는 req 객체에 담겨잇는 쿠키를 가져옵니다. 쿠키는 req.headers.cookie 에 들어 있습니다. req.headers는 요청의 헤더를 의미합니다.
응답의 헤더에 쿠키를 기록해야 하므로 res.writeHead 메서드를 사용합니다.
Set-Cookie는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미입니다. 실제로 응답반은 브라우저는 mycookie=test라는 쿠키를 저장합니다.

localhost:8083에 접속합니다. req.url과 req.headers.cookie에 대한 정보를 로깅하도록 합니다. req.url은 주소의 path와 serach부분을 알립니다.
만약 실행결과가 이상하다면 브라우저의 쿠키를 모두 제거한 후에 다시 실행해야합니다. 다른 사이트나 프로그램이 미리 쿠키를 넣어두었을 수 도 있기 때문입니다.

파비콘이란 웹사이트 텝에 보이는 이미지를 뜻합니다.
브라우저는 파비콘이 뭔지 HTML에서 유추할 수없으면 서버에 파비콘 정보에 대한 요청을 보냅니다.
예제는 HTML에 파비콘에 대한 정보를 넣어두지않았으므로 브라우저가 추가로 /favicon.ico를 요청한 것입니다.
요청 두 개를 통해 우리는 서버가 제대로 쿠키를 심었음을 확인할 수 있습니다. 첫 번째 요청 / 을 보내기 전에는 브라우저가 어떠한 쿠키정보도 가지고 있지 않습니다.
서버는 응답의 헤더에 mycookie=test라는 쿠키를 심으라고 브라우저에게 명령(Set-Cookie)했습니다.
따라서 브라우저는 쿠키를 심었고, 두번째 요청(/favicon.ico)의 헤더에 쿠키가 들어 있음을 확인할 수 있습니다.

다음 예제에서는 사용자를 식별하는 방법을 알아봅시다.

<!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>
onst 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' }
// 주소가 /login으로 시작하는 경우
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 (err) {
    res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(err.message);
  }
}
})
.listen(8084, () => {
  console.log('8084번 포트에서 서버 대기 중입니다!');
});
1. parseCookies함수
쿠키는 mycookie=test 같은 문자열입니다. 이를 쉽게 사용하기 위해서 자바스크립트 객채형식으로 바꾸는 함수입니다. 이 함수를 거치면 { mycookie: 'test' } 가 됩니다.

2.if(req.url.startsWith('login')) 부터 res.end();
주소가 /login으로 시작할 경우에는 url과 querystiring 모듈로 각각 주소와 주소에 딸려오는 query를 분석합니다. 그리고 쿠키의만료 시간도 지금으로부터 5분 뒤로 설정했습니다. 이제 302응답 코드, 리다이렉트 주소와 함께 쿠키를 헤더에 넣습니다. 브라우저는 이 응답코드를 보고 페이지를 해당 주소에 리다이렉트합니다. 헤더에는 한글을 설정할 수 없으므로 name 변수를 encodeURIComponent 메서드로 인코딩 했습니다. 또한 Set-Cookie의 값으로는 제한된 아스키 코드만 들어가야 하므로 줄바굼을 넣으면 안됩니다.

3. 그 외
그 외의 경우 (/로 접속했을 때 등) 먼저 쿠키가 있는지 없는지 확인합니다. 쿠키가 없다면 로그인할 수 있는 페이지를 보냅니다. 처음 방문한 경우에는 쿠키가 없으므로 cookie2, html이 전송됩니다. 쿠키가 있다면 로그인한 상태로 간주하여 인사말을 보냅니다.

쿠키를 설정할때 각종 옵션을 넣을 수 있으며, 옵션 사이에 ;를 써서 구분합니다.
쿠키에 들어가면 안되는 글자가 있는데, 대표적으로 한글과 줄바꿈입니다. 한글은 encodeURIComponent로 감싸 넣습니다.

- 쿠키명=쿠키값
Expires=날짜 : 만료 기간입니다. 이 기간이 지나면 쿠키가 제거됩니다. 기본값은 클라이언트가 종료될 때까지입니다.
max-age=초 : 날짜 대신 초를 입력합니다. expires보다 우선됩니다.
Domian=도메인명 : 쿠키가 전송될 도메인을 특정할 수있습니다.기본값은 현재 도메인 입니다.
Path=URL : 쿠키가 전송될 URL을 특정할 수있습니다.
Secure : HTTPS일 경우에만 쿠키가 전송됩니다
HttpOnly : 설정 시 자바스크립트에서 쿠키에 접근할 수없습니다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋습니다.
위 방식은 원하는대로 동작하지만 현재 Application 탭에서 보이는 것처럼 쿠키가 노출되어 있습니다. 또한, 쿠키가 조작될 위험도 있습니다. 따라서 개인정보는 쿠키에 넣어두는 것은 적절하지 못합니다. 아래와 같이 변경하여 서버가 사용자 정보를 관리하게 만듭시다.
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번 포트에서 서버 대기 중입니다!');
});
위위 코드와 달라진 부분이 있습니다. 쿠키에 이름을 담아서 보내는 대신, uniqueInt라는 숫자 값을 보냈습니다.
사용자의 이름과 만료 시간은 uniqueInt 속성명 아래에 있는 session이라는 객체에 대신 저장합니다.
서버에 사용자 정보를 저장하고 클라이어트와는 세션 아이디로만 소통합니다. 세션 아이디는 꼭 쿠키를 사용해서 주고받지 않아도 됩니다.
하지만 많은 웹 사이트가 쿠키를 사용합니다. 쿠키를 사용하는 방법이 가장 간단하기 때문입니다. 이 책에서도 쿠키를 사용해 세션 아이디를 주고받는 식으로 실습을 진행할 것입니다. 세션을 위해 사용하는 쿠키를 세션쿠키 라고 부릅니다.

실제 배포용 서버에는 세션을 위와 같이 변수에 저장하지 않습니다. 서버가 멈추거나 재시작되면 메모에 저장된 변수가 초기화 되기 때문입니다. 또한, 서버의 메모리가 부족하면 세션을 저장하지 못하는 문제도 생깁니다. 그래서 보통 레디스(Redis)나 멤캐시드(Memcached) 같은 데이터베이스에 넣어둡니다.

4.4 https와 http2

https 모듈은 웹 서버에 SSL 암호화를 추가합니다. GET이나 POST요청을 할 때 오가는 데이터를 암호화 해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없게 합니다. 요즘은 로그인이나 결제가 필요한 창에서 https 적용이 필수가 되는 추세입니다.
const http = require('http');

http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Server!</p>');
})
.listen(8080, () => { // 서버 연결
  console.log('8080번 포트에서 서버 대기 중입니다!');
});
https를 사용하려면 그것을 인증해줄 수 있는 기관도 필요합니다. 인증서는 인증기관에서 구입해야 하며, 무료로 발급해주는 기관도 있습니다.
const https = require('https');
const fs = require('fs');

https.createServer({
cert: fs.readFileSync('도메인 인증서 경로'),
key: fs.readFileSync('도메인 비밀키 경로'),
ca: [
  fs.readFileSync('상위 인증서 경로'),
  fs.readFileSync('상위 인증서 경로'),
],
}, (req, res) => {
res.writeHead(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번 포트에서 서버 대기 중입니다!');
});
발급받은 인정서가 있다면 다음과 같이 하면 됩니다.
다른 것은 거의 같지만 createServer메서드가 인수를 두개 받습니다.
두번째 인수는 http 모듈과 같이 서버 로직이고, 첫 번째 인수는 인증서에 관련된 옵션 객체입니다.
https에서 기본 포트는 443입니다.

https2와 https3 도 있습니다 아래는 https2 적용한 예제입니다.
const http2 = require('http2');
const fs = require('fs');

http2.createSecureServer({
cert: fs.readFileSync('도메인 인증서 경로'),
key: fs.readFileSync('도메인 비밀키 경로'),
ca: [
  fs.readFileSync('상위 인증서 경로'),
  fs.readFileSync('상위 인증서 경로'),
],
}, (req, res) => {
res.writeHead(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번 포트에서 서버 대기 중입니다!');
});
거의 비슷하지만 createServer 메서드를 createSecureServer 메서드로 바꾸면 됩니다.

4.5 cluster

cluster 모듈은 기본적으로 싱글 프로세스로 동작하는 노드가 cpu 코어를 모두 사용할 수 있게 해주는 모듈입니다. 포트를 공유하는 노드 프로세스를 여러 개 둘 수 있으므로, 요청이 많이 들어왔을 떄 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 잇습니다.
단점으로 메모리를 공유하지 못한다는 단점이 있지만 이는 레디스 등의 서버를 도입하여 해결 가능합니다.
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; i < 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}번 워커 실행`);
}
worker_threads의 예제와 모양이 비슷합니다. 다만 스레드가 아니라 프로세스입니다. 클러스터에는 마스터 프로세스와 워커 프로세스가 있습니다. 마스터 프로세스는 cpu 개수만큼 워커 프로세스를 만들고, 8086번 포트에서 대기합니다. 요청이 들어오면 만들어진 워커 프로세스에 요청을 분배합니다.
워커 프로세스가 실질적인 일을 하는 프로세스입니다. 이책에서 실험한 컴퓨터 cpu 코어가 여섯 개라서 워커가 여섯 개 생성 됩니다. 실제로 여섯 개가 생성되었는지 확인해보겠습니다.
setTimeout(() => { // 워커 존재를 확인하기 위해 1초마다 강제 종료
    process.exit(1);
  }, 1000);
하지만 종료된 상태로 놔둔다면 모든 워커가 종료되어 서버가 응답하지 않을것입니다.워커가 꺼진다면 다시 켜주는 코드가 있어야합니다.
 // 워커가 종료되었을 때
cluster.on('exit', (worker, code, signal) => {
  console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
  console.log('code', code, 'signal', signal);
  cluster.fork();
});
직접 위같이 cluster 모듈로 클러스터링을 구현할 수도 있지만, 실무에서는 pm2 등의 모듈로 cluster 기능을 사용하곤 합니다.

1개의 댓글

comment-user-thumbnail
2021년 4월 8일

오타가 많네요^^;

답글 달기