프로그래밍/Node.js

Node.js 4장 http모듈로 웹 서버만들기

p-a-r-k 2019. 1. 8. 16:13
반응형

* 해당 글은 (주)길벗 `Node.js교과서` 내용을 바탕으로 복습 차 정리중입니다.



4.1 요청과 응답의 이해

서버는 클라이언트가 있기에 동작합니다.

클라이언트에서 서버로 요청(request)을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에게 응답(response)을 보냅니다.

따라서, 서버에는 요청을 받는부분과 응답을 보내는 부분이 있어야합니다.

server1.js
1
2
3
4
5
6
7
8
const http = require('http')
 
http.createServer((req, res) => {
    res.write('<h1>Hello Node..</h1>')
  res.end('<p>Hello Server..</p>')
}).listen(8080, () => {
    console.log('8080포트에서 서버 대기중...')
})

http모듈을 사용해야 웹브라우저의 요청을 처리할 수 있습니다.

http모듈에는 createServer 메서드가 있고, request/response 매개변수를 받을 수 있습니다.

위에서 req는 요청, res는 응답정보를 가지고 있습니다.

listen 메서드는 클라이언트에게 공개할 포트를 넘겨주면 연결을해주고 콜백함수에서 결과를 받을 수 있습니다.


res 객체에는 res.write와 res.end 메서드가 있습니다. res.write의 첫 번째 인자는 클라이언트로 보낼 데이터이며,

위코드에서는 문자열을 보냈지만 버퍼를 보낼 수도 있습니다. 또한 여러 번 호출해서 여러개 보내도 됩니다.

res.end는 응답을 종료하는 메서드이고, 인자가 있다면 클라이언트로 보내면서 응답을 종료합니다.

위 코드를 실행시키고 localhost:8080에 접속하면 응답화면을 볼 수 있습니다.

간단한 웹서버가 생성되었습니다. 

html 파일을 읽어서 응답하는 방법도 있습니다.


server2.html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8"/>
      <title>Node.js 웹 서버</title>
   </head>
   <body>
      <h1>Node.js 웹 서버</h1>
      <p>만들즈ㅏㅏㅏㅏㅏ</p>
   </body>
</html>
server2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
const http = require('http')
const fs = require('fs')
 
http.createServer((req, res) => {
   fs.readFile('./server2.html', (e, d) => {
      if (e) {
         throw err
      }
      res.end(d)
   })
}).listen(8081, () => {
   console.log('8081번 포트에서 대기중..')
})

요청이 들어오면 fs모듈로 html 파일을 읽습니다. data변수로 받은 버퍼를 그대로 end메서드에 넘겨주면 됩니다.

위에서는 문자열을 보냈지만, 저렇게 버퍼를 보낼 수도 있습니다.

html파일을 읽어와서 응답하는 서버도 만들어보았습니다.

하지만 지금까지는 요청을 보내면 모두에게 같은 응답(파일)을 보내고 있습니다.

아래에서는 클라이언트를 기억하여 클라이언트별로 다르게 응답해보겠습니다.



4.2 쿠키와 세션 이해하기

서버는 클라이언트가 누구인지 기억하기위해 쿠키라는 것을 같이 보냅니다. 쿠키는 name=park 처럼 단순한 '키-값'의 쌍입니다.

서버에서 쿠키가 오면 브라우저는 쿠키를 저장해두었다가 요청 시 쿠키를 보내줍니다. 서버는 요청의 쿠키를 읽어서 클라이언트를 파악합니다.

브라우저는 쿠키가 있으면 자동으로 보내주므로 처리할 필요가 없고, 서버에서 브라우저로 보낼때만 코드를 작성하면 됩니다.

쿠키는 요청과 응답의 헤더에 저장됩니다. 

요청과 응답은 각각 헤더와 본문을 가집니다. 

server3.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const http = require('http')
 
const parseCookies = (cookie = '') => {
   return cookie.split(';')
         .map(v => v.split('='))
         .map(([k, ...vs]) => [k, vs.join('')])
         .reduce((acc, [k, v]) => {
            acc[k.trim()] = decodeURIComponent(v)
            return acc
         }, {})
}
 
const server = http.createServer((req, res) => {
   const cookies = parseCookies(req.headers.cookie)
   console.log(req.url, cookies)
 
   res.writeHead(200, {'Set-Cookie''mycookie=tests'})
   res.end('Hello Cookie')
}).listen(8082)
 
server.on("listening", () => {
  console.log("listen 8082");
});

parseCookies 함수는 name=park;age=26 처럼 문자열 형식으로 오는 쿠키를 객체로 바꾸어주는 함수입니다.

응답 시에는 헤더에 쿠키를 기록해야하므로 res.writeHead 메서드를 사용했네요.

첫번째 인자인 200..은 성공이라는 상태값의 의미입니다.  두번째 인자로는 헤더의 내용을 입력합니다.

'Set-Cookie'는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미입니다.

localhost 8082에 접속하면 콘솔에 아래와같은 로그가 남습니다.

두줄이 찍히는 이유는, 브라우저는 파비콘이 html에 정의되지 않았으면 서버로 파비콘 정보요청을 추가로 보냅니다.

요청 두개를 통해 서버가 두번째 응답에서 쿠키를 심어준것을 확인했습니다.


마지막으로 세션을 사용하는 방법입니다.

server4.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
 
<head>
   <meta charset="utf-8" />
   <title>쿠키세션 이해하기</title>
</head>
 
<body>
   <form action="/login">
      <input id="name" name="name" placeholder="이름"/>
      <button id="login" type="submit">로그인</button>
   </form>
</body>
</html>

session 객체에 사용자 정보를 저장하고 클라이언트와는 세선아이디로만 소통합니다.

server5.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const http = require('http')
const fs = require('fs')
const url = require('url')
const qs = require('querystring')
 
const parseCookies = (cookie = '') => {
   console.log(cookie)
   return cookie.split(';')
      .map(v => v.split('='))
      .map(([k, ...vs]) => [k, vs.join('')])
      .reduce((acc, [k, v]) => {
         acc[k.trim()] = decodeURIComponent(v)
         return acc
      }, {})
}
 
const session = {}
const server = http.createServer((req, res) => {
   const cookies = parseCookies(req.headers.cookie)
 
   if (req.url.startsWith('/login')) {
      const { query } = url.parse(req.url)
      const { name } = qs.parse(query)
      const expires = new Date()
      expires.setMinutes(expires.getMinutes() + 5)
 
      const randomInt = +new Date();
      session[randomInt] = {
         name,
         expires
      }
 
      res.writeHead(302, {
         Location: '/',
         'Set-Cookie': `session=${randomInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`
      })
      res.end()
   else if (cookies.session && session[cookies.session].expires > new Date()) {
      res.writeHead(200, { 'Content-Type''text/html; charset=utf-8' })
      res.end(`${session[cookies.session].name}님 안녕하세용`)
   else {
      fs.readFile('./server4.html', (err, data) => {
         if (err) {
            throw err;
         }
         res.end(data)
      })
   }
}).listen(8082)
 
server.on("listening", () => {
   console.log("listen 8082");
});

실제 배포용 서버에서는 세션을 이와 같이 변수에 저장하지 않습니다.

서버가 재시작되거나 멈추면 메모리에 저장 된 변수가 초기화되기 때문입니다. 또한, 메모리가 부족하면 세션을 저장하지 못합니다.

그래서 보통은 데이터베이스에 넣어둡니다.

뒤에서 세션을 처리하는 모듈을 사용하면 안정적이고 쉽게 구현할 수 있습니다.

4.3 REST API와 라우팅

서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현합니다.

주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이고, /about.html이면 about.html을 보내달라는 뜻입니다.

요청이 항상 html을 요구할 필요는 없고, 위에서 server5.js에서도 /login이라는 주소를 처리했듯이,

서버가 이해하기 쉬운 주소를 사용하는 것이 좋습니다. 여기서 REST API가 등장합니다.

REST API

REpresentational State Trasnfer의 약어.

네트워크 구조의 한형식으로, 서버의 자원을 정의하고, 자원에 대한 주소를 지정하는 방법.

주소는 의미를 명확히 전달하기위해 명사로 구성됩니다.

REST API는 주소 외에도 HTTP 요청 메서드라는것을 사용합니다.

폼데이터 전송시에는 GET, POST...를 일반적으로 사용하고, PUT, PATCH, DELETE등 주로 5개가 많이 사용됩니다.


GET: 서버자원을 가져오고자 할 때. 서버로 데이터를 보낼 시 쿼리스트링을 사용.

POST: 서버에 자원을 새로 등록하고자 할때. 요청 본문에 데이터를 넣어보냅니다.

PUT: 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할 때. 본문에 넣습니다.

PATCH: 서버 자원의 일부만 수정하고자 할 때. 본문에 넣습니다.

DELETE: 서버 자원을 삭제하고자 할 때


주소하나가 요청메서드를 여러개가질 수 있습니다.

GET메서드의 /user주소면 사용자정보를 가져오라는 요청이고,

POST메서드의 /user주소면 새로운 사용자를 등록하려한다는것을 알 수 있습니다.


HTTP프로토콜을 사용하면 클라이언트가 누구든 서버와 소통할 수 있습니다.

IOS, 안드로이드, 웹이 모두 같은 주소로 요청을 보낼수 있습니다. 즉, 클라이언트와 서버가 분리되어 있다는 뜻입니다.

REST API를 따르는 서버를 RESTful하다고 표현합니다.


REST API 주소 체계로 RESTful한 웹 서버를 만들어봅니다.

restFront.css
1
2
3
a {
   colorbluetext-decorationnone;
}
restFront.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8"/>
      <title>RESTful SERVER</title>
      <link rel="stylesheet" href="./restFront.css"/>
   </head>
   <body>
      <nav>
         <a href="/">Home</a>
         <a href="/about">About</a>
      </nav>
      <div>
         <form id="form">
            <input type="text" id="username"/>
            <button type="submit">등록</button>
         </form>
      </div>
      <div id="list">
 
      </div>
   </body>
   <script src="./restFront.js"></script>
</html>
restFront.js 원본 펼치기

스크립트에서 페이지가 로딩괴면 GET /users로 사용자 목록을 가져옵니다. (getUser함수)

수정, 삭제 버튼에 각각 PUT /user/사용자id와 DELETE /users/사용자id 로 요청을 보냅니다.

form제출 시 에는 POST /users로 데이터와 함께 요청을 보내고 있습니다.

about.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8"/>
      <title>RESTful SERVER</title>
      <link rel="stylesheet" href="./restFront.css"/>
   </head>
   <body>
      <nav>
         <a href="/">Home</a>
         <a href="/about">About</a>
      </nav>
      <div>
         <h2>소개 페이지 입니다.</h2>
         <p>사용자 이름을 등록하세용</p>
      </div>
   </body>
</html>

about.html은 단순히 노드에서 html페이지 제공하는것을 보여주기위해 추가한 간단 html파일 입니다.

restServer.js 원본 펼치기

요청이 어떤 메서드를 사용했는지 req.method로 알 수 있습니다. 


크롬 콘솔 network 탭에서 요청 내용을 보면 REST API방식으로 주소를 만들었으므로 주소로만으로도 내용을 유추할 수 있습니다.

데이터는 메모리상 변수에 저장되어있으므로 서버종료 전까지 유지됩니다. 계속 유지하려면 데이터베이스와의 연동이 필요합니다.



4.5 cluster

cluster모듈은 싱글스레드인 노드가 CPU코어를 모두 사용할 수 있게해주는 모듈입니다.

포트를 공유하는 노드 프로세스를 여러 개 둘 수도 있어서,

요청이 많이 들어왔을 때 병렬로 실행 된 서버의 개수만큼 요청이 분산되게 할 수 있습니다.

예를들어 코어가 8개인 서버가 있을 때, 노드는 코어를 하나만 활용합니다. 

하지만 cluster 모듈을 설정하여 코어하나당 노드 프로세스 하나가 돌아가게 할 수 있습니다.

코어 하나만 사용할때보다는 성능이 개선되지만 세션을 공유하지 못하는 등 단점도 있습니다. (Redis 등의 서버도입으로 해결은 가능)

server1.js의 클러스터링 :: cluster.js
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++) {
      cluster.fork()
   }
   // 워커가 종료되었을 때
   cluster.on('exit', (worker, code, signal) => {
      console.log(`${worker.process.pid}번 워커가 종료되었습니다.`)
      cluster.fork()
   })
else {
   // 워커들이 포트에서 대기
   http.createServer((req, res) => {
      res.write('<h1>Hello Node!</h1>')
      res.end('<p>Hello Cluster..</p>')
 
      setTimeout(() => {
         process.exit(1);
      }, 1000)
   }).listen(8085)
 
 
   console.log(`${process.pid}번 워커 실행`)
}

클러스터에는 마스터 프로세스와 워커 프로세스가 있습니다.

마스터 프로세스는 CPU개수만큼 워커 프로세스를 만들고 8085번 포트에서 대기합니다. 

요청이 들어오면 만들어진 워커 프로세스들에게 요청을 분배합니다.


process.exit(1); 부분은 요청이 들어올 때마다 1초후에 서버가 종료되는 처리입니다.

cluster.fork(); 부분은 워커가 종료되면 다시 생성되도록 처리합니다.

실무에서는 pm2등의 모듈로 cluster기능을 사용합니다.

코드를 실행하면 4개의 워커가 실행됩니다. (본인PC 4코어)




그 후 localhost:8085에 접속하면 한번 접속시마다 종료되고 fork로인해 다시 실행됩니다.

fork부분이 없다면 4번 접속요청시 4개의 워커가 모두 종료됩니다.

하지만 이런식으로 오류로인한 재시작 처리는 좋지않은 생각입니다. 오류자체의 원인을 찾아야하도록 해야합니다.

그래도 예기치 못한 에러로인한 종료현상은 방지할수있으니 클러스터링은 적용하는게 좋습니다.


아직까지 알아본 라우팅과 api처리 등 코드가 상당히 길어보이기도하고 어렵고 관리도 힘듭니다.

주소의 수가 많아질수록 코드는 계속 길어질것입니다. 쿠키와 세션을 추가하면 더 늘어납니다.

이를 편리하게 만들어주는 모듈이 express모듈입니다.

expres모듈은 다른사람이 만들어둔 모듈이므로 npm을 이용하여 설치해야합니다.


반응형