Node.js | 여러 페이지의 라우팅 및 폼 POST 전송

서버 프로그램이라는 것은 클라이언트에서 보낸 요청을 받아 처리하는 것이다. 기본적인 처리로 “여러 페이지의 라우팅(routing)“와 “폼이 POST 전송 처리"에 대해 설명한다.

여러 페이지의 라우팅 개념

Node.js으로 간단한 페이지를 표시할 수 있게 됐다. 그런데, 한 페이지만 표시가 된다면, 실제로 사용할 수가 없다. 일반 Web에서는 더 다양한 요청에 처리를 해야 한다.

여러 페이지 작성

우선은 “복수의 페이지"부터 생각해 보자. 보통 Web이라는 것은 여러 페이지가 있다. Node.js에서 여러 페이지를 표시하려면 어떻게해야 하나?

여기에서는 전회에 사용한 EJS라는 템플릿 엔진을 사용하여 생각을 해보록 한다(사용하지 않아도 개념은 같지만 …). 기본적인 개념은 매우 간단하다. 여러 페이지를 이용하려면, 먼저 그 페이지를 미리 로드해 두고, 요청에 따라 어떤 페이지를 렌더링하여 표시할지 여부를 결정하면 된다.

그럼 “어느 페이지를 표시할지"를 클라이언트에서 어떻게 전달할 수 있는가? 보통의 Web 사이트에서는 URL에 의해 그것은 전달할 수 있다. http://xxx/index라면 index 페이지를 http://xxx/helo라면 helo 페이지를…… 이와 같은 식이다.

보통의 Web 서버에서는 자동으로 해당 HTML 페이지를 로드하여 반환하지만, Node.js의 경우 프로그램 중에 이러한 처리를 해야 한다. 보내 온 URL에서 도메인 이후의 부분을 얻어서 그 값에 의해 표시하는 페이지를 바꾸는 등의 작업을 생각할 수 있다.

그럼 실제로 해보자. 먼저, 아래 준비로 표시에 사용하는 EJS 템플릿 파일을 준비해 둔다. 여기에는 페이지 전체의 레이아웃이 된다. “template.ejs”(이전까지 hello.ejs라는 파일명으로 사용하고 있었던 파일이다)과 실제 표시되는 컨텐츠가 되는 ‘content1.ejs”, “content2.ejs"까지 총 3개의 템플릿 파일을 준비하록 한다.

template.ejs

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta http-equiv="content-type"
        content="text/html; charset=UTF-8">
    <title><%=title %></title>
    <style>
    body { font-size:12pt; color:#006666; }
    h1 { font-size:18pt; background-color:#AAFFFF; }
    pre { background-color:#EEEEEE; }
    </style>
</head>
 
<body>
    <header>
        <h1 id="h1"><%=title %></h1>
    </header>
    <div role="main">
        <p><%-content %></p>
    </div>
</body>
 
</html>

content1.ejs

<h2>예제로 작성한 컨텐츠입니다.</h2>
<p><%= message %></p>
<hr>
<p><a href="/other">other pageへ</a></p>

content2.ejs

<p>다른 페이지의 컨텐츠이다.</p>
<p><%= message %></p>
<p> </p>
<p><a href="/">돌아가기</a></p>

이 파일들을 사용하는 두 페이지를 표시하는 스크립트를 작성한다.

url개체를 사용하여 URL 처리

그럼, 준비한 템플릿을 사용하는 스크립트를 작성한다. 이것은 앞에 스크립트를 보고나서 설명을 하는 편이 빠를 것이다.

아래에 Node.js 스크립트 예제는 아래와 같다.

var http = require('http');
var fs = require('fs');
var ejs = require('ejs');
var url = require('url');
 
var template = fs.readFileSync('./template.ejs', 'utf8');
var content1 = fs.readFileSync('./content1.ejs', 'utf8');
var content2 = fs.readFileSync('./content2.ejs', 'utf8');
 
var routes = {
    "/":{
        "title":"Main Page",
        "message":"이것을 예제 페이지입니다.",
        "content":content1},
    "/index":{
        "title":"Main Page",
        "message":"이것을 예제 페이지입니다.",
        "content":content1},
    "/other":{
        "title":"Other Page",
        "message":"다른 페이지 표시하고 있다.",
        "content":content2}
};
 
var server = http.createServer();
server.on('request', doRequest);
server.listen(1234);
console.log('Server running!');
 
// 요청 처리
function doRequest(request, response) {
    var url_parts = url.parse(request.url);
    // route check
    if (routes[url_parts.pathname] == null){
        console.log("NOT FOUND PAGE:" + request.url);
        response.writeHead(200, { 'Content-Type': 'text/html' });
        response.end("<html><body><h1>NOT FOUND PAGE:" + 
            request.url + "</h1></body></html>");
        return;
    }
    // page render 
    var content = ejs.render( template,
        {
            title: routes[url_parts.pathname].title,
            content: ejs.render(
                routes[url_parts.pathname].content,
                {
                    message: routes[url_parts.pathname].message
                }
            )
        }
    );
    response.writeHead(200, {'Content-Type': 'text/html'});
    response.write(content);
    response.end();
}

이를 실행하여 http://127.0.0.1:1234/로 접근해 본다. content1.ejs의 내용이 표시된다. 이 페이지에있는 링크를 클릭하면, http://127.0.0.1:1234/other로 이동하고 content2.ejs의 내용을 표시한다.

그럼 동작이 확인되었다면, 스크립트의 내용을 보면서 처리 방법을 설명하겠다.

url의 로드

var url = require('url');

URL을 처리하기 위해서는 “url"라는 개체를 로드한다. 이 url 객체는 URL 문자열을 파싱하고, 거기에서 필요한 것을 얻어오는 기능을 제공한다.

URL의 파싱

var url_parts = url.parse (request.url);

요청이 액세스해 온 URL을 파싱 처리한다. 요청된 URL은 request 이벤트 핸들러의 인자로 전달된 request 객체의 ‘url’이라는 속성(property)에서 얻을 수 있다.

url 객체의 “parse"는 URL의 문자열을 요소마다 분할하여 오브젝트화 하여 반환한다. 이것으로 변수 url_parts에 URL 요소가 저장된다. 각각의 요소는 생성된 객체의 속성으로 저장되어 언제든지 사용할 수 있다.

각 페이지의 데이터를 준비한다

이번 스크립트에서는 각 페이지의 데이터를 routes라는 변수에 정리하고 있다. 이것은 액세스하는 곳의 경로를 키로 준비하고, 그 경로에 표시되는 페이지의 정보를 연관 배열로 정리한 것을 값으로 설정되어 있다. 예를 들어, 루트인 “/“의 값을 보면,

"/":{
    "title":"Main Page",
    "message":"이것을 예제 페이지입니다.",
    "content":content1 }

이런 식으로 되어있는 것을 알 수 있을 것이다. 연관 배열에는 title, message, content라는 키가 준비되어 있으며, 각각 “제목 텍스트”, “페이지에 표시하는 메시지 텍스트”, “표시하는 페이지의 내용(템플릿 데이터)“를 값으로 저장되어 있다. 이 변수 routes에서 액세스하는 주소의 경로마다 필요한 정보를 얻는 처리 하는 것이다.

경로를 얻을 수 없는 경우의 처리

if (routes[url_parts.pathname] == null){...}

변수 url_parts에 URL의 각 요소가 오브젝트로 저장되어 있는데, 이 중에 “경로"값은 “pathname"라는 속성으로 저장되어 있다.

변수 routes에는 기존에 언급한 바와 같이 각 경로마다 필요한 정보가, 경로를 키로 한 연관 배열에 정리되어 있다. 이는 routes[url_parts.pathname] 값을 꺼내면, 현재 요청이 액세스하고 있는 경로의 정보를 얻을 수 있게 된다. 만약이 값이 null이라면, 변수 routes는 정보가 없다는 것은 “그 경로에 액세스할 수 없다’라는 것을 의미한다.

그래서 null의 경우에는 에러 메시지 등을 표시해야 한다. 이것으로 준비되지 않은 주소로 대응을 할 수 있다.

액세스한 경로의 페이지를 렌더링하기

var content = ejs.render( template,
    {
        title: routes[url_parts.pathname].title,
        content: ejs.render(
            routes[url_parts.pathname].content,
            {
                message: routes[url_parts.pathname].message
            }
        )
    }
);

이후에는 변수 routes에서 필요한 값을 얻어 렌더링을 하면 된다. 예를 들어, title에 설정하는 값은

title: routes[url_parts.pathname].title

이렇게 준비하면 되고, 컨텐츠의 렌더링을 content 준비하려면

content: ejs.render( 
    routes[url_parts.pathname].content, ...)

이와 같이하면 된다. 여기에 표시할 내용의 템플릿은 routes[url_parts.pathname].content에서 얻을 수 있기에, 이를 render 할 뿐이다.

그 후로는 writeHead, write, end 세트로 실행하여 페이지 출력이 완료된다. 템플릿 렌더링 처리가 있기 에 조금은 귀찮게 보이지만, 기본적으로 “페이지에서 사용하는 값은 routes[url_parts.pathname]에 정리되고 있다"라는 것을 알고 있으면 어렵지 않다.

입력폼 POST 전송

이어서 폼을 POST 전송했을 때의 처리에 대해 생각해 본다. 이미 주소로 페이지 처리하는 방법은 알았기에 입력폼을 가진 페이지를 준비하고, 그 전달 대상의 주소 처리하는 작업은 알고 있다.

GET과 POST

가장 큰 문제는 “GET인지, POST인지"를 어떻게 알 것인가하는 점이다. 그리고 또 하나는 “전송된 입력폼의 정보를 어떻게 얻을 것인가"라는 점이다.

우선, GET과 POST 분리하는 방법이다. 이는 사실 간단하다. request 이벤트 핸들러에 인수로 전달된 request 객체의 ‘method’를 조사하는 것만으로 끝이다. 핸들러 안에 이런 식으로 처리를 준비하면 된다.

if (request.method == "GET"){
    ...... GET 처리 ......
}
if (request.method == "POST"){
    ...... POST 처리 ......
}

다음으로 “POST 전송된 입력폼 데이터를 가져오기"는 실제 코드를 보는 편이 알기 쉬울 것이다. 그럼, 이에 대해서도 샘플을 만들어 설명한다. 우선은 표시하는 내용의 템플릿이다. 이번에는 content1.ejs에 입력폼을 넣고, 새롭게 준비하는 content3.ejs에서 전송된 입력폼의 표시를 하도록 한다.

예제는 아래와 같다. 우선 이를 작성한다.

content1.ejs

<h2>예제로 작성한 내용입니다.</h2>
<p><%= message %></p>
<hr>
<form method="post" action="./post">
<table>
    <tr><td>ID:</td><td><input type="text" name="idname"></td></tr>
    <tr><td>PASS:</td><td><input type="password" name="pass"></td></tr>
    <tr><td></td><td><input type="submit"></td></tr>
</table>
</form>

content3.ejs

<p>POST으로 액세스된 내용입니다.</p>
<p>ID: <%= idname %></p>
<p>PASS: <%= pass %></p>
<p><a href="/">돌아가기</a></p>

POST로 전송된 데이터 처리

이어서 Node.js 스크립트를 작성한다. 이번에는 입력폼의 대상으로 content3.ejs을 표시하기 위해 “/ post"라는 경로로 정보를 추가하고 있다.

var http = require('http');
var fs = require('fs');
var ejs = require('ejs');
var url = require('url');
var qs = require('querystring');
 
var template = fs.readFileSync('./template.ejs', 'utf8');
var content1 = fs.readFileSync('./content1.ejs', 'utf8');
var content2 = fs.readFileSync('./content2.ejs', 'utf8');
var content3 = fs.readFileSync('./content3.ejs', 'utf8');
 
var routes = {
    "/":{
        "title":"Main Page",
        "message":"이것은 예제 페이지입니다.",
        "content":content1},
    "/index":{
        "title":"Main Page",
        "message":"이것은 예제 페이지입니다.",
        "content":content1},
    "/other":{
        "title":"Other Page",
        "message":"다른 페이지를 표시하고 있습니다.",
        "content":content2},
    "/post":{
        "title":"Post Page",
        "content":content3}
};
 
var server = http.createServer();
server.on('request', doRequest);
server.listen(1234);
console.log('Server running!');
 
// 요청 처리
function doRequest(request, response) {
    var url_parts = url.parse(request.url);
    // route check
    if (routes[url_parts.pathname] == null){
        response.writeHead(200, { 'Content-Type': 'text/html' });
        response.end("<html><body><h1>NOT FOUND PAGE:" + 
            request.url + "</h1></body></html>");
        return;
    }
    // get
    if (request.method == "GET"){
        var content = ejs.render( template,
            {
                title: routes[url_parts.pathname].title,
                content: ejs.render(
                    routes[url_parts.pathname].content,
                    {
                        message: routes[url_parts.pathname].message
                    }
                )
            }
        );
        response.writeHead(200, {'Content-Type': 'text/html'});
        response.write(content);
        response.end();
        return;
    }
    // post
    if (request.method == "POST"){
        if (url_parts.pathname == "/post"){
            var body='';
            request.on('data', function (data) {
                body +=data;
            });
            request.on('end',function(){
                var post =  qs.parse(body);
                var content = ejs.render( template,
                    {
                        title: routes[url_parts.pathname].title,
                        content: ejs.render(
                            routes[url_parts.pathname].content,
                            {
                                idname: post.idname,
                                pass: post.pass
                            }
                        )
                    }
                );
                response.writeHead(200, {'Content-Type': 'text/html'});
                response.write(content);
                response.end();
            });
        } else {
            response.writeHead(200, {'Content-Type': 'text/plain'});
            response.write("NO-POST!!");
            response.end();
        }
    }
}

스크립트를 작성한 후에 Node.js를 기동시켜서 액세스해 본다. http://127.0.0.1:1234/ 에 액세스하면 준비된 양식이 표시된다. 여기에 ID와 PASS를 적당히 값을 기입하고 전송하면, 전송된 내용이 표시된다.

이번 스크립트에는 “querystring"라는 객체를 로드하여 사용하고 있다. 시작 부분에 있는 다음 문장 이다.

var qs = require('querystring');

이 querystring은 쿼리 문자열을 처리하는 기능을 제공한다. 이를 이용하여 쿼리 문자열에서 필요한 값을 지정하여 꺼낼 올 수 있게 되다. 그럼, 스크립트의 내용을 살펴 보자.

여기에서는 request.method으로 GET시와 POST에서 처리를 나눈다. GET인 경우에 처리는 앞전과 동일하다. 문제는 POST에서의 처리이다. 여기에서는 먼저 “data"라는 이벤트 핸드링을 하고 있다.

var body='';
request.on('data', function (data) {
    body +=data;
});

이 data 이벤트는 POST로 전송된 데이터를 수신했을 때 발생한다. 이벤트 핸들러에는 보내져 온 데이터가 인수로 전달된다. 이렇게 얻어진 데이터를 변수 body에 하나씩 추가하여 수신된 데이터가 완성되어 간다.

그리고 모든 수신 처리가 완료된 후에 POST 데이터의 처리와 페이지의 렌더링을 실시한다. 이것은 “end"라는 이벤트 핸들러를 준비하고 구현한다.

request.on('end',function(){
    var post =  qs.parse(body);
    ...... 중략 ......

end 이벤트 핸들러으로 최초에 수행하고 있는 것은 앞전에 data 이벤트에서 받은 데이터를 정리한 변수 body를 파싱하는 과정이다. 이는 querystring 객체의 “parse"라는 메소드로 실행하고 있다. 이 메소드는 인수로 전달된 쿼리 문자열을 파싱하여 객체에 정리한다. 예를 들면, 아래와 같은 식이다.

a=abc&x=xyz

{ a: "abc", x:"xyz" }

이 때, URL 인코딩된 값도 자동으로 원래의 문자열로 디코딩된다. 이렇게 얻어진 변수 post에서 필요한 값을 꺼내면 된다. 이번에는 content3를 렌더링할 때 다음과 같이하고 있다.

content: ejs.render(
    routes[url_parts.pathname].content,
    {
        idname: post.idname,
        pass: post.pass
    }
)

전송된 값은 post.idname, post.pass에서 꺼낼 수 있다. 이 후에는 이들을 정리해서 렌더링할 뿐이다.

POST 전송은 데이터의 수신이 조금 복잡하지만, 그래도 알게 되면 쉽다. 이것으로 대부분의 일반 Web 페이지는 만들 수 있을 것이다.




최종 수정 : 2018-07-16