노드로 만드는 단순한 웹 서버

노드로 만드는 단순한 웹 서버

지금 만드는 이 곧 서버이다.
노드는 웹 서버를 만드는 작업을 아주 단순하게, 코드 몇 줄로도 가능하게 만들었다.

Hello World

노드가 어떻게 동작하며, 프로그래머가 그 과정을 얼마나 컨트롤할 수 있는지 예제로 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// hello-world.js
const http = require("http");

// http.createServer 메서드는 함수를 매개변수로 받는다.
// 호출 시기: HTTP 요청이 있을 때마다 이 함수를 호출한다.

http
.createServer(function(req, res) {
// 단순히 콘텐츠 타입을 평범한 텍스트로 지정하고,
// 문자열 'Hello world!'를 보낸다.
// Content-Type: 응답 정보의 타입, 캐릭터셋(인코딩 정보) -> 해석 -> 표시
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello world!");
})
.listen(3000);

console.log("Server started on localhost:3000; press Ctrl_C to terminate....");
  • 이 서버는 HTML을 전송하지 않고 평범한 텍스트 메시지 ‘Hello World!’를 브라우저에 전송한다.

이벤트 기반 프로그래밍

이벤트 기반 프로그래밍에서는 프로그래머가 어떤 이벤트를 사용할 수 있는지,
거기에 어떻게 응답해야 하는지 이해해야 한다.
즉, 사용자가 뭔가를 클릭하면 프로그래머가 ‘클릭 이벤트’를 처리하는 식이다.

라우팅(Rounting)

라우팅이란 요청받은 콘텐츠를 클라이언트에 보내는 메커니즘이다.

  • 웹 기반 클라이언트/서버 애플리케이션에서는 클라이언트가 원하는 콘텐츠를 URL에 표시,
    -> 경로쿼리스트링에 표시한다.

  • Hello World! 예제를 좀 더 확장하여
    홈페이지, 어바웃 페이지, 404(찾을 수 없음) 페이지만 들어 있는 정말 기본적인 웹사이트를 만들어보자.

  • 아직은 이전 예제를 그대로 써서, 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
25
26
27
28
29
30
const http = require("http");

http
.createServer(function(req, res) {
// url에서 쿼리스트링과 옵션인 마지막 슬래시를 지우고 소문자로 바꿔서 정규화한다.
let path = req.url.replace(/\/?(?:\?.*)?$/, "").toLowerCase();
switch (path) {
// (http://localhost:3000)으로 이동
case "":
res.writeHead(200, { "Content-Type": "text/plain" });
// res.end()는 응답 프로세스를 종료한다.
res.end("Homepage");
// 쿼리스트링은 무시하므로 (http://localhost:3000/?foo=bar)를 치면,
// 홈페이지로 이동한다.
break;
// (http://localhost:3000/about)으로 이동
case "/about":
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("About");
break;
default:
// (http://localhost:3000/foo)같은 기타 URL은 404 페이지로 이동
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
break;
}
})
.listen(3000);

console.log("Server started on localhost:3000; press Ctrl_C to terminate....");

정적 자원 전송

이제 진짜 HTML과 로고 이미지를 전송해보자.

  • ex)주식 가격 표시기는 페이지를 새로 고칠 때마다 가격을 반영하여 바뀌지만,
    HTML이나 로고 이미지 같은 것들은 바뀌지 않는다. -> 이를 '정적 자원'이라고 부른다.

tip!
개발 도중 혹은 작은 프로젝트를 진행할 때는 노드에서 정적 자원을 전송해도 별 문제가 없다.
But, 큰 프로젝트를 진행할 때는 엔진X 같은 프록시 서버나 CDN을 사용한다.

  • 노드는 파일을 열고 내용을 읽어서 콘텐츠를 브라우저에 전송하는 작업을 직접해야 한다.
  • 프로젝트에 이 목적으로 사용할 public 디렉터리를 만들자.
    • 그 디렉터리에 homte.html, about.html, 404.html을 만든다.
    • 서브 디렉터리 img를 만든 다음 img/logo.jpg 이미지도 만든다.
1
2
<!-- home.html -->
<h1>home</h1>
1
2
<!-- about.html -->
<h1>about</h1>
1
<h1>404 Not Found!!!</h1>
  • hello-world.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
// hello-world.js
const http = require("http");
fs = require("fs");

// 보조함수인 serverStaticFile에서 필요한 작업을 대부분 수행한다.
function serverStaticFile(res, path, contentType, responseCode) {
if (!responseCode) responseCode = 200;
// fs.readFile: 파일을 읽는 비동기적 메서드
// 동기적 버전인 fs.readFileSync도 있지만, 생각을 비동기적으로 바꾸는 것을 추천

// fs.readFile을 호출해 파일 콘텐츠를 읽는다.
// fs.readFile은 파일을 읽은 뒤 콜백 함수를 실행한다.
// 파일이 존재하지 않거나 권한 문제 때문에 파일을 읽을 수 없다면, err 변수가 만들어지며,
// 함수는 서버 에러를 나타내는 HTTP 상태 코드 500을 반환한다.
fs.readFile(__dirname + path, function(err, data) {
if (err) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("500, - Internal Error");
// 파일을 성공적으로 읽으면,
// 지정한 응답 코드, 콘텐츠 타입, 파일을 클라이언트에 보낸다.
} else {
res.writeHead(responseCode, { "Content-Type": contentType });
res.end(data);
}
});
}

http
.createServer(function(req, res) {
// url에서 쿼리스트링과 옵션인 마지막 슬래시를 지욱 소문자로 바꿔서 정규화한다.
let path = req.url.replace(/\/?(?:\?.*)?$/, "").toLowerCase();
switch (path) {
case "":
serverStaticFile(res, "/public/home.html", "text/html");
// res.end()는 응답 프로세스를 종료한다.
break;
case "/about":
serverStaticFile(res, "/public/about.html", "text/html");
break;
case "/img/logo.jpg":
serverStaticFile(res, "/public/img/logo.jpg", "image/jpeg");
break;
default:
serverStaticFile(res, "/public/404.html", "text/html", 404);
break;
}
})
.listen(3000);

console.log("Server started on localhost:3000; press Ctrl_C to terminate....");

이 예제의 라우팅은 매우 단순하다.
http://localhost:3000/about으로 이동하면, public/about.html 파일이 전송된다.
경로나 파일을 원하는 대로 바꿀 수 있다.

  • ex) 요일마다 다른 About 페이지를 표시하고 싶다면,
  • public/about_mon.html, public/about_tue.html 등을 만들고,
  • 사용자가 localhost:3000/about으로 이동할 때 적절한 페이지를 전송하게끔 라우팅 프로그램을 만들 수 있다.

Tip!
__dirname은 실행 중인 스크립트가 들어있는 디렉터리로 해석된다.
-> 스크립트가 /home/sites/app.js에 들어 있다면,
__dirname/home/sites로 해석된다.
이 전역 변수를 사용하지 않으면, 앱을 다른 디렉터리에서 실행했을 때,
분석하기 어려운 에러가 일어나곤 한다.

익스프레스(Express)로 출발

  • Express는 이미 존재하고, 시간을 소비해서 구현해야 할 기반 구조는 이미 만들어져 있다.