4. http 모듈로 서버 만들기

최상민·2023년 5월 1일
0

Node.js 교과서

목록 보기
4/9
post-thumbnail

요청과 응답 이해하기


위 그림과 같이 클라이언트에서는 서버로 요청을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답을 보낸다.

요청응답은 이벤트 방식이라고 생각하면 된다. 클라이언트로부터 요청이 왔을 때 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해둬야 한다.

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 서버가 있어야 웹 브라우저의 요청을 처리할 수 있으므로 http 모듈을 사용하였고, createServer 메서드의 인수로 요청에 대한 콜백 함수를 넣고, 응답을 적었다.

createServer의 req와 res라는 매개변수가 있는데, 보통 request를 줄여서 req, response를 줄여서 res라고 표현한다.

res 객체에는 res.writeHead와 res.write, res.end 메서드가 있다.

  • res.writeHead - 응답에 대한 정보를 기록하는 메서드. 첫번째 인수로 성공적인 요청을 의미하는 200(상태코드)을, 두번째 인수로 응답에 대한 정보를 보내는데 콘텐츠의 형식인 HTML임을 알리고 있고, 한글 표시를 위한 charset을 utf-8로 지정했다. 이 정보가 기록되는 부분을 헤더라고 한다.

  • res.write - 메서드의 첫 번째 인수는 클라이언트로 보낼 데이터이다. 위 코드는 HTML 모양의 문자열을 보냈지만 버퍼를 보낼 수도 있다. 또한, 여러 번 호출해서 데이터를 여러 개 보내도 된다. 데이터가 기록되는 부분을 본문이라고한다.

  • res.end - 응답을 종료하는 메서드이다. 만약 인수가 있다면 그 데이터도 클라이언트로 보내고 응답을 종료한다. 위 코드는 Hello Server 문자열까지 클라이언트로 보낸 후 응답을 종료한다.

createServer 메서드 뒤에 listen 메서드를 붙이고 클라이언트에 공개할 포트 번호와 실행될 콜백 함수를 넣고 실행하면 서버는 8080번 포트에서 요청이 오기를 기다리게 된다.

위 코드를 실행 후 http://localhost:8080 또는 http://127.0.0.1:8080에 접속해보자.

아주 잘 실행된 것을 확인할 수 있다. 박수 짝짝짝짝짝

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

위 코드와 같이 listen 메서드에 콜백 함수를 넣는 대신, 서버에 이벤트 리스너를 붙여도 된다.

http.createServer를 여러개 사용하여 여러 서버를 실행할 수도 있다.
※ 하지만 포트 번호는 모두 달라야한다. 포트 번호가 같으면 EADDRINUSE 에러가 발생하게 된다.

이때 동안 res.write와 res.end에 일일이 HTML 문자열을 일일이 적었지만 이게 굉장히 많아지면 복잡하고 귀찮게 된다. 그래서 보통은 HTML 파일을 만들어 놓은 후 불러와서 사용한다.

//server2.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번 포트에서 서버 대기 중');
  });

요청이 들어오면 fs 모듈로 HTML 파일을 읽고, data 변수에 저장된 버퍼를 그대로 클라이언트에 보내게 된다.

REST와 라우팅 사용하기

서버 개발할 때 REST API, RESTful API라는 말들을 한번씩 들어봤을 것이다. 물론 나도 들어보았고, 구글링을 통해 찾아보기도 하였다.

우리는 서버에 요청을 보낼 때 주소를 통해 요청의 내용을 표현한다. 예를들어 주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이고, /about.html이면 about.html을 보내달라는 뜻이다.

이와 같이 요청의 내용이 주소를 통해 표현되므로 서버가 이해하기 쉬운 주소를 사용하는 것이 좋은데, 여기서 REST라는 것이 등장한다.

REST? RESTful?

REST는 REpresentational State Transfer의 줄임말로, 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킨다.

일종의 약속이라고 봐도 무방하며, 자원이라고 해서 꼭 파일일 필요는 없고 서버가 행할 수 있는 것들을 통틀어서 의미한다고 보면 된다고 한다.

REST API에는 많은 규칙이 있지만, 모든 규칙을 지키는 것은 어려우므로 책에선 기본적인 개념만 설명해주고 있다.

주소는 의미를 명확하게 전달하기 위해 명사로 구성된다. 예를들어 /user라면 사용자 정보에 관련된 자원을 요청하는 것이고, /post라면 게시글에 관련된 자원을 요청하는 것이라고 추측할 수 있다.

하지만 단순 명사만 있으면 무슨 동작인지 알기 어려우므로 REST에서는 주소 외에도 HTTP 요청 메서드라는 것을 사용한다.

폼 데이터를 전송할 때 GET 또는 POST 메서드를 지정해본 적이 있을텐데, 여기서 GETPOST 그리고 추가적으로 PUT, PATCH, DELETE, OPTIONS 등의 메서드가 요청 메서드이다.

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

주소 하나는 여러 개의 요청 메서드를 가질 수 있다.
GET 메서드의 /user 주소로 요청을 보내면 사용자 정보를 가져오는 요청이라는 것
POST 메서드의 /user 주소로 요청을 보내면 새로운 사용자를 등록하려 한다는 것

장점

  1. 주소와 메서드만 보고 요청의 내용을 바로 알아볼 수 있다.
  2. GET 메서드의 경우 브라우저에서 캐싱(기억)할 수도 있어 같은 주소로 GET 요청을 할 때 서버에서 가져오는것이 아니라 캐시에서 가져올 수도 있다.(성능 향상)
  3. HTTP 통신을 사용하면 클라이언트가 누구든 상관없이 같은 방식으로 서버와 소통할 수 있다. 즉, 서버와 클라이언트가 분리되어 있어 서버를 확장할 때 클라이언트에 구애되지 않아서 좋다.

REST를 따르는 서버를 'RESTful하다'고 표현한다.

RESTful은 위에서 설명한 것 외에도 REST 방식을 잘 사용하고 따르는 서버를 RESTful하다고 표현하며 RESTful API라고 부르기도 한다.

쿠키와 세션

클라이언트에서 요청을 보낼 때 IP 주소나 브라우저의 정보를 받아올 수는 있지만, 누가 보내는 요청인지는 알 수 없기 때문에 로그인 기능을 통해 알아볼 수 있다.

이러한 로그인 기능을 구현하기 위해선 쿠키세션을 알고 있어야 한다.

우리가 누구인지 기억하기 위해서 서버는 요청에 대한 응답을 할 때 쿠키라는 것을 같이 보낸다.
쿠키는 유효기간이 있고, name=zerocho와 같은 '키-값'의 쌍이다.

서버로부터 쿠키가 오면, 웹 브라우저는 쿠키를 저장해뒀다가 다음에 요청할 때마다 쿠키를 동봉해서 보낸다. 서버는 요청에 들어 있는 쿠키를 읽어서 사용자가 누구인지 파악한다.

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

const http = require('http');

http.createServer((req, res) => {
	console.log(req.url, req.header.cookie);
    res.writeHead(200, { 'Set-Cookie': 'mycookie=test' });
    res.end('Hello Cookie');
})
  .listen(8083, () => {
    console.log('8083번 포트에서 서버 대기 중');
  });

위 코드는 간단하게 서버에서 직접 쿠키를 만들어 요청자의 브라우저에 넣는 코드이다.

쿠키는 요청과 응답의 헤더를 통해 오간다고 하였으므로 res.writeHead 메서드를 사용하였다.
Set-Cookie는 브라우저에게 다음과 같은 쿠키를 저장하라는 의미이며 mycookie=test라는 쿠키를 저장한다.

실제 로그인 기능 코드를 구현한 후 실행시켜 브라우저에서 쿠키를 확인해보면

위 그림과 같이 Application 탭에서 Name/Value에 쿠키를 확인할 수 있다.

하지만 이 방법은 Application 탭에 쿠키가 노출되어 있으므로 위험하다.

그래서 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통하는 세션 방식을 사용한다.

위 그림은 세션 방식을 사용하였을때의 결과이며, 쿠키의 값이 직접적으로 노출되지 않고 다른 수가 보여지고있다.

https와 http2

https 모듈은 웹 서버에 SSL 암호화를 추가한다. GET이나 POST 요청을 할 때 오가는 데이터를 암오화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없게 하며, 로그인이나 결제가 필요한 창에서 https 적용이 필수가 되는 추세이다.

https는 인증서를 인증 기관에서 구입해야 한다. Let's Encrypt 같은 기관에서 무료로 발급해주기도 하지만 인증서 발급 과정은 복잡하고 도메인도 필요로한다.

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

위 코드는 요청과 응답 이해하기에서 처음 나온 서버 코드를 https로 변경한 것이다.

createServer 메서드가 인수를 두 개 받는데, 두 번째 인수는 http모듈과 같이 서버 로직이고, 첫 번째 인수는 인증서에 관련된 옵션 객체이다.

노드의 http2 모듈은 SSL 암호화와 더불어 최신 http 프로토콜인 http/2를 사용할 수 있게 한다. http/2는 요청 및 응답 방식이 기존 http/1.1보다 개선되어 훨씬 효율적으로 요청을 보낸다. 그래서 http/2를 사용하면 웹의 속도도 많이 개선된다.

위 그림은 http/1.1과 http/2를 비교한 그림이다.

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

위 코드는 http2를 적용한 코드이며, https 모듈과 거의 유사하다.
https 모듈을 http2로, createServer 메서드를 createSecureServer 메서드로 바꾸면 된다.

cluster

cluster 모듈은 기본적으로 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈이다.

예를 들어 코어가 여덟 개인 서버가 있을 때, 노드는 보통 코어를 하나만 활용하지만, cluster 모듈을 설정해 코어 하나당 노드 프로세스 하나가 돌아가게 할 수 있다.

코어가 하나만 사용할 때에 비해 성능이 개선되지만, 스레드가 아니라 프로세스이므로 메모리를 공유하지 못하는 등의 단점도 있다.

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if(cluster.isMaster){
	console.log(`마스터 프로세스 아이디: ${process.pid}`);
    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>');
    }).listen(8086);
    
    console.log(`${process.pid}번 워커 실행`);
}

위 코드는 요청과 응답 이해하기에서 처음 나온 서버 코드를 클러스터링한 코드이다.

worker_threads와 비슷하지만, 스레드가 아닌 프로세스이다.

클러스터링은 예기치 못한 에러로 인해 서버가 종료되는 현상을 방지할 수 있어 적용해두면 좋은 모듈이다.


그저 유튜브, 구글링을 통해 나오는 express 코드를 따라치기만 해서 서버 동작 원리 등 많은 것을 알아가는 장이였다.
특히 쿠키와 세션은 프론트 부분에서만 다룬다고 생각했던 나는 다시 공부를 빡세게 해야겠다고 느꼇다..

profile
기록으로 복습하자

0개의 댓글

관련 채용 정보