kakasoo

Express 프레임워크 만들기 (3), Route 만들기 본문

프로그래밍/JavaScript

Express 프레임워크 만들기 (3), Route 만들기

카카수(kakasoo) 2021. 5. 20. 10:36
반응형

Router의 필요성

application은 어쩌면 Router라고 봐도 될지 모르겠습니다. 갑자기 이런 말을 하는 게 의아할 수 있겠습니다만, 사실 앞서 구현한 코드에는 Application 하나만이 존재할 뿐, 다른 Router는 존재하지 않습니다.

 

"그래도 사용하는 데에는 아무런 문제가 없지 않았나요?"

 

그렇습니다. 아직 path 기능이 완벽하진 않고 ( 지금은 완벽하게 매칭되어야만 하니깐요. ), 그 외에도 추가해야 할 게 산더미처럼 많지만, 어쨌든 원하는대로 동작하는 것을 볼 수 있긴 했습니다. 그렇지만 이렇게 되면 결국 한 덩어리로 만드는 것입니다. 당연히 코드가 길어질수록 힘들어질 것입니다.

 

앞서 app이라는 함수에 if문으로 분기 처리하는 게 지저분했기 때문에 분리한 거잖아요? 결국 같은 이유입니다. 지저분하다는 걸 알면 깨끗하게 해야겠죠. 이는 결과적으로 코드, 파일을 분리하는 데에 도움을 줍니다. 지금과 같은 상태로는 express의 메서드인 use를 쓸 수 없을 테니깐요.

 

아래는 express.js를 사용한 예시입니다.

 

// /dog.js
const express = require('express');

const router = express.router();
router.get('/', (req,res,next) => res.send('dogs'));

module.export = router;
const express = require('express');
const dogRouter = require('/dog.js');

const app = express();
app.use(dogRouter); // 이런 기능을 위해서라도 Router는 분리될 필요성이 있습니다.

module.export = app;

 

위 코드에 주석처리한 것과 같이, 나중을 위해서라도 Router는 분리될 필요성이 있습니다. 저 기능은 사실 상 각각 완성된 Router들을 하나로 묶어서 application의 구성품을 채우는 거니깐요.

 

그럼 다시 TExpress로 가봅시다.

 

const { METHODS } = require("http");

const TExpress = () => {
    const app = (req, res) => {
        const url = req.url;
        const method = req.method.toLowerCase();
        app[url][method](req, res);
    };

    METHODS.forEach((METHOD) => {
        const method = METHOD.toLowerCase();
        app[method] = (path, callback) => {
            if (!app[path]) {
                app[path] = {}; // 사실 여기가 Router였습니다.
            }

            app[path][method] = callback;
        };
    });
    return app;
};

 

우리는 if 문을 통해 app[path]가 없는 경우 빈 객체를 만들어주도록 하였습니다. 이 빈 객체에 대한 언급은 없었습니다만, 사실 저 부분이 Router에 해당합니다. 우리는 Router에 method를 등록해준 것이죠. 더 자세하게 하면, Router 안에 Route가 있다느니, 이런 얘기가 오갑니다만, 일단은 순서대로 진행을 해봅시다.

 

자, 진행에 앞서 질문을 해야 할 거 같습니다.

 

"application은 Router일까요?"

"Router도 application이라고 할 수 있을까요?"

 

제가 정의할 건 아니지만, 저는 둘이 겹치는 범위가 있다고 생각합니다. application은 그 자체로 Router라고도 생각하고요. 비유하자면, application은 나무의 줄기입니다. 이 나무의 줄기에 여러 개의 가지들이 뻗어 나갑니다. 이 가지들이 Router 입니다. 나무 줄기도 나무의 가지냐고 묻는다면, 글쎄요, 저는 그렇게 볼 수도 있지 않나 생각합니다. 사실 이 편이 더 이해하기 쉬울 거 같기도 하고요.

 

납득이 안될 수도 있을 거 같습니다. 그렇지만 납득을 했다고 가정하고 진행할 수 밖에 없습니다. 왜냐하면 express.js는 그런 생각을 기초로 하여 만든 것 같기 때문입니다.

( 같다고는 확언할 수 없겠습니다. )

 

제 생각엔, application은 일종의 router이고, router는 일종의 mini app이라고 보는 게 맞는 거 같습니다.

 

METHODS.forEach((METHOD) => {
    const method = METHOD.toLowerCase();
    app[method] = (path, callback) => {
        if (!this.router) {
            this.router = new Router();
        }

        this.router[method] = callback; // 아직 수정할 부분이 남아 있습니다!
    };
});

 

따라서 this.router를 만들어주었습니다. app[path]에 해당하는 부분이 this.router 였던 것이죠. 사실 상, app이 첫번째 router이기 때문에 생성해준 것입니다. 그렇지만 이렇게 분리됨에 따라 이전에 되던 동작도 이제 먹히질 않습니다. 따라서 router에는 여러 개의 path를 담아둘 필요성이 생겼습니다.

 

METHODS.forEach((METHOD) => {
    const method = METHOD.toLowerCase();
    app[method] = (path, callback) => {
        if (!this.router) {
            this.router = new Router();
        }

                const route = this.router.route(path); // 이 함수가 추가되었습니다.
                route[method] = callback;
    };
});

 

아직 Router의 내부를 다루지는 않았지만, 이전에는 Application에 모든 path가 등록되어 있던 것을, path 단위로 나눌 수 있게 되었습니다.

( 아직 동작하지는 않습니다. )

 

this.router의 route method가 path에 따라 새로운 Route를 만들고, 그것을 return 해주고 있습니다.

따라서 return 받은 route의 method에 callback 등록해주게끔 수정된 것입니다! 그러면 이제 route 라는 method만 구현해두면 될 거 같습니다.

 

// Router.js
// 나중에는 이 파일을 클래스 별로 분리할 예정입니다.

class Layer {
        constructor(path, option, handler) {
        this.path = path;
        this.option = option;
        this.handler = handler;
    }
}

class Route {
    constructor(path) {
                this.path = path;
                this.stack = [];
        }

    dispatch(req, res, done) {
        const method = req.method.toLowerCase();

        req.route = this;

        for (const curLayer of this.stack) {
            if (curLayer.method && curLayer.method !== method) {
                continue;
            }
            curLayer.handleRequest(req, res);
        }

        throw new Error("No Layer on this route.");
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(path) {
        const route = new Route(path);
          const layer = new Layer(path, {}, route.dispatch.bind(route));
        layer.route = route; // layer 안에 자신의 route가 무엇인지 등록해둡니다.
        this.stack.push(layer);

        return route;
    }
}

module.exports = Router;

 

갑작스럽게 너무 많은 것이 등장했지만, 일단 이것들에 대한 구분은 바로 다음 번에 말하기로 하고, 일단 동작하게끔 해봅시다. 어쨌거나, 이 코드에서 알 수 있는 것은 Router의 route method를 보면 분명해집니다. layer가 있고, layer의 내부에 route가 있습니다. Router.route method는 이런 구조를 만든 다음에, 최종적으로 생성된 layer를 router의 stack에 저장하고, route는 반환해주는 것입니다.

 

( 미리 말하자면, Router (app.router) 안에 여러 개의 Layer가 있고, 그 Layer는 기능이 되는 함수나 path를 저장하는 쌍입니다. Layer는 저장하는 대상이 달라질 수 있기 때문에 나중에 설명드리고자 하는 것입니다! )

 

METHODS.forEach((METHOD) => {
    const method = METHOD.toLowerCase();
    app[method] = (path, callback) => {
        if (!this.router) {
            this.router = new Router();
        }

                const route = this.router.route(path); // 이 함수가 추가되었습니다.
                route[method] = callback;
    };
});

 

다시 이 코드를 볼 때, 생성된 route가 반환될 것이고, 그 route의 method에 callback 함수가 등록된 것까지, 이제 이해가 될 것입니다.

하지만 이 상태로는 이벤트를 처리해주지 못합니다. app 함수도 수정을 해줘야 하니깐요.

 

const { METHODS } = require("http");

const TExpress = () => {
    const app = (req, res) => {
        // const url = req.url;
        // const method = req.method.toLowerCase();
        // app[url][method](req, res);

        if (!this.router) {
            throw new Error("No router defined on this app.");
        }
        this.router.handle(req, res); // 이제 this.router를 통해서 처리하게 합니다.
    };

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLowerCase();
                app[method] = (path, ...callback) => { // callback이 배열 형태라고 가정합니다.
             if (!this.router) {
                    this.router = new Router();
                }

                        const route = this.router.route(path); // 이 함수가 추가되었습니다.
                        // route[method] = callback;
            route[method].apply(route, callback); // 등록해주는 함수를 대입이 아니라 apply를 사용합니다.
            };
        });
    return app;
};

 

일단 위 코드처럼 this.router에 handle method를 통해 처리한다고 칩시다. 그러면 다시 Router 코드를 보러 가야겠군요. 거기서 handle 함수를 아래처럼 고쳐줍니다.

 

class Layer {
        constructor(path, option, handler) {
        this.path = path;
        this.option = option;
        this.handler = handler;
    }
}

class Route {
    constructor(path) {
        this.path = path;
                this.stack = [];
    }

    dispatch(req, res, done) {
        const method = req.method.toLowerCase();

        req.route = this;

        for (const curLayer of this.stack) {
            if (curLayer.method && curLayer.method !== method) {
                continue;
            }
            curLayer.handleRequest(req, res);
        }

        throw new Error("No Layer on this route.");
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(path) {
        const route = new Route(path);
          const layer = new Layer(path, {}, route.dispatch.bind(route));
        layer.route = route; // layer 안에 자신의 route가 무엇인지 등록해둡니다.
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const url = req.url; // 1) url을 알아낸 다음에,
        const method = req.method.toLowerCase();

        for (let i = 0; i < this.stack.length; i++) {
            const curLayer = this.stack[i];

            if (curLayer.path !== url) {
                // 2) 각각의 Layer에서 path가 일치하는 걸 찾습니다.
                continue;
            }

            if (!curLayer.route.hasMethod(method)) {
                // 3) 일치한다면 method에 해당하는 함수가 있는지 체크합니다. 없으면 continue.
                continue;
            }

            curLayer.handleRequest(req, res); // 3) 일치하는 Layer에게 요청된 method를 실행합니다.
        }
    }
}

module.exports = Router;

 

path가 같은 Layer를 찾았다면, 그 Layer에게 해당 method에 대응할 함수가 있는지를 찾아야 합니다. 없다면 탐색을 이어나가면 되고 있다면 아마도 찾고자 하는 Layer가 맞을 것입니다. 따라서 이 Layer에게 handle() 함수가 필요합니다. 이 함수들을 차례대로 만들어보겠습니다. 먼저, Route의 hasMethod입니다.

 

class Route {
    constructor(path) {
        this.path = path;
                this.stack = [];
        this.methods = {};
    }

    hasMethod(method) {
//        if (this.methods.all) {
//            return true;
//        }

        return Boolean(this.methods[method]);
    }

    dispatch(req, res, done) {
        const method = req.method.toLowerCase();

        req.route = this;

        for (const curLayer of this.stack) {
            if (curLayer.method && curLayer.method !== method) {
                continue;
            }
            curLayer.handleRequest(req, res);
        }

        throw new Error("No Layer on this route.");
    }
}

 

그냥 Boolean 형태로, 있냐 없냐만 체크해서 돌려주면 되기 때문에 간단하게 작성되었습니다. ( methods를 저장하기 위해서 this.methods를 만들었습니다. ) 당장은 all에 대한 부분은 다루지 않겠습니다.

 

작성하고 보니 이상한 점을 볼 수 있습니다. this.methods에서 탐색을 하는 것은 좋은데, 애초에 우리는 this.methods에 method 이름을 넣은 적이 없었습니다. 이 부분을 먼저 해결해줘야 할 거 같습니다. 방법은 이미 알고 있을 것입니다. 우리가 application에서 method를 등록하는 함수를 만들었으니깐요.

 

const { METHODS } = require("http");

class Layer {
    constructor(path, option, handler) {
        this.path = path;
        this.option = option;
        this.handler = handler;
    }

    handle() {}
}

class Route {
    constructor(path) {
        this.path = path;
        this.stack = [];
        this.methods = {};

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLowerCase();

            this[method] = (...handlers) => { // 1) 여러 개의 함수들을 받을 수도 있다.
                for (const handler of handlers) {
                    const layer = new Layer("/", {}, handler); // 2) 함수 하나 당 하나의 Layer를 만든다.
                    layer.method = method; // 3) layer에 method가 무엇인지 지정한다.

                    this.methods[method] = true; // 4) route에는 method가 있음을 나타낸다.
                    this.stack.push(layer); // 5) route의 stack에 layer를 넣는다.
                    return this;
                }
            };
        });
    }

    hasMethod(method) {
        if (this.methods.all) {
            return true;
        }

        return Boolean(this.methods[method]);
    }

    dispatch(req, res, done) {
        const method = req.method.toLowerCase();

        req.route = this;

        for (const curLayer of this.stack) {
            if (curLayer.method && curLayer.method !== method) {
                continue;
            }
            curLayer.handleRequest(req, res);
        }

        throw new Error("No Layer on this route.");
    }
}

 

또 코드가 복잡해진 것처럼 보이지만, 결국 등록해주는 함수를 만들었을 뿐입니다. 특이할 점을 1,2,3,4,5로 표시해서 주석을 달았는데 차례대로 설명해보겠습니다. 그 중에서 첫 번째인 파라미터 부분만 설명드리자면, 미들웨어처럼, 여러 개의 함수들을 받을 수 있습니다. 따라서 handlers라는 배열 형태로 받도록 한 것입니다.

( 다시 말씀드리지만, Router, Route, Layer의 관계는 추후 설명해드릴 것입니다! )

 

Route에도 등록하는 함수를 만들었으니, application에도 고칠 부분이 생겼습니다.

 

const TExpress = () => {
    const app = (req, res) => {
        if (!this.router) {
            throw new Error("No router defined on this app.");
        }
        this.router.handle(req, res);
    };

    METHODS.forEach((METHOD) => {
        const method = METHOD.toLowerCase();
        app[method] = (path, ...callback) => { // 여러 개의 함수를 받을 수 있다고 수정.
            if (!this.router) {
                this.router = new Router();
            }

            const route = this.router.route(path);
            // route[method] = callback;
            route[method].apply(route, callback); // 등록해주는 함수를 대입이 아니라 apply를 사용
        };
    });
    return app;
};

좋습니다. 이제 정말로, 정말로 Layer.handleRequest() 부분만 작성해주면 됩니다!

( method의 이름을 handleRequest 라고 한 이유는, express의 명칭을 사용하기 위해서입니다. )

class Layer {
    constructor(path, option, handler) {
        this.path = path;
        this.option = option;
        this.handler = handler;
    }

    handleRequest(req, res) {
                this.handler(req,res);
        }
}

 

여기까지 작성되었다면 코드는 정상적으로 동작합니다. 아래는 전체 코드입니다. 전체 코드를 다시 읽어보시고, 위 흐름을 이해해주셨으면 합니다. 제가 잘 설명했는지는 자신이 없습니다만, 코드는 거짓말을 안하니깐요.

( 문제가 있다면, 굳이 말씀드리지 않아도 모두 아시겠지만, express의 문제가 아니라 제가 작성한 코드의 문제입니다. )

 

const { METHODS } = require("http");

class Layer {
    constructor(path, option, handler) {
        this.path = path;
        this.option = option;
        this.handler = handler;
    }

    handleRequest(req, res) {
        this.handler(req, res);
    }
}

class Route {
    constructor(path) {
        this.path = path;
        this.stack = [];
        this.methods = {};

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLowerCase();

            this[method] = (...handlers) => {
                for (const handler of handlers) {
                    const layer = new Layer("/", {}, handler);
                    layer.method = method;

                    this.methods[method] = true;
                    this.stack.push(layer);
                    return this;
                }
            };
        });
    }

    hasMethod(method) {
        if (this.methods.all) {
            return true;
        }

        return Boolean(this.methods[method]);
    }

    dispatch(req, res, done) {
        const method = req.method.toLowerCase();

        req.route = this;

        for (const curLayer of this.stack) {
            if (curLayer.method && curLayer.method !== method) {
                continue;
            }
            curLayer.handleRequest(req, res);
        }

        throw new Error("No Layer on this route.");
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(path) {
        const route = new Route(path);
        const layer = new Layer(path, {}, route.dispatch.bind(route));
        layer.route = route;
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const url = req.url; // 1) url을 알아낸 다음에,
        const method = req.method.toLowerCase();

        for (let i = 0; i < this.stack.length; i++) {
            const curLayer = this.stack[i];

            if (curLayer.path !== url) {
                continue; // 각각의 Layer에서 path가 일치하는 걸 찾습니다.
            }

            if (!curLayer.route.hasMethod(method)) {
                continue; // 일치한다면 method에 해당하는 함수가 있는지 체크합니다.
            }

            curLayer.handleRequest(req, res); // 일치하는 Layer에게 요청된 method를 실행합니다.
        }
    }
}

module.exports = Router;

 

prototype으로 되어있던 것을, 제 나름대로 ES6로 고쳐가며 만들고 있습니다. 틀린 점이 있다면 언제든지 말씀해주세요. 저도 아직 모르는 게 많은 개발자기 때문에 ( 아직 학생입니다. ) 많은 분의 도움이 필요합니다.

반응형