4장. http 모듈로 서버 만들기

My_Code·2024년 1월 9일

Node.js 교과서

목록 보기
4/11
post-thumbnail

다음 내용은 인프런에서 공부한 내용을 복습하는 차원에서 기록한 것입니다.
출처 : https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-js-%EA%B5%90%EA%B3%BC%EC%84%9C


💻 4.1 요청과 응답 이해하기

📌 서버와 클라이언트

✏️ 서버와 클라이언트의 관계

  • 클라이언트는 서버로 요청(Request)을 보냄
  • 서버는 요청을 처리 후 클라이언트로 응답(Response)을 보냄

📌 노드로 http 서버 만들기

✏️ http 요청에 응답하는 노드 서버

  • createServer로 요청 이벤트에 대기
  • req 객체는 요청에 관한 정보가, res 객체는 응답에 관한 정보가 담겨 있음
const http = require('http');
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.write('<h2>hello world!!</h2>');
    res.write('<p>hello world!!</p>');
    res.write('<p>hello world!!</p>');
    res.end('hello world!!');
});

server.listen(8080, () => {
    console.log("8080번 포트에서 서버 대기 중입니다.");
});

📌 localhost와 포트

✏️ localhost는 컴퓨터 내부 주소

  • 외부에서는 접근 불가능

✏️ 포트는 서버 내에서 프로세스를 구분하는 번호

  • 기본적으로 http 서버는 80번 포트 사용(생략가능, https는 443)
  • 예) www.gilbut.com:80 -> www.github.com
  • 다른 포트로 데이터베이스나 다른 서버 동시에 연결 가능

📌 이벤트 리스너 붙이기

✏️ listening과 error 이벤트를 붙일 수 있음

const http = require('http');
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.write('<h2>hello world!!</h2>');
    res.write('<p>hello world!!</p>');
    res.write('<p>hello world!!</p>');
    res.end('hello world!!');
}).listen(8080);

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

📌 html 읽어서 전송하기

✏️ write와 end에 문자열을 넣는 것은 비효율적

  • fs 모듈로 html을 읽어서 전송하자
  • write가 버퍼도 전송 가능
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Node.js 웹서버</title>
    </head>
    <body>
        <h1>Node.js 웹서버</h1>
        <p>만들 준비 되셨나요?</p>
    </body>
</html>
const http = require('http');
const fs = require('fs').promises;

const server = http.createServer(async (req, res) => {
    try {
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        const data = await fs.readFile('./server2.html');
        res.end(data);
    }catch (error) {
        console.error(error);
        res.writeHead(500, {'Content-Type': 'text/html; charset=utf-8'});
        res.end(error.message);
    }
});

server.listen(8080, () => {
    console.log("8080번 포트에서 서버 대기 중입니다.");
})

💻 4.2 REST API와 라우팅

📌 REST API

✏️ 서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현

  • /index.html이면 index.html을 보내달라는 뜻
  • 항상 html을 요구할 필요는 없음
  • 서버가 이해하기 쉬운 주소가 좋음

✏️ REST API(Representational State Transfer)

  • 일종의 주소를 정하는 규칙
  • 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법
  • /user이면 사용자 정보에 관한 정보를 요청하는 것
  • /post면 게시글에 관련된 자원을 요청하는 것
  • 체계적이고 구조적으로 쉽게 만들어진 만큼 해킹의 위험도 따름

✏️ HTTP 요청 메서드

  • GET: 서버 자원을 가져오려고 할 때 사용
  • POST: 서버에 자원을 새로 등록하고자 할 때 사용(또는 뭘 써야할 지 애매할 때)
  • PUT: 서버의 자원을 요청에 들어있는 자원으로 치환하고자할 때 사용(전체 수정 시)
  • PATCH: 서버 자원의 일부만 수정하고자 할 때 사용(부분 수정 시)
  • DELETE: 서버의 자원을 삭제하고자할 때 사용

📌 HTTP 프로토콜

✏️ 클라이언트가 누구든 서버와 HTTP 프로토콜로 소통 가능

  • iOS, 안드로이드, 웹이 모두 같은 주소로 요청 보낼 수 있음
  • 서버와 클라이언트의 분리

✏️ RESTful

  • REST API를 사용한 주소 체계를 이용하는 서버
  • GET /user는 사용자를 조회하는 요청, POST /user는 사용자를 등록하는 요청
  • 실제로 RESTful한 서버는 보기 어렵기에 최대한 깔끔하고 이해하기 쉽도록 만드는 것이 중요함

💻 4.3 쿠키와 세션 이해하기

📌 쿠키의 필요성

✏️ 요청에는 한 가지 단점이 있음

  • 누가 요청을 보냈는지 모름(IP 주소와 브라우저 정보 정도만 앎)
  • 로그인을 구현하면 됨
  • 쿠키와 세션이 필요

✏️ 쿠키: 키=값의 쌍

  • name=zerocho
  • 매 요청마다 서버에 동봉해서 보냄
  • 서버는 쿠키를 읽어 누구인지 파악

📌 쿠키 서버 만들기

✏️ 쿠키 넣는 것을 직접 구현

  • writeHead: 요청 헤더에 입력하는 메서드
  • Set-Cookie: 브라우저에게 쿠키를 설정하라고 명령

✏️ 쿠키: 키=값의 쌍

  • name=zerocho
  • 매 요청마다 서버에 동봉해서 보냄
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번 포트에서 서버 대기 중입니다!');
  });

📌 헤더와 본문

✏️ http 요청과 응답은 헤더와 본문을 가짐

  • 헤더는 요청 또는 응답에 대한 정보를 가짐
  • 본문은 주고받는 실제 데이터
  • 쿠키는 부가적인 정보이므로 헤더에 저장

📌 http 상태코드

✏️ writeHead 메서드에 첫 번째 인수로 넣은 값

  • 요청이 성공했는지 실패했는지를 알려줌
  • 2XX: 성공을 알리는 상태 코드입니다. 대표적으로 200(성공), 201(작성됨)이 많이 사용됩니다.
  • 3XX: 리다이렉션(다른 페이지로 이동)을 알리는 상태 코드입니다. 어떤 주소를 입력했는데 다른 주소의 페이지로 넘어갈 때 이 코드가 사용됩니다. 대표적으로 301(영구 이동), 302(임시 이동)가 있습니다.
  • 4XX: 요청 오류를 나타냅니다. 요청 자체에 오류가 있을 때 표시됩니다. 대표적으로 401(권한 없음), 403(금지됨), 404(찾을 수 없음)가 있습니다.
  • 5XX: 서버 오류를 나타냅니다. 요청은 제대로 왔지만 서버에 오류가 생겼을 때 발생합니다. 이 오류가 뜨지 않게 주의해서 프로그래밍해야 합니다. 이 오류를 클라이언트로 res.writeHead로 직접 보내는 경우는 없고, 예기치 못한 에러 발생 시 서버가 알아서 5XX대 코드를 보냅니다. 500(내부 서버 오류), 502(불량 게이트웨이), 503(서비스를 사용할 수 없음)이 자주 사용됩니다.

📌 세션 사용하기

✏️ 쿠키의 정보는 노출되고 수정되는 위험이 있음

  • 중요한 정보는 서버에서 관리하고 클라이언트에는 세션 키만 제공
  • 서버에 세션 객체(session) 생성 후, uniqueInt(키)를 만들어 속성명으로 사용
  • 속성 값에 정보 저장하고 uniqueInt를 클라이언트에 보냄

💻 4.4 https와 http2

📌 https

✏️ 웹 서버에 SSL 암호화를 추가하는 모듈

  • 오고 가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없음
  • 요즘에는 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

✏️ SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용하는 모듈

  • 요청 및 응답 방식이 기존 http/1.1보다 개선됨
  • 웹의 속도도 개선됨

📌 http2 적용 서버

✏️ https 모듈을 http2로, createServer 메서드를 createSecureServer 메서드로

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 코어를 모두 사용할 수 있게 해주는 모듈

  • 포트를 공유하는 노드 프로세스를 여러 개 둘 수 있음
  • 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산됨
  • 서버에 무리가 덜 감
  • 코어가 8개인 서버가 있을 때: 보통은 코어 하나만 활용
  • cluster로 코어 하나당 노드 프로세스 하나를 배정 가능
  • 성능이 8배가 되는 것은 아니지만 개선됨
  • 단점: 컴퓨터 자원(메모리, 세션 등) 공유 못 함
  • Redis 등 별도 서버로 해결
  • 하나의 포트에서 여러개의 서버를 사용 가능

📌 서버 클러스터링

✏️ 마스터 프로세스와 워커 프로세스

  • 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만듦(worker_threads랑 구조 비슷)
  • 요청이 들어오면 워커 프로세스에 고르게 분배
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}번 워커 실행`);
}

📌 워커 프로세스 개수 확인

✏️ 요청이 들어올 때마다 서버 종료되도록 설정

  • 실행한 컴퓨터의 코어가 8개이면 8번 요청을 받고 종료됨

📌 워커 프로세스 다시 살리기

✏️ 워커가 죽을 때마다 새로운 워커를 생성

  • 이 방식은 좋지 않음
  • 오류 자체를 해결하지 않는 한 계속 문제가 발생
  • 하지만 서버가 종료되는 현상을 막을 수 있어 참고할 만함.
profile
조금씩 정리하자!!!

0개의 댓글