본문 바로가기

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

[2024-2 Node.js 스터디] 류상우 #2주차

반응형

4장 http 모듈로 서버 만들기

 

4-1. 요청과 응답 이해하기

내용이 어렵지는 않았어서 본문을 읽어본 뒤에 바로 코드를 작성해보았다.

localhost:8080 localhost:8081

res가 json인 경우가 많아서 json파일을 임의로 만들어 사용해봤는데 잘 작동했다.

 

코드 살펴보기

  • require('http'): node의 http 모듈을 불러온다. 당연하지만 node 필요
  • http.createServer(): http 모듈의 메서드로 서버를 만든다. listen 메서드에서 포트를 결정한다.
  • req, res: 각각 request(요청), response(응답)
    • res.writeHead(): 첫 번째 인수에 HTTP 상태 코드(ex. 200: 성공), 두 번째 인수에 응답의 헤더를 보낸다.
    • res.write(): 코드에는 없지만 응답의 바디를 보낸다. 여러 번 호출 가능하고 버퍼나 문자열 등을 보낸다.
    • res.end(): 응답을 종료한다. 만약 인수가 있다면 해당 인수까지 보낸 후 종료한다.
  • listen(): 인수로는 연결할 포트를 작성하고, 콜백 함수에 연결에 성공했을 때 실행할 코드를 작성한다.
  • promise/async,await: 2.1.7, 2.1.8에 있는 내용. 비동기 작업을 위함.
    • promise: 비동기 작업의 완료나 실패를 처리할 수 있게 해주는 객체. require('fs').promises 는 node.js에서 fs의 promise 기반 API를 사용할 수 있게 해준다.
    • async: 함수 앞에 async를 붙이면 그 함수는 비동기 함수가 된다.
    • await: 비동기 작업을 처리할 때, 작업이 완료될 때까지 기다렸다가 그 다음 코드를 실행하게 한다.

 

HTTP 상태 코드

상태 코드 의미
200 OK 요청 메시지에 대한 성공적인 응답을 나타내는 메시지.
201 Created 요청이 성공했고 자원을 서버 내에 만들었다. PUT 메소드에 대한 전형적인 응답.
301 Moved Permanently 요청한 자원이 새로운 URL로 영구히 이동했다. 보통은 404 에러를 발생시킨다
302 Found 요청한 자원이 일시적으로 다른 URL을 사용하고 있다.
403 Forbidden 서버가 요청을 거부했다.
404 Not Found 요청한 자원을 찾을 수 없다.
500 Internal Server Error 서버 문제로 인해 요청을 수행할 수 없다.

4-2. REST와 라우팅 사용하기

REST(REpresentational State Transfer): 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법. 이 방법을 따르는 서버를 RESTful하다고 표현.

  • GET: 서버 자원을 가져오고자 할 때 사용한다. 요청의 본문(body)에 데이터를 넣지 않는다. 데이터를 서버로 보내야 한다면 쿼리스트링을 사용한다.
  • POST: 서버에 자원을 새로 등록하고자 할 때 사용한다. 요청의 본문에 새로 등록할 데이터를 넣어 보낸다.
  • PUT: 서버의 자원을 요청에 들어 있는 자원으로 치환하고자 할 때 사용한다. 요청의 본문에 치환할 데이터를 넣어 보낸다.
  • PATCH: 서버 자원의 일부만 수정하고자 할 때 사용한다. 요청의 본문에 일부 수정할 데이터를 넣어 보낸다.
  • DELETE: 서버의 자원을 삭제하고자 할 때 사용한다. 요청의 본문에 데이터를 넣지 않는다.
  • OPTIONS: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용한다.

 

책에 나온 코드를 그대로 옮겨두었다.

HTTP 메서드 주소 역할
GET / restFront.html 파일 제공
GET /about about.html 파일 제공
GET  /users 사용자 목록 제공
GET 기타 기타 정적 파일 제공
POST /user 사용자 등록
PUT /user/사용자id 해당 id의 사용자 수정
DELETE /user/사용자id 해당 id의 사용자 제

API 명세서와 프론트의 각 구간이 어떤 역할인지 표시한 사진이다.

users를 반환하는 GET 말고도 Home 페이지와 About 페이지도 GET으로 불러왔다.

 

그냥 코드를 긁어서 복사해둔 후 처음 실행했을 때는 당연히 restFront.html이 별개인줄 알아서 html파일과 node.js 파일을 따로 실행했는데, html을 별개로 실행할 필요 없이 서버에서 반환해주는 구조였다.

 

restServer.js 코드 자체는 길이는 길었지만 로직 자체는 어렵지는 않서 크게 눈여겨 볼 곳은 없었는데, 왜 switch 문이 아닌 else if 문을 사용했는지 모르겠다.

코드 일부

그래서 switch 문으로 바꿔서 작성해봤는데 정상적으로 작동하기도 하고 내 기준으로는 switch 문의 가독성이 훨씬 좋기 때문에 이 방식을 선호할 것 같다.

 

이렇게 개발자 도구의 Network 탭에서 어떤 통신이 있었는지 확인할 수 있다.

API 연동할 때 오류가 나면 저 미리보기 부분을 백엔드 분들에게 보내주었던 기억이 있다.

 

그리고 내가 못 찾은 건지 책에는 라우팅에 대한 개념이 없었는데 라우팅이란  HTTP 요청에 대해 해당 요청을 적절한 처리 함수로 연결해주는 과정이다


4-3. 쿠키와 세션 이해하기

쿠키: 웹 브라우저와 서버 간의 통신에서 사용자의 정보를 저장하고 관리하기 위한 작은 데이터 파일

웹 페이지에서 종종 나오는 쿠키를 허용하시겠습니까?의 그 쿠키이다. 왜 이런 이름이 붙었을까 궁금해 찾아보니 1970년대 멀티시스템 환경에서 데이터를 주고받는 데이터 조각을 뜻하는 "Magic Cookie"에서 유래됐다고 한다. 또한 쿠키라는 단어 자체도 서로 주고받을 수 있는 작은 선물이라는 뜻으로 봐도 된다.

참고로 뉴진스의 Cookie를 들으며 작성 중이다.

 

우선 쿠키를 지우고

확인을 해봤는데 책에서와 다른 결과가 나왔다. 아마 webstorm때문인 듯 한데 쿠기가 없는 상태를 확인하는 게 중요할 것 같지는 않아서 넘어가기로 했다.

쿠키를 설정하는 파일을 만들어준 뒤 로그인을 하면 새로고침을 하거나 서버를 재시작해도 쿠키가 남아있어 로그인이 유지되는 모습을 볼 수 있다.

 

쿠키는 헤더 부분에 들어있어 req.headers.cookie로 읽거나 res.writeHead()로 보낸다.

 

쿠키 옵션

 

그런데 이 방식은 원하는 대로 동작하기는 하지만 쿠키가 노출되어 있 위험하다.

 

이를 보완하기 위해 사용하는 게 세션이다.

 

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

 

하지만 이 방식도 세션 ID가 노출되어 있기 때문에 안전하지는 않다. 이는 테스트를 위한 코드일 뿐이니 안전하게 사용하려면 다른 모듈 등을 사용해야 한다.


4-4. https와 http2

https 모듈은 웹 서버에 암호화를 추가한다. GET이나 POST 요청을 할 때 오가는 데이터를 암호화해서 타인이 요청을 가로채도 내용을 확인할 수 없게끔 한다.

https가 적용되면 위와 같이 자물쇠 모양을 확인할 수 있다.

 

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

 

인증서가 있으면 pem, crt, key 등의 확장자를 가진 파일을 제공하는데 이를 읽어서 cert, key, ca 옵션에 알맞게 넣어주면 된다. 또한 80이 아닌 443 포트를 사용한다.

 

http2는 암호화도 제공하고 기존 http/1.1 보다 개선된 성능을 보여준다.

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

https를 http2로 createServer를 createSecureServer로 바꾸기만 하면 된다.


4-5. Cluster

Cluster 모듈은 기본적으로 싱글 프로세스로 동작하는 node가 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);
        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(8080);

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

cluster로 워커를 CPU의 코어 갯수만큼 생성하고 요청이 들어오면 프로세스를 종료한 뒤 다시 생성한다. 이러면 예기치 못한 오류로 서버가 종료되는 것을 막을 수 있다.

반응형