본문 바로가기
Development/데브코스(TIL, 회고록 등등...)

[데브코스 웹 풀스택 과정 TIL] Day 6~9 - 웹 생태계 전반에 대한 이론 및 실습 (3) - Node.js로 만드는 웹서버

by Polaris_ 2023. 11. 30.

1. 백엔드

웹서버 만든다면서 뜬금없이 웬 백엔드 설명인가 싶겠지만 뒤에서 필요한 내용이니 한번 훑어보고 가자.

백엔드의 구조

클라이언트(프론트엔드) - 서버(백엔드) 도식

위 그림을 보면 알겠지만 백엔드는 크게 세가지,

웹 서버 / 웹 어플리케이션 서버(WAS: Web Application Server) / 데이터베이스로 구분할 수 있다. 

  • 웹 서버: 정적 페이지에 대응하는 서버. 동적 페이지 처리는 직접 하지 않고 WAS에 전달하여 처리함.
  • 웹 어플리케이션 서버: 동적 페이지에 대응하는 서버. 데이터 처리 / 연산을 하여 웹 서버에 전달해줌으로써 화면의 내용이나 데이터의 변화를 구현함.
  • 데이터베이스: 데이터통합하여 효율적인 관리를 하기 위한 데이터 집합체.

그런데 위의 그림에서 보다시피 웹 서버와 WAS가 분리되어 있는데, 여기서 하나 의문이 들 수 있는 것이 '서버를 하나로 통합해서 운영하면 되는데 굳이 왜 두개를 분리해서 운영하지?' 일 것이다.

사실 가능은 하다. 한 서버에 두개를 통합해서 돌리면 그만이다.

그런데 그렇게 되면 서버 사용량(=트래픽)이 적을 때에는 문제가 없겠지만 트래픽이 늘고 처리해야되는 데이터 양이 늘어나다 보면 서버에 부하가 걸리게 되고, 결국에는 서버가 뻗는 상황이 올 수도 있다.

이때 두 서버를 분리해서 운영하면 서버의 부하도 줄임과 동시에 하나의 웹 서버에서 여러 웹 애플리케이션을 사용할 수 있게 된다.


2. Node.js

자바스크립트는 알다시피 웹 브라우저 상에서 돌아가는 스크립트 언어이다.

그리고 Node.js는 그 자체로는 자바스크립트를 웹 브라우저 외의 네이티브 환경에서 실행하기 위해 만들어진 프로그램(플랫폼)이다. (크롬 브라우저에 들어가있는 V8 자바스크립트 엔진을 바탕으로 만들어졌다고.)

이 둘이 만나니 자바스크립트는 네이티브 환경에서 실행할 수 있게 되면서 본격적인 프로그래밍이 가능해졌다. 또한 Node.js에는 HTTP 서버 라이브러리가 내장되어 있어서 별도 소프트웨어 없이도 서버를 구축 할 수 있다.

이게 무슨 소리냐 하면,

Node.js를 통해 프론트엔드에 쓰이던 자바스크립트로 백엔드까지 구현할 수 있다는 것이다!

그렇다면 이제 Node.js를 가지고 웹 서버를 만들어를 만들어 볼 차례다.


웹 서버 만들기

/* server.js */

let http = require('http');  // http 모듈 불러오기

function onRequest(request, response) {  // http 모듈에서 요청을 받았을 때 수행할 함수
    /* 응답의 header 설정(200: 정상 / 'Content-Type' : 'text/html' --> '이 페이지는 text/html 임') */
    response.writeHead(200, {'Content-Type': 'text/html'});
    response.write('Hello!');  // 응답 컨텐츠 작성
    response.end();  // 응답 종료
}

/* onRequest를 콜백 함수로 하는 서버 생성 (여기서는 8080포트를 사용함) */
http.createServer(onRequest).listen(8080);

이 코드를 터미널 상에서 node server.js로 실행하고 브라우저에서 localhost:8080을 치고 들어가면

정상적으로 응답이 출력되었음을 알 수 있다

그렇다면 서버를 모듈화 하는 방법을 보자.


서버 모듈화

/* index.js */

let server = require('./server');

server.js를 모듈로서 불러오는 구문이다. 이렇게만 해서 실행할 수 있으면 좋겠지만 아쉽게도(?) 그렇게는 안된다.

그렇다면 방법은 server.js에서 서버 실행 부분을 함수로 감싸고 그 함수를 export시키는 것이다.

/* index.js */

let server = require('./server');

server.start();
/* server.js */

let http = require('http');

function start() {
    function onRequest(req, resp) {
        response.writeHead(200, {'Content-Type': 'text/html'});
        response.write('Hello!');
        response.end();
    }
    http.createServer(onRequest).listen(8080);  //localhost:8080
}

exports.start = start;  // 외부에서 모듈 내부 함수를 쓸 수 있게 함

이렇게 하고 node index.js를 실행하면 브라우저로 가보면 방금과 같은 결과를 얻는다.

방금과의 차이점이라면 앞에서는 단일 파일이였다면 이번엔 실행 코드와 서버 코드를 모듈로써 분리했다는 것이다.


라우터 생성 및 라우팅

웹에서 라우팅이라 함은 클라이언트의 요청에 따라 요청에 맞게 정해진 페이지로 안내하는 것이다.

/* router.js */

function route(pathname) {
    console.log('[router] pathname : ' + pathname);
}

exports.route = route;

먼저 라우팅을 해줄 route() 함수를 router.js에 만들어 export 해준다. 아직은 콘솔에 경로명만 띄워주기로 한다.

/* index.js */

let server = require('./server');

server.start(router.route);

그리고 route.js에서 export 해준 route() 함수를 server.jsstart() 함수에 인수로 넘겨준다.

/* server.js */

let http = require('http');

function start(route) {
    function onRequest(req, resp) {
        let path = new URL(req.url, `http://${req.headers.host}/`).pathname;  // url.parse().pathname 대체
        route(path);
        
        response.writeHead(200, {'Content-Type': 'text/html'});
        response.write('Hello!');
        response.end();
    }

    http.createServer(onRequest).listen(8080);  //localhost:8080
}

마지막으로 URL에서 경로명만 떼내어 index.js에서 넘겨준 route() 함수에 인수로 넣어 실행해주면 된다.

여기까지 하면 라우팅은 성공이다.


그렇다면 요청에 따라 각기 다른 페이지로 라우팅을 해보자.

/* requestHandler.js */

function main(response) {
    console.log('main');

    response.writeHead(200, {'Content-Type' : 'text/html'});
    response.write('main')
    response.end();
}

function login(response) {
    console.log('login');

    response.writeHead(200, {'Content-Type' : 'text/html'});
    response.write('login')
    response.end();
}

function default_call(response, pathname) {
    response.writeHead(404, {'Content-type': 'text/html'});
    console.error('No hander of ' + pathname + '!')
    response.write('Not found');
    response.end();
}

let handler = {};  // key-value pair로 이루어진 함수
handler['/'] = main;
handler['/login'] = login;
handler['default'] = default_call;  // handler에 없는 함수 호출 시를 위한 함수

exports.handler = handler;

먼저 라우터에서 넘어온 요청을 처리하기 위한 핸들러를 만들어준다.

/* router.js */

function route(pathname, handler, response, prod_id) {
    console.log('[router] pathname : ' + pathname);
    (typeof handler[pathname] === 'function') ? handler[pathname](response, prod_id) : handler['default'](response, pathname);
}

exports.route = route;

다음 라우터에 핸들러로 경로 별 정해진 함수를 실행하도록 수정해준다.

/* server.js */

let http = require('http');

function start(route, handler) {
    function onRequest(req, resp) {
        let path = new URL(req.url, `http://${req.headers.host}/`).pathname;  // url.parse().pathname 대체
        route(path, handler, resp);
        
        response.writeHead(200, {'Content-Type': 'text/html'});
        response.write('Hello!');
        response.end();
    }

    http.createServer(onRequest).listen(8080);  //localhost:8080
}

server.js에도 라우터가 경로 별로 처리할 수 있도록 인수를 넘겨주고,

/* index.js */

let server = require('./server');

server.start(router.route, requestHandler.handler);

index.js에서 server.start() 함수에서 사용할 핸들러 함수를 넘겨주면 끝이다.

 

 

 

[다음글에 이어서]