티스토리 뷰

728x90
SMALL

1. 요청과 응답 이해하기

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

ex)

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);
});

 

ex) 여러 서버를 실행

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

 

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

 

note HTTP 상태 코드

2XX : 성공

3XX : 리다이렉션

4XX : 요청 오류

5XX : 서버 오류

2. REST와 라우팅 사용하기

REST(REpresentational State Transfer) : 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법

REST에서는 주소 외에도 HTTP 요청 메서드라는 것을 사용

GET, POST, PUT, PATCH, DELETE, OPTIONS

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

HTTP 통신을 사용하면 클라이언트가 누구든 상관없이 같은 방식으로 서버와 소통할 수 있다.

즉, 서버와 클라이언트가 분리되어 있다. => 서버를 확장할 때 편해

 

req.method

req.url

    } 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');
        });
      }
    }

예시는 4.2 코드 참조

3. 쿠키와 세션 이해하기

로그인을 구혀낳려면 쿠키와 세션에 대해 알고 있어야 한다. 새로고침을 해도 로그인이 유지되는 이유는 클라이언트가 서버에 여러분이 누구인지를 지속적으로 알려주고 있기 때문.

즉, 서버는 미리 클라이언트에 요청자를 추정할 만한 정보를 쿠키로 만들어 보내고, 그다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악한다.

ex) 기본

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) 개인정보를 쿠키에 담아서 위험해

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);
  // 주소가 /login으로 시작하는 경우
  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 (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end(err.message);
    }
  }
})
  .listen(8084, () => {
    console.log('8084번 포트에서 서버 대기 중입니다!');
  });

 

쿠키를 설정할 때는 옵션 사이에 세미콜론(;)을 써서 구분하고 한글과 줄바꿈이 들어가면 안된다. 한글은 encodeURIComponent로 감싸서 넣는다.

쿠키명=쿠키값 : 기본적인 쿠키의 값. name=gilldong

Expires=날짜 : 만료 기한. default는 클라이언트 종료

Max-age=초 : Expires와 비슷하지만 날짜 대신 초 입력. Expires보다 우선

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

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

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

HttpOnly : 설정 시 자바스크립트에서 쿠키에 접근 불가능. 쿠키 조작을 방지. 추천

 

참고

encodeURI 와 encodeURIComponent 함수의 차이점 : 아래 표현된 문자를 제외하고 인코딩

encodeURI : ; , / ? : @ & = + $ – _ . ! ~ * ‘ ( ) # a-z 0-9

encodeURIComponet : – _ . ! ~ * ‘ ( ) a-z 0-9

 

ex) 세션사용

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

 

이 방식은 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통한다. 세션 아이디는 꼭 쿠키를 사용해서 주고받지 않아도 된다. 하지만 간단하기 때문에 보통 쿠키를 사용한다. 세션을 위해 사용하는 쿠키를 세션 쿠키라고 부른다. 실제 배포용 서버에서는 위와 같이 변수에 저장 안해. 보통은 레디스나 멤캐시드 같은 db에 넣어둬

 

위의 코드는 실습용이야. 위험해. 다른 사람들이 만든 검증된 코드를 사용해야해

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

 

http2 모듈은 SSL2 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용할 수 있게 한다. http/2는 요청 및 응답 방식이 기존 http/1.1보다 개선되어 훨씬 효율적으로 요청을 보낼 수 있다.

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

5. cluster

cluster 모듈은 기본적으로 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈이다. 포트를 공유하는 노드 프로세스를 여러 개 둘 수도 있으므로, 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 있다. 서버 무리 줄어듬

성능이 개선 => 메모리를 공유하지 못하는 단점 => 레디스 등의 서버를 도입하여 해결

 

ex)

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

 

worker_threads의 예제와 모양이 비슷하다. 다만 스레드가 아니라 프로세스이다. 클러스터에는 마스터 프로세스와 워커 프로세스가 있다. 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만들고, 8086번 포트에서 대기한다. 요청이 들어오면 만들어진 워커 프로세스에 요청을 분배한다.

cluster.fork()가 없다고 생각을 하자 일단.

그러면 새로고침할때마다 하나씩 워커가 종료되고 cpu개수만큼 새로고침을 하면 모든 워커가 종료되어 서버가 응답하지 않는다. cluster.fokr() 코드를 넣으면 워커 하나가 종료될 때마다 워커 하나가 생성된다. 이런 방식으로 오류를 처리하는 것은 좋지 않다. 오류의 원인을 찾아야 한다. 하지만 예상치못한 에러롤 인해 서버가 종료되는 것을 막기위해 클러스터링을 적용해두는 것이 좋다.

보통은 pm2 등의 모듈로 cluster 기능을 사용한다.

728x90
LIST

' > nodejs 교과서' 카테고리의 다른 글

6. 익스프레스 웹 서버 만들기  (0) 2021.02.21
5. 패키지 매니저  (0) 2021.02.20
3. 노드 기능 알아보기  (0) 2021.02.19
2. 알아두어야 할 자바스크립트  (0) 2021.02.18
1. 노드 시작하기  (0) 2021.02.18
댓글
공지사항