본문 바로가기

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

[2024-2 Node.js 스터디] 김민재 #2주차 - HTTP 모듈로 서버 만들기

반응형

1. 요청과 응답 이해하기


  • 클라이언트와 서버는 요청과 응답을 주고 받는다
    • 고로 이벤트 리스너를 가진 노드 서버를 만들어 확인해보자
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번 포트에서 서버 대기 중입니다!');
});
- req(uest): 요청에 관한 정보
- res(ponse): 응답에 관한 정보
    - res.writeHead: 응답에 대한 정보를 기록하는 메서드 (헤더에 기록)
    - res.write: 데이터를 클라이언트로 보내는 메서드 (본문에 기록)
    - res.end: 응답을 종료하는 메서드
- listen: 포트와 콜백 함수를 보내놓고 요청을 대기함

책의 코드를 따라 첫 서버를 실행했다! (근데 localhost랑 포트가 있는데 뭘까?

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

  • 이벤트 리스너
    • **server.on()**은 이벤트 리스너를 설정하기 위해 사용하는 메서드이다 - 특정 이벤트가 발생할 때 해당 이벤트에 등록된 리스너 함수가 호출된다.
    • listening과 error 둘 다 이벤트가 발생할때 콜백 함수를 보내 응답한다
  • res.write와 res.end를 일일이 적는 것은 비효율적이므로 미리 HTML 파일을 만들자!
경로가 ./~ 일때 왜 오류가 날까..??

 

지금까지는 모든 요청에 대해 한 가치 요청밖에 할 수 없었다. 다음 절에서 요청별로 다른 응답을 하는 방법을 알아보자~

 

2. REST와 라우팅 사용하기


  • 서버에 요청을 보낼땐 주소를 통해 요청의 내용을 표현한다. 예를 들어 주소가 /index.html 이면 서버의 index.html을 보내달라는 뜻이다.
  • REST: 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킨다.
    • GET: 서버 자원을 가져오고자 할 때 사용. 본문에 데이터를 넣지 않는다
    • POST: 서버에 자원을 새로 등록할 때 사용. 새로 등록할 데이터를 넣어 보낸다
    • PUT: 서버의 자원을 요청에 들어있는 자원으로 치환할 때 사용. 본문에 수정할 데이터를 넣어 보낸다
    • PATCH: 서버 자원의 일부만 수정 할 때 사용. 본문에 수정할 데이터를 넣어 보낸다
    • DELETE: 서버의 자원을 삭제할 때 사용. 본문에 데이터를 넣지 않는다
    • OPTIONS: 요청을 하기전 통신 옵션을 설명하기 위해 사용
    • Ex) GET 메서드의 /user 주소로 요청을 보내면 유저의 정보를 가져오는 것임

위 REST를 사용한 주소 체계를 따르는 서버를 ‘RESTful하다’라고 표현한다….

  • 코드를 작성하기 전 주소 구조를 미리 정리해둔 후 시작하면 더욱 체계적이고 수월하다
    • 무작정 시작해서 망한 경험이 있어 더욱 더 와닿는다ㅎ

 

 

  • 우리는 지금 백엔드 공부 중이니 핵심인 restServer.js 를 가지고 왔다
    • 아래의 코드가 복잡해보이지만 이해해보려고 노력했다ㅎ
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번 포트에서 서버 대기 중입니다');
  });

  - res.end 앞에 return을 붙여줘야 함수가 종료된다. end라고 함수가 끝났다고 오해ㄴㄴ
  • method로 HTTP 요청 메서드를 구분하고 있고, if 문을 통해 요청한 메소드와 일치하면 실행시킨다. 만약에 존재하지 않는 파일을 요청하면 404 NOT FOUND 에러가 응답으로 전송된다.
  • 개발자 도구(f12)의 네트워크 창을 통해 요청이 어떻게 되는지 알아봤다!

 

3. 쿠키와 세션 이해하기


  • 클라이언트에서 보내는 요청에는 한 가지 큰 단점이있다. 누가 요청을 보내는지 모른다
    • 쿠키: 웹에 저장되는 작은 데이터 조각. 응답과 요청 시에 쿠키를 함께 전송함 (헤더에 담겨 전송)
    • 세션: 서버 측에서 사용자 데이터를 저장하는 방법. 서버가 고유한 세션 생성하고, 쿠키로 저장
  • → 쿠키와 세션을 이용해 로그인 구현!

  • 즉, 서버는 미리 클라이언트에 요청자를 추청할 만한 정보를 쿠키로 만들어 보내고, 다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악한다. (쿠키가 사용자가 누구인지 추적하고 있다는 것)
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번 포트에서 서버 대기 중입니다!');
  });

위 코드를 실행시키면 볼수 있는 쿠키이다

 

  • 쿠기를 이용해 사용자를 식별하는 방법
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 와 같은 형식으로 설정
- expires=날짜: 만료 기한. 기한이 지나면 쿠키가 제거되고 기본은 클라이언트가 종료될 때 까지
  1. parseCookies 함수를 사용해 문자열인 쿠키를 js 객체 형식으로 바꾼다
  2. 로그인을 위해 GET /login 하는 부분이다. 쿠키의 이름과 유효 시간을 받고, 응답에 응답코드와 함께 보냄
  3. 그 외 / 로 접속했을 때 부분이다. 쿠키의 유무를 확인하고 있으면 쿠키 이름과 함께 응답을 보내고, 만약에 없으면 로그인할 수 있는 페이지를 보낸다.

위 코드를 실행시면 개발자 도구에서 볼 수 있는 쿠키이다. 하지만 볼 수 있다는 것은 큰 단점이 된다…

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


  • http vs https
    • http: 일반 텍스트이므로, 권한이 없는 당사자가 인터넷을 통해 쉽게 액세스하고 읽을 수 있음
    • https: 모든 데이터를 암호화된 형태로 전송 (SSL 암호화)
    • GET, POST 요청할 때 오가는 데이터를 암호화해 가로채도 내용을 확인할 수 없게 함
  • http1 vs http2
    • 노드의 http2 모듈은 암호화와 더불어 최신 프로토콜을 사용할 수 있게 함

 

5. cluster


  • cluster 모듈은 싱글 프로세스로 동작하는 노드가 cpu코어를 모두 사용할 수 있게 해줌
    • 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산 → 서버에 무리가 덜 간다
    • 클러스터엔 마스터 프로세스와 워커 프로세스가 있다.
      • 마스터 프로세스: cpu 개수만큼 워커 프로세스를 만들고 대기, 요청이 들어오면 워커 프로세스에 분배
      • 워커 프로세스: 실적적인 일을 하는 프로세스
  • 코드를 이용해 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>');
      setTimeout(() => { // 워커가 존재하는지 확인하기 위해 1초마다 강제 종료
        process.exit(1);
      }, 1000);
    }).listen(8086);

    console.log(`${process.pid}(번 워커 실행`);
  }

 

6. 함께 보면 좋은 자료


반응형