상세 컨텐츠

본문 제목

[노드1] 4장~5장 http 모듈로 서버 만들기 & 패키지 매니저

23-24/Node.js 1

by Hetbahn 2023. 11. 10. 10:00

본문

728x90

 

 

 

 

 

 

 

 

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와 http2

- 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 교과서』 개정판 3판, 길벗, 4장 ~ 5장

Node.js #1

Editor : 수정

728x90

관련글 더보기