본문 바로가기

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

[2023 백엔드 스터디] 김민선 #1주차 - 4장.HTTP 모듈로 서버 만들기

반응형

4.1 요청과 응답 이해하기

🌕  서버와 클라이언트 소통 순서

1. 클라이언트가 서버로 요청(request) 전송

2. 서버가 요청을 처리

3. 서버가 클라이언트로 응답(response) 전송

 

🌕  노드로 http 서버 만들기

  • createServer로 요청 이벤트에 대기
  • req 객체 : 요청(request)에 관한 정보
  • res 객체: 응답(response)에 관한 정보
const http = require("http");

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

 

🌕  8080 포트 연결하기

  • res 메서드로 응답 보냄
    • write로 응답 내용을 적고, end로 응답 마무리(내용을 넣어도 됨)
  • listen(포트) 메서드로 특정 포트에 연결
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번 포트에서 서버 대기 중입니다!');
  });

 

🌕  이벤트 리스너 붙이기

  • listening과 error 이벤트 추가 가능
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를 여러 번 호출하면 됨(두 서버의 포트를 다르게 지정)
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번 포트에서 서버 대기 중입니다!');
  });

 

🌕  HTML 읽어서 전송하기

  • write와 end에 문자열을 넣는 것은 비효율적
    • fs 모듈로 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번 포트에서 서버 대기 중입니다!');
  });

 

🌕  HTTP 상태 코드

  • writeHead 메서드에 첫 번째 인수로 넣은 값, 요청이 성공했는지 실패했는지를 알려줌
  • 2XX: 성공을 알리는 상태 코드, 200(성공), 201(작성됨)
  • 3XX: 리다이렉션(다른 페이지로 이동)을 알리는 상태 코드, 어떤 주소를 입력했는데 다른 주소의 페이지로 넘어갈 때 이 코드 사용, 301(영구 이동), 302(임시 이동)
  • 4XX: 요청 오류를 나타내는 상태 코드, 요청 자체에 오류가 있을 때 표시, 401(권한 없음), 403(금지됨), 404(찾을 수 없음)
  • 5XX: 서버 오류를 나타내는 상태 코드, 요청은 제대로 왔지만 서버에 오류가 생겼을 때 발생, 클라이언트로 res.writeHead로 직접 보내는 경우 없음, 500(내부 서버 오류), 502(불량 게이트웨이), 503(서비스를 사용할 수 없음)

 

4.2 REST와 라우팅 사용하기

🌕  REST API

  • 서버에서 요청을 보낼 때는 주소를 통해 요청의 내용을 표현
    • 주소가 /index/html이면 index/html을 보내 달라는 뜻
    • 항상 html을 요구할 필요 x, 서버가 이해하기 쉬운 주소 GOOD
  • REST API(REpresentational State Transfer)
    • 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법
    • 명사 사용
    • /user면 사용자 정보에 관한 내용을 요청하는 것
    • /post면 게시글에 관련된 자원을 요청하는 것

 

🌕  HTTP 요청 메서드

  • GET: 서버 자원을 가져오고자 할 때 사용
  • POST: 서버에 자원을 새로 등록하고자 할 때 사용(또는 뭘 써야 할지 애매할 때)
  • PUT: 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할 때 사용
  • PATCH: 서버 자원의 일부만 수정하고자 할 때 사용
  • DELETE: 서버의 자원을 삭제하고자 할 때 사용
  • OPTIONS: 요청을 하기 전에 통신 옵션 설명하기 위해 사용

 

🌕  HTTP 프로토콜

  • 클라이언트가 누구든 서버와 HTTP 프로토콜로 소통 가능
    • ios, 안드로이드, 웹이 모두 같은 주소로 요청 보낼 수 있음
    • 서버와 클라이언트의 분리
  • RESTful
    • REST API를 사용한 주소 체계를 이용하는 서버
    • REST를 따르는 서버를 ‘RESTful하다’고 표현
    • GET /user는 사용자를 조회하는 요청, POST /user는 사용자를 등록하는 요청

 

🌕  REST 서버 만들기

  • GET 메서드에서 /, /about 요청 주소는 페이지를 요청하는 것이므로 HTML 파일을 읽어서 전송, AJAX 요청을 처리하는 /users에서는 users 데이터를 전송, JSON 형식으로 보내기 위해 JSON.stringify 사용, 그 외의 GET 요청은 CSS나 JS 파일을 요청하는 것이므로 찾아서 보내주고, 없다면 404 NOT FOUND 에러 응답
  • POST와 PUT 메서드는 클라이언트로부터 데이터를 받으므로 특별한 처리 필요, req.on('data', 콜백)과 req.on('end', 콜백), 3.6.2절의 버퍼와 스트림에서 배웠던 readStream, readStream으로 요청과 같이 들어오는 요청 본문을 받을 수 있음, 단, 문자열이므로 JSON으로 만드는 JSON.parse 과정 한 번 필요
  • DELETE 메서드로 요청이 오면 주소에 들어 있는 키에 해당하는 사용자 제거
  • 해당하는 주소가 없을 경우 404 NOT FOUND 에러 응답
const http = require('http');
const fs = require('fs').promises;
const path = require('path');

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

http.createServer(async (req, res) => {
  try {
    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': 'application/json; 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/plain; 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/plain; 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/plain; 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번 포트에서 서버 대기 중입니다');
  });

 

🌕  REST 요청 확인하기

  • 개발자도구(F12) Network 탭에서 요청 내용 실시간 확인 가능
    • Name은 요청 주소, Method는 요청 메서드, Status는 HTTP 응답 코드
    • Protocol은 HTTP 프로토콜, Type은 요청 종류(xhr은 AJAX 요청)

 

🌕  헤더와 본문

  • http 요청과 응답은 헤더와 본문을 가짐
    • 헤더는 요청 또는 응답에 대한 정보를 가짐
    • 본문은 주고받는 실제 데이터

 

4.3 쿠키와 세션 이해하기

🌕  쿠키의 필요성

  • 요청의 한가지 단점
    • 누가 요청을 보냈는지 알 수 없음
    • 로그인 구현으로 해결, 쿠키와 세션 필요
  • 쿠키: 키=값의 쌍
    • name=zerocho
    • 매 요청마다 서버에 동봉해서 보냄
    • 서버는 쿠키를 읽어 누구인지 파악

 

🌕  쿠키 서버 만들기

  • 쿠키 넣는 것을 직접 구현
    • writeHead: 요청 헤더에 입력하는 메서드
    • 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번 포트에서 서버 대기 중입니다!');
  });

 

🌕  쿠키로 나를 식별하기

  • 쿠키에 내 정보를 입력
    • parseCookies: 쿠키 문자열을 객체로 변환
    • 주소가 /login인 경우와 /인 경우로 나뉨
    • /login인 경우 쿼리스트링으로 온 이름을 쿠키로 저장
    • 그 외의 경우 쿠키가 있는지 없는지 판단, 있으면 환영 인사, 없으면 로그인 페이지로 리다이렉트
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' }
  // 주소가 /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();
  // 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(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 시 다양한 옵션이 있음
    • 쿠키명=쿠키값: 기본적인 쿠키의 값, mycookie=test 같이 설정
    • Expires=날짜: 만료 기한, 이 기한이 지나면 쿠키 제거, 기본값은 클라이언트가 종료될 때까지
    • Max-age=초: Expires와 비슷, 날짜 대신 초 입력, 해당 초가 지나면 쿠키 제거, Expires보다 우선
    • Domain=도메인명: 쿠키가 전송될 도메인 특정, 기본값은 현재 도메인
    • Path=URL: 쿠키가 전송될 URL 특정, 기본값은 ‘/’, 이 경우 모든 URL에서 쿠키 전송
    • Secure: HTTPS일 경우에만 쿠키 전송
    • HttpOnly: 설정 시 자바스크립트에서 쿠키 접근 불가능, 쿠키 조작 방지 위해 설정

 

🌕  세션 사용하기

  • 쿠키의 정보는 노출되고 수정되는 위험
    • 중요한 정보는 서버에서 관리하고 클라이언트에는 세션 키만 제공
    • 서버에 세션 객체(session) 생성 후, uniqueInt(키)를 만들어 속성명으로 사용
    • 속성 값에 정보 저장하고 uniqueInt를 클라이언트에 보냄
  • 실 서버에서는 express-session 사용하기
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번 포트에서 서버 대기 중입니다!');
  });

 

4.4 HTTPS와  HTTP2

🌕  HTTPS

  • 웹 서버에 SSL 암호화를 추가하는 모듈
    • 오고 가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없음
    • 요즘에는 https 적용이 필수(개인 정보가 있는 곳은 특히)

 

🌕  HTTPS 서버

  • http 서버를 https 서버로
    • 암호화를 위해 인증서가 필요, 발급받아야 함
  • createServer가 인자를 두 개 받음
    • 첫 번째 인자는 인증서와 관련된 옵션 객체
    • pem, crt, key 등 인증서를 구입할 때 얻을 수 있는 파일 넣기
    • 두 번째 인자는 서버 로직
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 등 별도 서버로 해결

 

🌕  서버 클러스터링

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

 


출처

Node.js 교과서 개정 3판(조현영)

https://thebook.io/080334/

반응형