4. http 모듈로 서버 만들기

Donghun Seol·2022년 9월 20일
0

node.js 교과서

목록 보기
6/12

역시 애들 다 재우고 맥주마시면서 하는 개발공부가 개꿀잼

4. http 모듈로 서버 만들기

4.1 요청과 응답 이해하기

요청, 응답, 포트번호에 대한 기본적인 내용은 생략
기본적 메서드는 res.writehead, res.write, res.end가 있음.

// run multiple servers
const server = http.createServer((req, res) => {/*callback content*/}).listen(8080, callback);
const server2 = http.createServer((req, res) => {/*callback content*/}).listen(8081, callback);

// eventlistener for servers
server.on('listening', callback);
server.on('error', callback);

4.2 REST와 라우팅 사용하기

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번 포트에서 서버 대기 중입니다');
  });

4.3 쿠키와 세션 이해하기

쿠키와 세션 모두 요청자를 파악하기 위한 기능이다.

쿠키

  • 브라우저에 저장, 요청할 때마다 서버로 전달되므로 민감한 내용은 포함하지 않아야 한다.
  • 세션과 함께 사용할 경우 세션키만 쿠키에 저장해 놓는다.
  • 아래와 같이 쿠키값 설정한다.
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
res.writeHead(302, { // 302 redirection
	Location: '/',
	'Set-Cookie': `name=${encodeURIComponent(name)};
    Expires=${expires.toGMTString()};
    HttpOnly; Path=/`
});

세션

  • 서버에서는 레디스 또는 멤캐시드와 같은 디비에 세션 정보를 저장한다.
  • 세션들 중 쿠키로부터 받은 세션 id와 일치하는 세션 정보를 불러와서 요청자를 파악한다.
  • 세션과 쿠키가 함께 적용된 사례, uniqueInt와 else if에서 세션값을 검사하는 로직이 핵심
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() + 1);
        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);
        }
    }
})

아우 제발 그냥 Express 쓸게요

4.4 https와 http2

https 모듈은 서버와 통신하는 데이터에 SSL 암호화를 추가한다.
실무에서는 필수
Let's Encrypt와 같은 기관에서 무료로 발급해준다.

http2 는 http/1.1 을 개선해 효율적이고 성능개선된 프로토콜, 가능하면 http2를 사용한다.
아래는 http2와 https 모두 적용된 코드 예제

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 SECURE world!</h1>');
  res.end('<p>bye secured server</p>');
})
.listen('443', () => {
  console.log('https 프로토콜은 443번 포트를 사용합니다.');
});

4.5 cluster

cluster는 동일 포트를 공유하는 여러 프로세스를 생성해서 노드가 멀티코어 CPU를 활용할 수 있게 해주는 모듈이다. 서버간 세션을 공유하지 못하는 등의 단점이 있지만, Redis 등을 활용하면 해결 가능하다.

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

const numCPUs = require('os').cpus().length; // returns CPU core nums

if (cluster.isMaster) {
    console.log(`마스터 프로세스 아이디 : ${process.pid}`);
    for (let i = 0; i < numCPUs; i++) {
        cluster.fork(); // 새로운 워커를 생산
    }

    cluster.on('exit', (worker, code, signal) => {
        console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
        console.log('code', code, 'signal', signal);
        cluster.fork(); // fork another process when a worker is terminated
    });
} 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(() => { // let server exit whenever /GET localhost:8086
            console.log('pid in setTimeout', process.pid);
            process.exit(1);
        }, 1000);
    }).listen(8086);

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

위의 예제와 같이, 워커 하나가 종료될 때마다 새로운 워커를 생성시켜 오류로 인해 전체 서버가 다운되는 것을 막을 수 있다.
실무에서는 cluster모듈을 직접 사용하기보다 pm2 등으로 클러스터링을 구현한다.

profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글