본문 바로가기
node.js

4장 ~ 5장

by BIGENGINEER 2023. 11. 6.

 

 

4장

http 모듈로 서버 만들기

 

 

 

 

4.1 요청과 응답 이해하기

- 클라이언트에서 서버로 요청을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답을 보낸다.

- 서버 : 요청을 받는 부분과 응답을 보내는 부분이 있다. 

-> 클라이언트로부터 요청이 왔을 때 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해두어야한다.

 

1) 이벤트 리스너를 가진 노드 서버

const http = require('http');

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

- http 서버가 있어야 웹 브라우저의 요청을 처리할 수 있으므로 http 모듈을 사용한다.

- http 모듈에는 createServer 메서드가 있다. 

- 메서드의 인수로는 콜백함수를 넣을 수 있고, 요청이 들어올때마다 콜백함수 실행된다.

- 매개변수 : rep과 res 

- rep 객체 : 요청에 관한 정보

- res 객체 : 응답에 관한 정보

 

 

 

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

- '8080번 포트에서 대기 중입니다.' 라는 문장이 출력됨.

- http://localhost:8080 또는 http://127.0.0.1:8080에 접속된다. 

 

 

※ localhost 와 포트의 의미

- localhost는 현재 컴퓨터의 내부 주소를 가리키고, 외부에서는 접근할 수 없으며 자신의 컴퓨터에서만 접근할 수 있다. 

- localhost 대신 127.0.0.1을 사용해도 되며 서버 개발 시 테스트 용으로 많이 사용됨  -> 숫자 주소 = IP 라고 한다.

 

- 포트는 서버 내에서 프로세스를 구분하는 번호이다. 

- 서버는 프로세스에 포트를 다르게 할당해 들어오는 요청을 구분한다. 

- 유명한 포트 번호로는 80(HTTP), 443(HTTPS) 등이 있다.

- 포트 번호는 IP주소 뒤에 콜론 : 과 함께 붙여 사용한다.

- 80번 포트는 생략할 수 있다. 

-> 일반적으로 컴퓨터에서 80번 포트는 이미 다른 서비스가 사용하고 있을 확률이 크기 때문에

   충돌을 방지하기 위해서 생략한다. 

 

 

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

- createServer 메서드 뒤에 listen 메서드를 붙이고 클라이언트에 공개할 포트 번호와 포트 연결 완료 후

  실행될 콜백 함수를 넣는다. 

- 이 파일을 실행하면 서버는 8080번 포트에서 요청이 오기를 기다린다.

 

 

- res 객체

- res.writeHead res.write,  res.end 메서드가 있다. 

 

- res.writeHead :  응답에 대한 정보를 기록하는 메서드이다.

- > 첫 번째 인수로 성공적인 요청임을 의미하는 200을, 두 번째 인수로 응답에 대한 정보를 보낸다.

 -> 위의 코드에는 콘텐츠의 형식이 HTML임을 알리고, 한글 표시를 위해 charset utf-8로 지정한 것이다. 

-> 이 정보가 기록되는 부분을 헤더라고 한다. 

 

- res.write 메서드의 첫 번째 인수는 클라이언트로 보낼 데이터이다. 

- > 데이터가 기록되는 부분을 본문이라고 한다. 

 

- res.end:  응답을 종료하는 메서드이다. 

- > 인수가 있다면 그 데이터도 클라이언트로 보내고 응답을 종료한다.

 

-> 브라우저는 응답 내용을 받아서 렌더링한다. 

 

 

 

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.end('<p>Hello Server!</p>');
});
server.listen(8080);

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

- listen 메서드에 콜백 함수를 넣는 대신 ,  listening 이벤트 리스너를 붙여도 된다. 

 

 

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

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(8081, () => { // 서버 연결
    console.log('8081번 포트에서 서버 대기 중입니다!');
  });

- 위 처럼  createServer를 원하는 만큼 호출해서 한 번에 여러 서버를 실행할 수 도 있다.

-> 주의 !! : 포트번호가 달라야한다.

 

 

 

 

- res.write res.end에 일일이 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>

 

- html 파일을 미리 만들어 둔다.

 

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

http.createServer(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);
    res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(err.message);
  }
})
  .listen(8081, () => {
    console.log('8081번 포트에서 서버 대기 중입니다!');
  });

 

 - 요청이 들어오면 먼저 fs 모듈로 html 파일을 읽는다.- data 변수에 저장된 버퍼를 그대로 클라이언트에 보낸다.

 

 

 

※ HTTP 상태 코드

- 상태 코드 : 요청이 성공했는지 실패했는지를 판단한다.

- 2XX : 성공을 알리는 상태 코드이다.

- 3XX : 리다이렉션(다른 페이지로 이동)을 알리는 상태코드이다.

- 4XX : 요청 오류를 나타낸다. 요청 자체에 오류가 있을 때 표시된다.

- 5XX : 서버 오류를 나타낸다. 요청은 제대로 왔지만 서버에 오류가 생겼을 때 발생한다.

 

주의 !!

- 요청 처리 과정 중에 에러가 발생했다고 응답을 보내지 않으면 안 된다. 

무조건 응답을 보내서 요청이 마무리 되었음을 알려야한다. 그렇지 않으면 계속 기다리다가 Timeout 처리한다.

 

 

 

 

4.2 REST와 라우팅 사용하기

1) REST : REpresentational State Transfer의 줄임말, 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법이다.

- 주소는 명사로 구성되는데, 명사만으로 무슨 동작을 행하라는 것인지 알기 어려우므로 REST에서는 주소외에

  HTTP 요청  메서드 라는 것을 사용한다. 

 

2) 요청  메서드  

- 폼 데이터 전송할 때 GET, POST 와 같은 메서드를 지정하는데, 이것이 요청메서드이다.

- 요청 메서드의 종류

GET: 서버 자원을 가져오고자 할 때 사용한다. 요청의 본문에 데이터를 넣지 않고 데이터를 서버로 보내야 한다면 쿼리스트링을 사용한다.

 POST: 서버에 자원을 새로 등록하고자 할 때 사용한다. 요청의 본문에 새로 등록할 데이터를 넣어 보낸다.

 PUT: 서버의 자원을 요청에 들어 있는 자원으로 치환하고자 할 때 사용한다. 요청의 본문에 치환할 데이터를 넣어 보낸다.

 PATCH: 서버 자원의 일부만 수정하고자 할 때 사용한다. 요청의 본문에 일부 수정할 데이터를 넣어 보낸다.

 DELETE: 서버의 자원을 삭제하고자 할 때 사용한다. 요청의 본문에 데이터를 넣지 않는다.

 OPTIONS: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용한다. 

 

 

- 주소 하나가 요청 메서드를 여러 개 가질 수 있다. 

- GET 메서드는 브라우저에서 캐싱(기억)할 수도 있어 같은 주소로 GET요청을 할 때 서버에서 가져오는 것이 아니라 캐시 에서 가져올 수도 있다. -> 이렇게 캐싱되는 경우에 성능이 좋아진다.

 

 

3) RESTful한 웹 서버

- REST를 따르는 서버를 ‘RESTful하다’고 표현한다.

 

HTTP 메서드 주소 역할
GET / restFront.html 파일 제공
GET /about about.html 파일 제공
GET /users 사용자 목록 제공
GET 기타 기타 정적 파일 제공
POST /user 사용자 등록
PUT /user/사용자id 해당 id의 사용자 수정
DELETE /user/사용자id 해당 id의 사용자 제거

 

 

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

http.createServer(async (req, res) => {
  try {
    console.log(req.method, req.url);
    if (req.method === 'GET') {
      if (req.url === '/') {
        const data = await fs.readFile(path.join(__dirname, '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(path.join(__dirname, 'about.html'));
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
        return res.end(data);
      }
      // 주소가 /도 /about도 아니면
      try {
        const data = await fs.readFile(path.join(__dirname, req.url));
        return res.end(data);
      } catch (err) {
        // 주소에 해당하는 라우트를 찾지 못했다는 404 Not Found error 발생
      }
    }
    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번 포트에서 서버 대기 중입니다');
  });

- req.method로 HTTP 요청 메서드를 구분하고 있다.

- 메서드가 GET이면 다시 req.url로 요청 주소를 구분한다. 주소가 /일 때는 restFront.html을 제공하고, 주소가 /about이면      about.html 파일을 제공한다. 이 외의 경우에는 주소에 적힌 파일을 제공한다.

- 만약 존재하지 않는 파일을 요청했거나 GET 메서드 요청이 아닌 경우라면 404 NOT FOUND 에러가 응답으로 전송한다.

 

!!주의

- res.end 앞에 return은 무조건 붙여야한다.

-> 노드가 자바스크립트 문법을 따르기 때문에 return을 붙이지 않는 한 함수가 종료되지 않기 때문이다.

 

 

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

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

http.createServer(async (req, res) => {
  try {
    console.log(req.method, req.url);
    if (req.method === 'GET') {
      if (req.url === '/') {
        const data = await fs.readFile(path.join(__dirname, '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(path.join(__dirname, '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': 'text/plain; charset=utf-8' });
        return res.end(JSON.stringify(users));
      }
      // /도 /about도 /users도 아니면
      try {
        const data = await fs.readFile(path.join(__dirname, 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/html; charset=utf-8' });
          res.end('등록 성공');
        });
      }
    } 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/html; charset=utf-8' });
          return res.end(JSON.stringify(users));
        });
      }
    } 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/html; charset=utf-8' });
        return res.end(JSON.stringify(users));
      }
    }
    res.writeHead(404);
    return res.end('NOT FOUND');
  } catch (err) {
    console.error(err);
    res.writeHead(500);
    res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(err);
  }
})
  .listen(8082, () => {
    console.log('8082번 포트에서 서버 대기 중입니다');
  });

 - 데이터베이스 대용으로 users라는 객체를 선언해 사용자 정보를 저장한다.

- POST /user 요청에서는 사용자를 새로 저장하고, PUT /user/아이디 요청에서는 해당 아이디의 사용자 데이터를 수정하      며, DELETE /user/아이디 요청에서는 해당 아이디의 사용자를 제거한다.

- POST와 PUT 요청에 req.on('data') req.on('end')는 요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업이다.

- 더불어 받은 데이터는 문자열이므로 JSON으로 만드는 JSON.parse 과정이 필요하다.

 

 

 

 

- 웹 사이트에서 서버에 보내는 요청들 확인

 

Name은 요청 주소, Method는 요청 메서드, Status는 HTTP 응답 코드, Protocol은 통신 프로토콜, Type은 요청의 종류,

xhr은 AJAX 요청을 의미한다.

 

 

※ HTTP 요청/응답

- 요청과 응답은 모두 HTTP 헤더와 HTTP 본문을 갖고 있다.

- HTTP 헤더 : 요청 또는 응답에 대한 정보는 갖고 있는 곳이다.

- HTTP 본문 : 서버와 클라이언트 간에 주고받을 실제 데이터를 담아두는 공간이다. 

 

 

!! 주의 

- REST 서버에서 데이터가 메모리에 저장되므로 서버를 종료하면 데이터가 소실된다. 

- 데이터를 영구적으로 저장하려면 데이터베이스를 사용해야한다.

 

 

 

4.3 쿠키와 세션 이해하기

- 클라이언트에서 보내는 요청에는누가 요청을 보내는지 모른다는 단점이 있다. 

- 여러 컴퓨터가 공통으로 IP 주소를 갖거나 한 컴퓨터를 여러 사람이 사용할 수 도 있기 때문에 로그인 구현이 중요한데, 로그인을 구현하려면 쿠키와 세션을 알고 있어야한다.

 

1) 쿠키

- 쿠키는 유효 기간이 있으며 name=zerocho와 같이 단순한 ‘키-값’의 쌍이다.

- 누구인지 기억하기 위해 서버는 요청에 대한 응답을 할 때 쿠키라는 것을 같이 보낸다. 

- 서버로부터 쿠키가 오면, 웹 브라우저는 쿠키를 저장해뒀다가 다음에 요청할 때마다 쿠키를 동봉해서 보낸다.

- 서버는 요청에 들어 있는 쿠키를 읽어서 사용자가 누구인지 파악한다.

- 쿠키는 요청의 헤더(Cookie)에 담겨 전송되며, 브라우저는 응답의 헤더(Set-Cookie)에 따라 쿠키를 저장한다. 

- 브라우저는 쿠키가 있다면 자동으로 동봉해서 보내주므로 따로 처리할 필요가 없고, 서버에서 브라우저로 쿠키를 보낼      때만 코드를 작성해 처리하면 된다.

 

 

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

- createServer 메서드의 콜백에서는 req 객체에 담겨 있는 쿠키를 가져온다.

- 쿠키는 req.headers.cookie에 들어 있고, req.headers는 요청의 헤더를 의미한다.

- 응답의 헤더에 쿠키를 기록해야 하므로 res.writeHead 메서드를 사용했다.

- Set-Cookie는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미이며, 실제로 응답을 받은 브라우저는 mycookie=test라는 쿠키를 저장한다.

 

 

결과:

/ undefined
/favicon.ico { mycookie: 'test' }

- 첫 번째 요청('/')에서는 쿠키에 대한 정보가 없다고 나오며, 두 번째 요청('/favicon.ico')에서는 { mycookie: 'test' }가 기록된다.

( 파비콘(favicon)이란 웹 사이트 탭에 보이는 이미지를 뜻한다.)

- 첫 번째 요청(/)을 보내기 전에는 브라우저가 어떠한 쿠키 정보도 갖고 있지 않고, 서버는 응답의 헤더에 mycookie=test라는 쿠키를 심으라고 브라우저에 명령(Set-Cookie)한다.

- 따라서 브라우저는 쿠키를 심었고, 두 번째 요청(/favicon.ico)의 헤더에 쿠키가 들어 있음을 확인할 수 있다.

 

 

 

 

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

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 같은 문자열이다. parseCookies 함수는 쿠키 문자열을 쉽게 사용하기 위해 자바스크립트 객체 형식으로 바꾸는 함수이다.

 

  // 주소가 /login으로 시작하는 경우
  if (req.url.startsWith('/login')) {
    const url = new URL(req.url, 'http://localhost:8084');
    const name = url.searchParams.get('name');
    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();

 

 

 - 로그인 요청(GET /login)을 처리하는 부분이다. 

- form은 GET 요청인 경우 데이터를 쿼리스트링으로 보내기에 URL 객체로 쿼리스트링 부분을 분석하고, 쿠키의 만료 시간을 지금으로부터 5분후로 설정한다.

- 그 후302 응답 코드, 리다이렉트 주소와 함께 쿠키를 헤더에 넣는다. 브라우저는 이 응답 코드를 보고 페이지를 해당 주소로 리다이렉트한다.

 

  // 주소가 /이면서 name이라는 쿠키가 있는 경우
  } else if (cookies.name) {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`${cookies.name}님 안녕하세요`);
  } else { // 주소가 /이면서 name이라는 쿠키가 없는 경우
    try {
      const data = await fs.readFile(path.join(__dirname, '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번 포트에서 서버 대기 중입니다!');
});

 

- 그 외의 경우, 먼저 쿠키가 있는지 없는지를 확인하고 쿠키가 없다면 로그인할 수 있는 페이지를 보낸다.

 

- Set-Cookie로 쿠키를 설정할 때 만료 시간 HttpOnly, Path 같은 옵션을 부여한다.

- 쿠키를 설정할 때는 각종 옵션을 넣을 수 있으며, 옵션 사이에 세미콜론(;)을 써서 구분하면 된다.

- 쿠키에는 들어가면 안 되는 글자들이 있는데, 대표적으로 한글과 줄바꿈이 있다.

- 한글은 encodeURIComponent로 감싸서 넣습니다.

 

 

쿠키명=쿠키값: 기본적인 쿠키의 값이다. mycookie=test 또는 name=zerocho와 같이 설정한다.

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

 Max-age=초: Expires와 비슷하지만 날짜 대신 초를 입력할 수 있다. 해당 초가 지나면 쿠기가 제거되며 Expires보다 우선한다.

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

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

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

 HttpOnly: 설정 시 자바스크립트에서 쿠키에 접근할 수 없다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋다.

 

 

 

 

* 세션

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

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 url = new URL(req.url, 'http://localhost:8085');
    const name = url.searchParams.get('name');
    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(path.join(__dirname, '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이라는 객체에 대신 저장한다.

-  cookie.session이 있고 만료 기한을 넘기지 않았다면 session 변수에서 사용자 정보를 가져와 사용한다.

 

- 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통한다.

- 세션 아이디는 꼭 쿠키를 사용해서 주고받지 않아도 됩니다. 하지만 많은 웹 사이트가 쿠키를 사용하고 이를 세션쿠키라고 한다.

- 서버가 멈추거나 재시작되면 메모리에 저장된 변수가 초기화되고, 서버의 메모리가 부족하면 세션을 저장하지 못하는 문제도 생기기때문에 보통은 세션을 레디스(Redis)나 멤캐시드(Memcached) 같은 데이터베이스에 넣어둔다.

 

 

 

4.4 https와 https2

- https 모듈은 웹 서버에 SSL 암호화를 추가한다. GET이나 POST 요청을 할 때 오가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없게 한다.

 

 

* 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사용을 위해서는 인증서를 구입해야하며, 인증서를 구입하면 pem이나 crt, 또는 key 확장자를 가진 파일들을 제공한다.

- 파일들을 fs.readFileSync 메서드로 읽어서 cert, key, ca 옵션에 알맞게 넣으면 되고, 실제 서버에서는 80번 포트 대신 443번 포트를 사용한다.

 

 

* http2

 

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

 

- https 모듈과 거의 유사하고 https 모듈을 http2로, createServer 메서드를 createSecure Server 메서드로 바꾸면 된다.

 

- 노드의 http2 모듈은 SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용할 수 있게 한다. http/2는 요청 및 응답 방식이 기존 http/1.1보다 개선되어 훨씬 효율적으로 요청을 보내고 http/2를 사용하면 웹의 속도도 많이 개선 된다.

 

 

 

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);
  });
} 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>');
  }).listen(8086);

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

 

-  클러스터에는 마스터 프로세스와 워커 프로세스가 있다.

- 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만들고, 8086번 포트에서 대기한다. 요청이 들어오면 만들어진 워커 프로세스에 요청을 분배한다.

- 워커 프로세스가 실질적인 일을 하는 프로세스이다.

 

 

} 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}(번 워커 실행`);
}

 

- 요청이 들어올 때마다 1초 후에 서버가 종료되도록 한다.

- 서버를 실행하면, process.pid는 실행할 때마다 달라진다. 

 

 

 

16136번 워커가 종료되었습니다.
code 1 signal null
17272번 워커가 종료되었습니다.
code 1 signal null
16452번 워커가 종료되었습니다.
code 1 signal null
9004번 워커가 종료되었습니다.
code 1 signal null
11040번 워커가 종료되었습니다.
code 1 signal null
7368번 워커가 종료되었습니다.
code 1 signal null

 

( 이 책에서 실험한 컴퓨터는 CPU 코어가 여섯 개이므로 워커가 여섯 개 생성 )

 

- 서버에  접속하면 1초 후 콘솔에 워커가 종료되었다는 메시지가 뜬다. 여섯 번 새로 고침을 하면 모든 워커가 종료되어 서버가 응답하지 않는다.

 

 

- 코드(code)는 process.exit의 인수로 넣어준 코드가 출력되고, 신호(signal)는 존재하는 경우 프로세스를 종료한 신호의 이름이 출력된다.

- 워커 프로세스가 존재하기에 여섯 번까지는 오류가 발생해도 서버가 정상적으로 작동할 수 있다는 뜻이다. 종료된 워커를 다시 켜면 오류가 발생해도 계속 버틸 수 있다.

 

 

* 워커 프로세스가 종료되었을 때 새로 하나를 생성

...
  cluster.on('exit', (worker, code, signal) => {
    console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
    console.log('code', code, 'signal', signal);
    cluster.fork();
  });
...

 

28592번 워커가 종료되었습니다.
code 1 signal null
10520번 워커 실행
10520번 워커가 종료되었습니다.
code 1 signal null
23248번 워커 실행

 

 

- 워커 하나가 종료될 때마다 새로운 워커 하나가 생성된다. 하지만 이러한 방식으로 오류를 처리하려는 것은 좋지 않다.

- 그래도 예기치 못한 에러로 인해 서버가 종료되는 현상을 방지할 수 있어 위처럼 클러스터링을 적용해두는 것이 좋다.

 

 

 

 

 

 

 

5장

패키지 매니저 

 

 

5.1 npm 알아보기

 

- npm은 Node Package Manager의 약어로, 이름 그대로 노드 패키지 매니저를 의미 한다.

- 노드가 자바스크립트 프로그램을 컴퓨터에서도 실행할 수 있게 해준다. 대부분의 자바스크립트 프로그램은 패키지라는 이름으로 npm에 등록되어 있으므로 특정 기능을 하는 패키지가 필요하다면 npm에서 찾아 설치하면 된다.

 

- npm에 업로드된 노드 모듈을 패키지라고 부른다. 모듈이 다른 모듈을 사용할 수 있는 것처럼, 패키지가 다른 패키지를 사용할 수도 있는데, 이런 관계를 의존 관계라고 한다.

 

 

5.2 package.json으로 패키지 관리하기

- 설치한 패키지의 버전을 관리하는 파일이 바로 package.json이다.

- 노드 프로젝트를 시작하기 전에는 폴더 내부에 무조건 package.json부터 만들고 시작해야 한다.

( npm은 package.json을 만드는 명령어를 제공 )

 

 

* package.json 만드는 과정

$ 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 json` 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.
package name: (폴더명) [프로젝트 이름 입력]
version: (1.0.0) [프로젝트 버전 입력]
description: [프로젝트 설명 입력]
entry point: index.js
test command: [엔터 키 클릭]
git repository: [엔터 키 클릭]
keywords: [엔터 키 클릭]
author: [여러분의 이름 입력]
license: (ISC) [엔터 키 클릭]
About to write to C:\Users\zerocho
pmtest\package.json:

{
  "name": "npmtest",
  "version": "0.0.1",
  "description": "hello package.json",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "ZeroCho",
  "license": "ISC"
}

Is this ok? (yes) yes

 

 

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 공식 홈페이지에서 패키지를 쉽게 찾을 수 있게 한다. package.json의 keywords 속성에 저장된다.

 license: 해당 패키지의 라이선스를 넣으면 된다.

 

 

 

!!! 주의

- 라이선스

: 오픈 소스라고 해서 모든 패키지를 아무런 제약 없이 사용할 수 있는 것은 아니다. 라이선스별로 제한 사항이 있으므로 설치 전에 반드시 라이선스를 확인해야한다.

 

 

{
  "name": "npmtest",
  "version": "0.0.1",
  "description": "hello package.json",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "ZeroCho",
  "license": "ISC"
}

 

- npm init 실행이 완료되면 폴더에 다음과 같은 파일이 생성된다.

- scripts 부분은 npm 명령어를 저장해두는 부분이다. 콘솔에서 npm run [스크립트 명령어]를 입력하면 해당 스크립트가 실행 된다.

 

 

* 패키지 설치 

$ npm install express
added 50 packages, and audited 51 packages in 1s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

 

- npm install [패키지 이름]을 package.json이 있는 폴더의 콘솔에서 입력하면 된다.

- 설치한 패키지가 package.json에 기록된다. 

 

{
  "name": "npmtest",
  ...
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.3",
  }
}

 

- dependencies라는 속성이 새로 생겼고, express라는 이름과 함께 설치된 버전이 저장된다.

 

 

!!! 주의 

- 프로젝트 이름과 설치하는 패키지 이름은 달라야한다. 

- express를 설치했으면 프로젝트 이름은 express이면 안된다. 

 

 

- node_modules라는 폴더도 생성되는데, 그 안에 설치한 패키지들이 들어 있다.

- Express 하나만 설치했는데 패키지가 여러 개 들어 있는것은 Express가 의존하는 패키지들이다.

- 패키지 하나가 다른 여러 패키지에 의존하고, 그 패키지들은 또 다른 패키지들에 의존한다. 이렇게 의존 관계가 복잡하게 얽혀 있어 package.json이 필요한 것이다.

 

 

- package-lock.json이라는 파일도 생성되는데, 직접 설치한 express 외에도 node_modules에 들어 있는 패키지들의 정확한 버전과 의존 관계가 담겨 있다.

- npm으로 패키지를 설치, 수정, 삭제할 때마다 패키지들 간의 정확한 내부 의존 관계를 이 파일에 저장한다.

- 즉, package.json은 직접 설치한 패키지를 기록하는 파일이고, package-lock.json은 패키지 간의 의존 관계를 명시한 파일이다.

 

 

 

 

$ npm install morgan cookie-parser express-session
added 12 packages, and audited 63 packages in 2s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

 

- npm install [패키지1] [패키지2] [...]와 같이 패키지들을 나열해서 묘듈 여러 개를 동시에 설치한다.

- 설치한 패키지들이 dependencies 속성에 기록된다.

 

 

 

$ npm install --global rimraf
changed 12 packages, and audited 13 packages in 2s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

 

- npm에는 전역(global) 설치라는 옵션도 있다. 패키지를 현재 폴더의 node_modules에 설치하는 것이 아니라 npm이 설치되어 있는 폴더에 설치한다.

- 이 폴더의 경로는 보통 시스템 환경 변수에 등록되어 있으므로 전역 설치한 패키지는 콘솔의 명령어로 사용할 수 있다.

- 전역 설치를 했다고 해서 패키지를 모든 곳에서 사용한다는 뜻이 아니며, 대부분 명령어로 사용하기 위해 전역 설치한다.

 

$ npm install --save-dev rimraf
$ npx rimraf node_modules

 

- 단, 전역 설치한 패키지는 package.json에 기록되지 않아 다시 설치할 때 어려움이 있다.

- 위의 코드와 같이 npx 명령어를 붙여 실행하면 패키지를 전역 설치한 것과 같은 효과를 얻을 수 있고, 패키지가 package.json에 기록되었으므로 버전 관리도 용이하다. 

 

 

 

 

5.3 패키지 버전 이해하기 

- 노드 패키지들의 버전은 항상 세 자리로 이뤄져 있다.

- 버전이 세 자리인 이유는 SemVer 방식의 버전 넘버링을 따르기 때문이다. 

- 버전 번호를 어떻게 정하고 올려야 하는지를 명시하는 규칙이 SemVer이다.

 

 

* SemVer

- 버전의 첫 번째 자리는 메이저(major) 버전이다.

- 메이저 버전 0:  초기 개발 중이다.

- 메이저 버전 1 ~ : 정식 버전이다.

- 메이저 버전은 하위 호환이 안 될 정도로 패키지의 내용이 수정되었을 때 올린다. 

-  1.5.0에서 2.0.0으로 올렸다는 것은, 1.5.0 버전 패키지를 사용하고 있던 사람들이 2.0.0으로 업데이트했을 때 에러가 발생할 확률이 크다는 것이다.

 

- 두 번째 자리는 마이너(minor) 버전이다.

- 마이너 버전은 하위 호환이 되는 기능 업데이트를 할 때 올린다.

- 버전을 1.5.0에서 1.6.0으로 올렸다면, 1.5.0 사용자가 1.6.0으로 업데이트했을 때 아무 문제가 없어야 한다.

 

- 세 번째 자리는 패치(patch) 버전이다.

- 새로운 기능이 추가되었다기보다는 기존 기능에 문제가 있어 수정한 것을 내놓았을 때 패치 버전을 올린다.

-1.5.0에서 1.5.1처럼 업데이트 후 아무 문제가 없어야 한다.

 

- 새 버전을 배포한 후에는 그 버전의 내용을 절대 수정하면 안 된다. 만약 수정 사항이 생기면 메이저 버전, 마이너 버전, 패치 버전 중 하나를 의미에 맞게 올려서 새로운 버전으로 배포해야 한다.

 

 

 

* 다양한 문자

-  ^ : 마이너 버전까지만 설치하거나 업데이트한다.

- npm i express@^ 1.1.1이라면 1.1.1 이상부터 2.0.0 미만 버전까지 설치된다.

 

- ~ : 패치 버전까지만 설치하거나 업데이트한다.

-  npm i express@~1.1.1이라면 1.1.1 이상부터 1.2.0 미만 버전까지 설치된다.

 

- >, <, > =, < =, =은 알기 쉽게 초과, 미만, 이상, 이하, 동일을 뜻한다.

 

- @latest도 사용하는데, 안정된 최신 버전의 패키지를 설치한다.

 

- @next를 사용하면 가장 최근 배포판을 사용할 수 있다.

-> @latest와 다른 점은 안정되지 않은 알파나 베타 버전의 패키지를 설치할 수 있다는 것이다. 

 

 

 

5.4 기타 npm 명령어

 

1.  npm update [패키지 이름] : 업데이트한다.

- Current와 Wanted가 다른 경우 사용한다.

 

2. npm uninstall [패키지 이름] : 해당 패키지를 제거하는 명령어이다.

- 패키지가 node_modules 폴더와 package.json에서 사라진다.

 

3. npm search [검색어] : npm의 패키지를 검색한다. 

 

4. npm info [패키지 이름] : 패키지의 세부 정보를 파악하고자 할 때 사용하는 명령어이다.

- package.json의 내용과 의존 관계, 설치 가능한 버전 정보 등이 표시된다.

 

5. npm login : npm 로그인을 위한 명령어이다.

 

 

 

 

5.5 패키지 배포하기

 

1. npm 웹 사이트(https://www.npmjs.com) 우측 상단의 Sign Up을 눌러 회원 가입을 한다.

2. 회원 가입 confirm 메일을 확인한다.

3. 콘솔에서 npm login 명령어를 입력해 생성한 계정으로 로그인한다. 보안이 강화돼서 가입 시 입력했던 이메일로 OTP 코드가 발송된다. OTP 코드도 입력해야 로그인된다.

 

4. 패키지로 만들 코드 작성한다.

- package.json의 main 부분의 파일명과 일치해야한다. ( npm에서 이 파일이 패키지의 진입점임을 알 수 있기 때문이다.)

module.exports = () => {
  return 'hello package';
};

 

- 에러 발생 시에는 npm의 패키지 이름이 겹치는 것이므로 패키지 이름을 바꿔서 배포해야한다. 

 

$ npm publish
// notice 생략
+ npmtest-1234@0.0.1
$ npm info npmtest-1234
npmtest-1234@0.0.1 | ISC | deps: none | versions: 1
hello package.json
// 중략
maintainers:
- zerocho <zerohch0@gmail.com>

dist-tags:
latest: 0.0.1

published 51 seconds ago by zerocho <zerohch0@gmail.com>

 

- 배포 명령어를 입력하고,  배포한 패키지가 npm에 제대로 등록되었는지 확인한다.

 

$ npm publish
// notice 생략
npm ERR! code E403
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/npmtest-1234 - You cannot publish over the previously published versions: 0.0.1.

 

- 이 에러 메시지가 보인다면 이미 출시한 버전이라는 뜻이다. 

 

$ npm version patch
v0.0.2
$ npm publish
// notice 생략
+ npmtest-1234@0.0.2

 

- 버전을 올리기 위해 npm version 명령어를 사용한다.

 

 

* 배포한 패키지 삭제

- 삭제 명령어는 npm unpublish [패키지 이름] --force

 

$ npm unpublish npmtest-1234 --force
npm WARN using --force Recommended protections disabled.
- npmtest-1234
$ npm info npmtest-1234
npm ERR! code E404
npm ERR! 404 Unpublished on 2022-04-17T08:51:10.506Z
...

 

- 확인 후  404 에러가 발생한다면 지워진 것이다.

 

 

 

 

 

 


*빈칸문제 ( 드래그 해서 답을 확인해 보세요. )

1. 네트워크에는 (서버)와 (클라이언트)가 있으며, (서버)에는 요청을 받는 부분과 응답을 보내는 부분이 있다.

2. ( localhost)는 현재 컴퓨터의 내부 주소를 가리키고, 외부에서는 접근할 수 없으며 자신의 컴퓨터에서만 접근할 수 있다.

3. ( 포트)는 서버 내에서 프로세스를 구분하는 번호이며, 일반적으로 컴퓨터에서 (80번 포트)는 이미 다른 서비스가 사용하고 있을 확률이 크기 때문에 충돌을 방지하기 위해서 생략한다. 

4. res 객체에는 (res.writeHead) (res.write),  (res.end )메서드가 있다. 

5. 요청이 성공했는지 실패했는지를 판단하는 것을 ( http 상태코드)라고 하며, (2XX)은 성공을 알린다.

6. 폼 데이터 전송할 때 (GET), (POST) 와 같은 메서드를 지정하는데, 이것이 (요청메서드)이다.

7. 누구인지 기억하기 위해 서버는 요청에 대한 응답을 할 때 (쿠키)라는 것을 같이 보내는데 이는 유효 기간이 있으며 name=zerocho와 같이 단순한 ( ‘키-값’)의 쌍이다.

 

 

 

* 코드 문제 

1. 브라우저가 mycookie= duksung 라는 쿠키를 저장하도록 코드를 입력하시오.

(드래그 해서 코드 확인하시오.)

 

코드 : 'Set-Cookie': 'mycookie=duksung'

 

const http = require('http');

http.createServer((req, res) => {
  console.log(req.url, req.headers.cookie);
  res.writeHead(200, { //여기에 코드 입력 });
  res.end('Hello Cookie');
})
  .listen(8083, () => {
    console.log('8083번 포트에서 서버 대기 중입니다!');
  });

 

 

 

2. 쿠키에 이름을 담아서 보내는 대신 브라우저가 get으로 받아온 'name'을 세션을 이용하여 전달하는 코드를 작성하시오.

 

답 :  `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`

  if (req.url.startsWith('/login')) {
    const url = new URL(req.url, 'http://localhost:8084');
    const name = url.searchParams.get('name');
    const expires = new Date();
    // 쿠키 유효 시간을 현재 시간 + 5분으로 설정
    expires.setMinutes(expires.getMinutes() + 5);
    res.writeHead(302, {
      Location: '/',
      //여기에 코드 입력
    });
    res.end();

'node.js' 카테고리의 다른 글

Node.js - Express  (0) 2024.01.07
Node.js - 모듈 ~ 라우팅  (0) 2024.01.07
8장 몽고디비  (0) 2023.11.26