반응형

# 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}번 워커 실행`)
}
반응형

+ Recent posts