상세 컨텐츠

본문 제목

[노드 1팀] 4장. http 모듈로 서버 만들기

24-25/Node.js 1

by gooroominuna 2024. 11. 15. 10:00

본문

728x90

4.1 요청과 응답 이해하기

클라이언트와 서버

클라이언트에서 서버에 요청(request)을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답(response)를 보낸다.  ∴  서버에 요청 받는 부분 & 응답 보내는 부분이 필요하다.

(요청과 응답은 이벤트 방식이라고 생각하면 된다.)

 

이벤트 리스너를 가진 노드 서버 만들기

  • http 모듈을 사용한다.  ∵ http 서버가 있어야 웹 브라우저의 요청을 처리할 수 있기 때문이다.
  • http 모듈의 createServer() 메서드 : 인수로 요청에 대한 콜백 함수 삽입 가능. 이 콜백 함수에 응답을 적으면 된다.
  • listen(클라이언트에 공개할 포트 번호, 포트 연결 완료 후 실행될 콜백 함수);
  • res.writeHead() : 응답에 대한 정보를 기록한다. → 정보가 기록되는 부분을 헤더(header)라고 한다.                                     
  • res.write() : 클라이언트로 보낼 데이터를 인수로 전달한다. → 데이터가 기록되는 부분을 본문(body)이라고 한다.
  • res.end() : 인수가 있다면 그 데이터도 클라이언트로 보내고 응답을 종료한다.

cf ) http://gilbut.co.kr과 같은 사이트는 80번 포트를 사용하며, 생략 가능하다. https는 443번 포트를 사용하고 생략 가능하다.

 

 

1) listen 메서드에 콜백 함수 삽입

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'}); // 200 : 성공적인 요청임을 의미, 콘텐츠 형식이 HTML임을 알림. 한글 표시를 위해 utf-8로 설정. 
    res.write('<h1>Hello Node!</h1>');
    res.end('<p>Hello Server!</p>'); // 인수를 클라이언트로 보내고 응답 종료.
})

.listen(8080, () => { // 서버 연결
    console.log('8080번 포트에서 서버 대기 중입니다!');
});

 

2) listen 메서드에 콜백 함수 대신, listening 이벤트 리스너 사용

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를 여러 번 호출하여, 한 번에 여러 서버를 실행할 수도 있다. `

 

3) HTML 파일을 fs 모듈로 읽어 전송

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

 

=> 요청이 들어오면 fs 모듈로 HTML 파일을 읽고, 저장된 버퍼를 그대로 클라이언트에 전송한다.

     에러 메시지는 일반 문자열이므로 text/plain을 사용한다.

 

4.2 REST와 라우팅 사용하기

REST

REpresentational State Transfer

서버의 자원을 정의하고, 자원에 대한 주소를 지정하는 방법을 가리킨다. 

* 자원 : 서버가 행할 수 있는 것들

 

주소는 의미를 명확히 전달하기 위해 명사로 구성된다. ex) /user, /post

📌  REST에서는 주소 외에도 HTTP 요청 메서드를 사용한다.

 

HTTP 요청 메서드

  • GET : 리소스 조회, 요청 본문에 데이터 X
  • POST : 서버에 리소스를 새로 등록, 요청의 본문에 등록할 데이터 삽입
  • PUT : 리소스 수정, 요청의 본문에 치환할 데이터 삽입
  • PATCH : 리소스 일부만 수정, 요청의 본문에 수정할 데이터 삽입
  • DELETE : 리소스 삭제, 요청 본문에 데이터 X
  • OPTIONS : 서버와 브라우저가 통신하기 위한 통신 옵션 확인

위의 메서드로 표현하기 애매한 로그인 같은 동작 → POST 사용

 

REST 장점

  • 주소와 메서드만 보고 요청 내용을 알아볼 수 있다.
  • GET 메서드는 브라우저에서 캐싱할 수 있어 같은 주소로 GET 요청할 시, 캐시에서 가져올 수 있다. → 성능 ↑
  • HTTP 통신 : 서버와 클라이언트가 분리되어 있다. → 서버 확장 시, 클라이언트에 구애되지 않는다.

 

 REST에 기반한 서버 주소 구조

 

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') {
            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));
            }
            try {
                const data = await fs.readFile(path.join(__dirname, req.url));
                return res.end(data);
            } catch (err) {

            }
        } else if (req.method == 'POST') {
            if(req.url === '/user') {
                let body = '';
                req.on('data', (data) => {
                    body += data;
                });
                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(201, { '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': 'application/json; 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);
        res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
        res.end(err);
    }
})
.listen(8082, () => {
    console.log('8082번 포트에서 서버 대기 중입니다');
});

 

=> req.method로 HTTP 요청 메서드를 구분한다.

     메서드가 GET인 경우, 요청 주소를 구분하고 주소에 맞는 파일을 제공한다.

     존재하지 않는 파일 요청했거나 GET 메서드 요청 아닌 경우라면 404 NOT FOUND 에러를 응답으로 전송한다.

 

📌 POST, PUT 요청 처리

req.on('data'), req.on('end') → 요청 본문에 들어있는 데이터를 꺼내기 위한 작업이다.

req와 res도 내부적으로 readStream, writeStream으로 되어 있으므로 요청/응답 데이터가 스트림 형식으로 전달된다.

 

▶ 위 서버를 실행하고, 크롬의 개발자 도구 Network 탭을 통해 Method를 확인해보자.

 

 

4.3 쿠키와 세션 이해하기

HTTP 프로토콜 환경은 "connectionless, stateless"한 특성을 가지므로 서버는 클라이언트가 누구인지 매번 확인해야 한다.

이 문제를 보완하기 위해 쿠키와 세션을 사용한다.

쿠키

쿠키는 클라이언트(브라우저)에 저장되는 키와 값이 들어있는 작은 데이터 파일이다.

  1. 서버는 웹 브라우저에 사용자를 추정할 만한 정보를 쿠키로 만들어 보낸다.
  2. 웹 브라우저는 쿠키를 저장해뒀다가 다음에 요청할 때마다 쿠키를 같이 전송한다.
  3. 서버는 요청에 들어 있는 쿠키를 읽어 사용자를 파악한다.

 

 

※ 우리는 서버에서 브라우저로 쿠키를 보낼 때만 코드를 작성해 처리하면 된다. 

쿠키는 요청의 헤더에 담겨 전송되고, 브라우저는 응답의 헤더에 따라 쿠키를 저장한다.

 

다음은 서버에서 직접 쿠키를 만들어 요청자의 브라우저에 넣는 코드이다.

const http = require('http');

http.createServer((req, res) => {
    console.log(req.url, req.headers.cookie); // requ 객체에 담겨 있는 쿠키를 가져옴.
    res.writeHead(200, { 'Set-Cookie': 'mycookie=test' }); // 응답을 받은 브라우저는 쿠키 저장. 쿠키 간에는 세미콜론을 넣어 각각을 구분
    res.end('Hello Cookie');
})
.listen(8083, () => {
    console.log('8083번 포트에서 서버 대기 중입니다!');
})

 

실행하면 화면에 Hello Cookie가 출력되고 콘솔에서는 다음과 같은 메시지가 뜬다.

 

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

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

 

📌 /favicon.ico

브라우저가 *파비콘이 뭔지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보낸다.

위 코드 HTML에서 파비콘 정보가 없으므로 브라우저가 추가로 /favicon.ico를 요청한 것이다.

 

* 파비콘 : 웹 사이트 탭에 보이는 이미지

 

 

쿠키를 심고, 사용자를 식별하는 방법을 알아보자.

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

 

  • 주소가 /login과 /로 시작하는 것 2개이므로 주소별로 분기 처리했다.

       → http://localhost:8084에 접속하면, /로 요청 전송.

           cookie2.html에서 form을 통해 로그인 요청 보낼 시, /login으로 요청 전송.

  • /로 접속한 경우, 쿠키의 유무 확인 → 쿠키가 없다면 로그인할 수 있는 페이지(cookie.html)를 보낸다.

쿠키 설정 옵션

  • 쿠키명=쿠키값
  • Expires=날짜 : 만료 기한, 기본값은 클라이언트가 종료될 때까지.
  • Max-age=초 : 날짜 대신 초 입력.
  • Domain=도메인명 : 쿠키가 전송될 도메인 특정.
  • Path=URL : 쿠키가 전송될 URL 특정.
  • Secure : HTTPS일 경우만 쿠키 전송.
  • HttpOnly : 자바스크립트에서 쿠키 접근 불가. 쿠키 조작 방지 목적.

세션

세션이란, 웹에 접근하는 사용자의 상태 및 정보를 서버측에 저장하고 이용하는 것이다.

 

개인정보를 쿠키에 넣어두는 것은 적절하지 못하며, 쿠키가 조작될 위험이 있다. 

서버가 사용자 정보를 관리하도록 하자.

 

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

 

바로 이전 코드와 비교하면, 쿠키에 이름을 담아서 보내는 대신 uniqueInt라는 숫자 값을 보낸다.

사용자의 이름 및 만료 시간은 uniqueInt 속성명 아래의 session 객체에 대신 저장한다.

cookie.session이 있고 만료 기한을 넘기지 않았다면 session 변수에서 사용자 정보를 가져와 사용한다.

 

=> 서버에 사용자 정보를 저장하고, 클라이언트와는 세션 아이디로만 소통한다.

 

※ 실제 배포용 서버에서는 세션을 변수에 저장하지 않는다. 레디스나 멤캐시드와 같은 데이터에 저장해둔다.

 

4.4 https와 http2

https 모듈

https 모듈은 웹 서버에 SSL 암호화를 추가한다. GET/POST 요청 시, 오가는 데이터를 암호화하여 내용을 확인할 수 없게 한다.

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, () => { // 실제 서버에서는 80번 포트 대신 443번 포트 사용
    console.log('443번 포트에서 서버 대기 중입니다!');
  });

 

위 코드에서 createServer 메서드는 인수를 2개 받는다. 첫 번째 인수는 인증서에 관련된 옵션 객체로, 인증서 구입 시 제공된 파일들을 fs.readFileSync 메서드로 읽어 cert, key, ca 옵션에 알맞게 넣으면 된다.

 

http2 모듈

노드의 http2 모듈은 SSL 암호화 + 최신 HTTP 프로토콜 http/2를 사용할 수 있게 한다. → 웹 속도 개선

 

위 코드에서 http2를 적용하려면 다음 2가지를 수정하면 된다.

- https 모듈 → http2

- createServer() → createSecure()

 

4.5 cluster

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

 

📌 클러스터

마스터 프로세스 : CPU 개수만큼 워커 프로세스 생성, 8086번 포트에서 대기, 요청을 워커 프로세스에 분배.

워커 프로세스 : 실질적인 일 수행.

 


Quiz

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

2. HTTP 요청 메서드 중 리소스를 가져올 때 사용하고, 요청 본문에 데이터를 넣지 않는 메서드는 ( GET )이다.

3. 쿠키란 ( 웹 브라우저 )에 저장되는 데이터 파일이고, 요청의 ( 헤더 )에 담겨 전송된다.

4. ( 브라우저 )는 파비콘이 무엇인지 유추할 수 없으면 ( 서버 )에 파비콘 정보에 대한 요청을 보낸다.

5. 쿠키의 옵션은 ( 세미콜론 )으로 구분하고, 대표적으로 ( 한글 ) 과  ( 줄바꿈 )은 쿠키에 들어갈 수 없다.

6. https 모듈은 웹 서버에 ( SSL 암호화 )를 추가한다.

7. cluster에는 ( 마스터 프로세스 )와 ( 워커 프로세스 )로 구분된다.

 

Programming Quiz

1. 이벤트 리스너를 가진 노드 서버를 만드시오. 단, 서버에 listening 이벤트 리스너와 error 이벤트 리스너를 붙이시오.

 

2. 다음을 만족하는 HTTP 서버를 만드시오.

    - 서버는 8080 포트에서 실행

    - / 경로로 요청이 오면 "Welcome to the Home Page!"라는 응답 전송

    - /about 경로로 요청이 오면 "About Us"라는 응답 전송

    - 그 외의 경로로 요청이 오면 "Page Not Found"라는 응답과 함께 상태 코드 404를 반환

 

 


 

1번 답

예제  코드 server1-1.js 확인

 

2번 답

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/') {  // 홈 경로
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end("Welcome to the Home Page!");  // 홈 응답 메시지

  } else if (req.url === '/about') {  // About 페이지 경로
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end("About Us");  // About 페이지 응답 메시지

  } else {  // 그 외 경로
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end("Page Not Found");  // 404 에러 메시지
  }
});

server.listen(8080, () => {
  console.log('서버가 8080번 포트에서 실행 중입니다.');
});

 

 


출처 :  조현영,  Node.js 교과서 개정 3판, 길벗(2022)

Corner Node.js 1
Editor : Oze

728x90

관련글 더보기