본문 바로가기

WINK-(Web & App)/Express.js (Node.js) 스터디

[2024 여름방학 Node.js 스터디] 백채린 #3주차 4장

반응형

4장 http 모듈로 서버 만들기

 

 

4.1 요청과 응답 이해하기

 

 

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

 

→ 서버에는 요청을 받는 부분과 응답을 보내는 부분이 있어야 한다. 여기서 요청과 응답은 이벤트 방식이라고 생각

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

 

이벤트 리스너를 가진 노드 서버를 만들어보자!

 

// createServer.js

const http = require('http');

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

 

1. http 서버가 있어야 웹 브라우저의 요청을 처리할 수 있기 때문에 http 모듈을 사용했다.

2. 인수로 요청에 대한 콜백 함수를 넣을 수 있으며 요청이 들어올 때마다 매번 콜백 함수가 실행되므로 이 콜백 함수에 응답을 적으면 된다.

3. createServer의 콜백 부분

- req(request) 객체: 요청에 관한 정보를 담고 있다.

- res(response): 응답에 관한 정보를 담고 있다.

→ 코드 실행 결과 아무 일도 일어나지 않는다.(요청에 대한 응답을 넣어주고 서버를 연결해줘야 함)

 

응답을 보내는 부분과 서버 연결 부분을 추가해보자!

 

// server1.js

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

 

res 객체

- res.writeHead

: 응답에 대한 정보를 기록하는 메서드

첫 번째 인수로 성공적인 요청임을 의미하는 200, 두 번째 인수로 응답에 대한 정보로 이 정보가 기록되는 부분을 헤더라고 한다.

- res.write

첫 번째 인수로 클라이언트에게 보낼 데이터(버퍼도 가능),  데이터가 기록되는 부분을 본문이라고 한다.

- res.end

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

 

server1 실행해서 이렇게 나오면 성공!

 

 

이제 웹 브라우저를 열어서 http://localhost:8080 또는 http://127.0.0.1:8080에 접속하면

 

이렇게 뜹니다 ㅎㅎ

 

* localhost

: 현재 컴퓨터의 내부 주소를 가리키며, 외부에서는 접근할 수 없고 자신의 컴퓨터에서만 접근할 수 있다. 그래서 서버를 개발할 때 테스트용으로 많이 사용된다. localhost 대신 127.0.0.1을 주소로 사용해도 같은데 이러한 숫자 주소를 IP(Internet Protocol)라고 한다.

* 포트

: 서버 내에서 프로세스를 구분하는 번호, 서버는 다양한 작업을 하기 때문에 프로세스에 포트를 다르게 할당해 들어오는 요청을 구분한다. 포트 번호는 IP 주소 뒤에 콜론(:)과 함께 붙여 사용한다.

 

유명한 포트 번호: 21(FTP), 80(HTTP), 443(HTTPS), 3306(MYSQL)

 

* 보통 포트 하나에 서비스를 하나만 사용할 수 있으므로 다른 서비스가 사용하고 있는 포트를 사용하려고 하면 에러가 발생

* 리눅스, 맥에서는 1024 이하의 포트에 연결할 때 관리자 권한이 필요하기 때문에 명령어 앞에 sudo를 붙여야 한다.

 

!주의! 포트 충돌

Error: listen EADDRINUSE :::포트 번호

: 다른 서비스가 사용하고 있는 포트를 사용할 경우 발생

  이런 경우, 그 서비스를 종료하거나 노드의 포트를 다른 번호로 바꾸면 된다.

 

만약, 서버를 종료하려면 콘솔에서 Ctrl + C 를 입력

 

 

listen 메서드에 콜백 함수 대신 + listening 이벤트 리스너, error 이벤트 리스너

 

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

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

 

한 번에 여러 서버를 실행하기 위해서는 createServer를 원하는 만큼 호출

 

// server1-2.js

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

 

 

res.write res.end에 일일이 HTML을 적는 것은 비효율적이므로 미리 HTML 파일을 만들어두는 것이 좋은데,

그 HTML 파일은 fs 모듈로 읽어서 전송할 수 있다. 다음 예제를 통해 배워보자!

 

// 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;

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' });
    // 에러 메시지는 일반 문자열이므로 text/plain을 사용
    res.end(err.message);
  }
})
  .listen(8081, () => {
    console.log('8081번 포트에서 서버 대기 중입니다!');
  });

 

요청이 들어옴 → fs 모듈로 HTML 파일을 읽음 → data 변수에 저장된 버퍼를 그대로 클라이언트에 보내는 방식

 

* HTTP 상태 코드

- 2XX: 성공을 알리는 상태 코드, ex) 200(성공), 201(작성됨)

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

            ex) 301(영구 이동), 302(임시 이동)이 있다. 304(수정되지 않음): 요청의 응답으로 캐시를 사용했다는 뜻

- 4XX: 요청 오류를 나타낸다. ex) 400(잘못된 요청), 401(권한 없음), 403(금지됨), 404(찾을 수 없음)

- 5XX: 서버 오류를 나타낸다. ex) 500(내부 서버 오류), 502(불량 게이트웨이), 503(서비스를 사용할 수 없음)

 

!주의! 무조건 응답을 보내야 한다.

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

응답을 보내지 않으면, 클라이언트는 서버로부터 응답이 오길 하염없이 기다리다가 일정 시간이 지난 후 Timeout(시간 초과) 처리

 

 

 

4.2. REST와 라우팅 사용하기

 

서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다. ex) /index.html: 서버의 index.html을 보내달라는 뜻

요청의 내용이 주소를 통해 표현되므로 서버가 이해하기 쉬운 주소를 사용하는 것이 좋은데, 여기서 REST가 등장한다.

 

REST(REpresentational State Transfer)

: 서버의 자원(서버가 행할 수 있는 것들을 통틀어서 의미)을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킨다. 일종의 약속

 

주소는 의미를 명확히 전달하기 위해 명사로 구성

ex) /user라면 사용자 정보에 관련된 자원을 요청, /post라면 게시글에 관련된 자원을 요청하는 것이라고 추측

그러나, 단순히 명사만 있으면 무슨 동작을 행하라는 것인지 알기 어려움 → REST에서는 주소 외에도 HTTP 요청 메서드라는 것을 사용

 

HTTP 요청 메서드

 

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

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

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

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

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

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

 

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

ex) GET 메서드의 /user 주소로 요청 보냄: 사용자 정보를 가져오는 요청

      POST 메서드의 /user 주소로 요청 보냄: 새로운 사용자를 등록

→ 장점: 주소와 메서드만 보고 요청의 내용을 알아볼 수 있다.

* GET 메서드

: 브라우저에서 캐싱(기억)할 수도 있어 같은 주소로 GET 요청을 할 때 서버에서 가져오는 것이 아니라 캐시에서 가져올 수도 있다.

 

HTTP 통신을 사용하면 클라이언트가 누구든 상관없이 같은 방식으로 서버와 소통 가능 → 서버와 클라이언트가 분리되어 있다는 뜻

 

 

REST를 사용한 주소 체계로 RESTful한(REST를 따르는 서버) 웹 서버를 만들어보자!

 

restFront.css, restFront.html, restFront.js, about.html, restServer.js 파일을 만든 후 다음과 같이 작성하면 되는데 프론트엔드 코드는 그리 중요하지 않으므로 생략하겠습니다. ( 요 링크 https://github.com/zerocho/nodejs-book 에서 코드 복붙하세요 🤗 )

 

// restServer.js

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') { // req.method로 HTTP 요청 메서드 구분
      if (req.url === '/') { // 주소가 /일 때 restFront.html 제공
        const data = await fs.readFile(path.join(__dirname, 'restFront.html'));
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        return res.end(data); // return 을 붙여서 명시적으로 함수 종료
      } else if (req.url === '/about') { // /about이면 about.html 파일 제공
        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도 아니면
      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, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(err.message);
  }
})
  .listen(8082, () => {
    console.log('8082번 포트에서 서버 대기 중입니다');
  });

 

실행 결과 ~~

 

* 데이터가 메모리에 저장되므로 서버를 종료하면 데이터가 소실되기 때문에 영구적으로 저장하려면 데이터베이스를 사용

 

 

 

4.3 쿠키와 세션 이해하기

 

클라이언트에서 보내는 요청의 단점: 누가 요청을 보내는지 모름 → 로그인(쿠키와 세션 사용)을 구현해 보완

로그인한 후 새로 고침(새로운 요청)을 해도 로그아웃되지 않는 이유는 클라이언트가 서버에 여러분이 누구인지를 지속적으로 알려주고 있기 때문, 이를 위해 서버는 요청에 대한 응답을 할 때 쿠키라는 것을 같이 보낸다.

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

 

서버로부터 쿠키가 옴 → 웹 브라우저에서 쿠키 저장 → 다음 요청 시 쿠키를 동봉해서 보냄 → 서버는 요청 속 쿠키를 읽어 사용자 파악

* 쿠키가 누구인지 추적하고 있는 것이기 때문에 개인정보 유출 방지를 위해 쿠키를 주기적으로 지우는 것이 좋다.

 

쿠키는 요청의 헤더(Cookie)에 담겨 전송되고, 브라우저는 응답의 헤더(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번 포트에서 서버 대기 중입니다!');
  });

 

쿠키는 문자열 형식으로 존재하며, 쿠키 간에는 세미콜론을 넣어 각각을 구분한다. ex) name=zerocho;year=1994

 

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

   (쿠키는 req.headers.cookie에 들어 있으며, req.headers는 요청의 헤더를 의미)

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

    (Set-Cookie는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미)

4. 실제로 응답을 받은 브라우저는 mycookie=test라는 쿠키를 저장

5. localhost:8083에 접속

    (req.url req.headers.cookie에 대한 정보를 로깅하도록 했으며, req.url은 주소의 path search 부분을 알린다.)

6. 화면에는 Hello Cookie가 뜨고 콘솔에서는 아래와 같은 메시지를 볼 수 있다.

 

/ undefined
/favicon.ico { mycookie: 'test' }
// 만약 실행 결과가 위와 다르다면 브라우저의 쿠키를 모두 제거한 후에 다시 실행

 

 

요청은 분명 한 번만 보냈는데 두 개가 기록되어 있다. /favicon.ico는 요청한 적이 없는데?

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

 

▶ 파비콘(favicon): 다음과 같이 웹 사이트 탭에 보이는 이미지

브라우저는 파비콘이 뭔지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보낸다. 현재 예제에서 HTML에 파비콘에 대한 정보를 넣어두지 않았으므로 브라우저가 추가로 /favicon.ico를 요청한 것 !

(요청 두 개를 통해 우리는 서버가 제대로 쿠키를 심었음을 확인)

 

첫 번째 요청(/)을 보내기 전, 브라우저는 어떠한 쿠키 정보도 갖고 있지 않는다.

→ 서버는 응답의 헤더에 mycookie=test라는 쿠키를 심으라고 브라우저에 명령(Set-Cookie)

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

 

아직까지는 단순한 쿠키만 심었을 뿐이며, 그 쿠키가 나인지 식별해주지는 못하고 있다.

다음 예제에서 사용자를 식별하는 방법을 알아보자!

 

다음 두 파일을 같은 폴더 안에 생성해주세요 🙂‍↕️

 

▶ 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>

 

▶ cookie2.js

주소가 /login과 /로 시작하는 것까지 두 개이므로 주소별로 분기 처리 

http://localhost:8084에 접속하면 /로 요청을 보내고, cookie2.html에서 form을 통해 로그인 요청을 보낼 때 /login으로 요청을 보내게 된다. 소스 코드에서는 /login 부분이 먼저 나오지만 실제로 제일 먼저 실행되는 곳은 else 부분 !

 

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

// 1
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);
 
// 2
  // 주소가 /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();
 
// 3
  // 주소가 /이면서 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번 포트에서 서버 대기 중입니다!');
});

 

코드 분석

 

 쿠키는 mycookie=test 같은 문자열, parseCookies 함수는 쿠키 문자열을 쉽게 사용하기 위해 자바스크립트 객체 형식으로 바꾸는 함수이다. 이 함수를 거치면 { mycookie: 'test'}가 된다.

 로그인 요청(GET /login)을 처리하는 부분, form은 GET 요청인 경우 데이터를 쿼리스트링으로 보내기에 URL 객체로 쿼리스트링 부분을 분석했으며 쿠키의 만료 시간도 지금으로부터 5분 뒤로 설정했다. 이제 302 응답 코드, 리다이렉트 주소와 함께 쿠키를 헤더에 넣는다. 브라우저는 이 응답 코드를 보고 페이지를 해당 주소로 리다이렉트한다. 헤더에는 한글을 설정할 수 없으므로 name 변수를 encodeURIComponent 메서드로 인코딩했다.

* Set-Cookie의 값으로는 제한된 ASCII 코드만 들어가야 하므로 줄바꿈을 넣어서는 안 된다.

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

 

쿠키를 설정할 때는 각종 옵션(만료 시간(Expires)과 HttpOnly, Path 등)을 넣을 수 있으며, 옵션 사이에 세미콜론(;)을 써서 구분하면 된다. 쿠키에는 한글과 줄바꿈이 들어가서는 안 되며, 한글은 encodeURIComponent로 감싸서 넣는다.

쿠키 설정 시 각종 옵션

 

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

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

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

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

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

- Secure: HTTPS일 경우에만 쿠키가 전송

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

 

 

 

 

 

▶ 새로 고침을 해도 로그인이 유지되지만 Application 탭에서

보이는 것처럼 쿠키가 노출되어 있기 때문에 이 방식은 쿠키가 조작될 위험이 있다. 따라서 이름 같은 민감한 개인정보를 쿠키에 넣어두는 것은 좋지 않다.

 

 

 

 

 

 

다음과 같이 코드를 변경해 서버가 사용자 정보를 관리하도록 만들자!

 

▶ session.js

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

 

 

 

▶ 세션 (session)

쿠키에 이름 대신, uniqueInt라는 숫자 값을 보냈고 이름과 만료 시간은 uniqueInt 속성명 아래에 있는 session이라는 객체에 대신 저장, 이제 cookie.session이 있고 만료 기한을 넘기지 않았다면 session 변수에서 사용자 정보를 가져와 사용 → 이 방식이 세션

서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통한다. 세션 아이디는 쿠키를 사용해서 주고받는 방법이 젤 간단하며, 세션을 위해 사용하는 쿠키를 세션 쿠키라고 한다.

 

 

 

 

 

4.4 https와 http2

 

https 모듈: 웹 서버에 SSL 암호화를 추가

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

 

SSL이 적용된 웹 사이트 (브라우저 주소창의 🔒 표시를 봐주세요!!)

 

* 서버에 암호화를 적용하려면 https 모듈을 사용해야 하지만 https 사용을 위해서는 인증서를 발급해야 한다.

 

 발급받은 인증서가 있다면 다음과 같이 해보자!

 

▶ server1-3.js

createServer 메서드가 인수를 두 개 받는다. 두 번째 인수는 http 모듈과 같이 서버 로직이고, 첫 번째 인수는 인증서에 관련된 옵션 객체

 

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

 

인증서를 구입하면 pem이나 crt, 또는 key 확장자를 가진 파일들을 제공

→ 파일들을 fs.readFileSync 메서드로 읽어서 cert, key, ca 옵션에 알맞게 넣는다.

→ 실제 서버에서는 80번 포트 대신 443번 포트를 사용

 

노드의 http2 모듈: SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2 사용 가능

* http/2는 요청 및 응답 방식이 기존 http/1.1보다 개선되어 훨씬 효율적으로 요청을 보내며 http/2를 사용하면 웹의 속도도 많이 개선

 

 

▶ server1-4.js (http2 적용)

https 모듈 → http2, createServer 메서드 → createSecure Server 메서드로 변경하면 된다.

 

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

 

 

 

 

4.5 cluster

 

cluster 모듈: 기본적으로 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈

 

포트를 공유하는 노드 프로세스를 여러 개 둘 수도 있어, 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 있어 서버에 무리가 덜 가게 된다.

 

장점: 코어를 하나만 사용할 때에 비해 성능 개선

단점: 메모리 공유 불가능(세션을 메모리에 저장하는 경우 문제 발생) → 레디스 등의 서버를 도입해 해결 가능

 

▶ cluster.js

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

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

 

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

 

 

* 직접 cluster 모듈로 클러스터링을 구현할 수도 있지만, 실무에서는 pm2 등의 모듈로 cluster 기능을 사용하곤 한다.

 

 

4.2(REST와 라우팅 사용하기)에서 웹 서버 주소는 크게 HTML 또는 CSS 같은 정적 파일을 요청하는 주소와 서버의 users 자원을 요청하는 주소로 나뉘어져 있었다. 만약 파일이나 자원의 수가 늘어나면 그에 따라 주소의 종류도 많아지고 주소의 수가 많아질수록 코드는 계속 길어지는데 여기에 쿠키와 세션을 추가하게 되면 더 복잡해진다. → 그래서, Express 모듈 사용 (다른 사람이 만들어둔 모듈)

 

 

다른 사람의 모듈을 설치할 수 있게 해주는 npm과 npm에서 모듈을 설치하고 내가 직접 만들어 배포하는 방법은 다음장에서 알아보자

✌🏻(-⏝-) ✌🏻

반응형