[TIL][Node.js]간단한 API 만들어보기(with Simple MiddleWare and Express)

황연욱·2020년 9월 28일
2

Node.js

목록 보기
2/2
post-thumbnail

Node.js로 간단한 API 만들어보기

목차

Native Node.js로 만들어보기

  • Express등 다른 라이브러리 없이 기본적으로 http 요청을 받고 응답을 보내는 server를 만들어보겠습니다.

  • 먼저 실습용 디렉토리를 하나 만들어줍니다.
    mkdir nodeAPI / cd nodeAPI

  • 그 후에 package.json 파일을 만들어주기 위해서 yarn init 을 터미널에 입력해줍니다.

  • 그러면 nodeAPI란 디렉토리 하위에 package.json 파일이 생겼을 겁니다.

  • 이제 준비가 완료되었습니다. 간단한 node server를 만들어보겠습니다.

  • 먼저 server.native.js 파일을 하나 만들어줍니다. 파일명은 구분짓기 위함이니 편하신대로 지어주셔도 무방하지만 native node만을 통해서 만든 server라는 것을 위해서 server.natvie라는 이름에 js확장자를 붙여서 만들어주었습니다.

  • 그 다음에 실제로 요청을 받고 응답을 보내는 간단한 코드를 만들어보겠습니다.

    var http = require("http");
    
    http
      .createServer(function (req, res) {
        res.writeHead(200, { "Content-Type": "text/html" });
        res.end("<h1>Hello<h1>");
      })
      .listen(8000);
  1. 위 코드를 한번 해석해보자면 먼저, http통신을 통해서 요청을 받고 응답을 보내기에 http모듈을 import 해옵니다.(line1)

  2. http모듈의 내장 메소드인 createServer를 이용해서 서버를 생성해줍니다

  3. createServer라는 메소드는 인자로 requestLisnter함수를 받고 Server형태의 return을 하고, 이 requestLisnter는 req와 res라는 인자를 받아서 return값이 없는 함수입니다.

    function createServer(requestListener?: RequestListener): Server;
    ---
    type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;
  4. 우리는 requestListner함수를 익명함수로 정의하고, 함수바디에 res.writeHead로
    statusCode:200과, 응답의 Header부분에 들어갈 요소 중에 content-Type을 "text/html"로 정의해줍니다.

  5. 또한 res에 end 메소드를 통해서 <h1>Hello<H1> 형태의 respone을 보내줍니다.(write메소드를 통해서 사용해도 res 보낼 수도 있지만, end가 없을 경우 요청에 대한 응답을 끝내지 않고 계속해서 지속하는 중이기에 res.end를 통해서 respone이 끝났다는 것을 명확히 알려줘야 합니다.)

    res.write("<h1>Hello</h1>")
    res.end()

    위와 같이 표현해도 되지만, 하나만 보낼 경우 end로 처리하는게 좀 더 올바른 방향으로 생각됩니다.

  6. 그리고 마지막에 port번호를 명시해줍니다 이 port변호는 어떤 포트번호로 왔을때 이 요청을 보내줄지 정의하는 것입니다. listen(8000) 을 통해서 8000포트를 listen하도록 설정해줍니다.

  7. 그 다음에 terminal에 node server.native.js 를 입력해주면 서버가 구동됩니다.

  8. 그 후 브라우저를 켜서 http://localhost:8000/ 로 접속해보면

  1. 위와 같이 우리가 입력한 형태가 html문서에 담겨서 h1태그로 마크업된 형태로 표출된 것을 볼 수 있습니다.

  2. 또한, 개발자도구를 열어서 network탭을 열어보면, 200이라는 statusCode가 적혀있는 것과
    Response Header에 Content-Type이 text/html이라고 명시되어서 우리가 설정한 대로 적혀있는 것을 볼 수 있습니다.

  • 위의 코드에서 여러 부분을 수정하면서 실험해보고 싶을텐데, 그 경우 매번 서버를 껐다가
    (terminal에서 ctrl + c, 이후 다시 node server.native.js),
    다시 켜야지 수정사항이 반영되는 것을 알 수 있습니다

  • 그래서 nodemon이라는 라이브러리를 통해서 수정사항이 반영될 시 자동으로 서버를 reboot해주는 기능을 제공받을 수 있습니다.

  • terminal에서 yarn add nodemon --save를 해주면 설치가 되고 package.json에 dependencies에 추가된 것을 볼 수 있습니다.

  • 그 후, 매번 명령어를 다 쳐주기 귀찮으니까, script명령어로 추가해주도록 합니다.

{
  "name": "nodeapi",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "nodemon server.native.js"
  },
  "dependencies": {
    "nodemon": "^2.0.4"
  }
}
  • package.json 에 위와 같이 start script를 추가해준뒤, yarn start를 입력해주면 노드몬을 통해서 서버가 켜지고 계속해서 수정사항을 모니터링하고있다가 서버를 reboot시켜줍니다.

  • 그러면 코드에서 수정하고 저장한 뒤에 서버를 다시 킬 필요 없이, 웹브라우져에서 새로고침만 눌러주면은 변경사항대로 response가 오는 것을 볼 수 있습니다.

Express를 이용한 서버 구성

  • express는 Node.js 웹 서버 프레임워크로서, Node.js에 여러가지 편리한 기능을 추가한 웹서버 프레임워크입니다.

  • Node.js에는 많은 웹서버 프레임워크가 있지만 그 중 가장 다운로드 수가 많고 대중적으로 쓰이는 프레임워크가 Expres입니다.

  • 또한 다른 프레임워크들도 Express기반으로 짜여진 것들이 많기 때문에 Express를 사용하는 것이 권장됩니다.

  • 그럼 이제 Express를 이용해서 간단한 서버를 구성하는 법을 알아보겠습니다.

  • server.express.js 파일을 만들어준 뒤 아래의 코드를 입력해줍니다.

    var express = require("express");
    var app = express();
    
    app.get("/hello", function (req, res) {
      res.send("<h1>Hello World</h1>");
    });
    
    app.listen(8000);
  1. 위 코드를 하나씩 뜯어보면 express모듈을 import해오고(line1),

  2. app이라는 변수에 express묘듈을 실행시킨 결과를 할당해줍니다(line2)

  3. 그 뒤에 app.get이라는 메소드를 사용해줍니다.

  4. 이 메소드는 첫번째 인자로 path를 받고 두번째 인자로 handler함수를 받습니다.

  5. 첫번째 인자인 path는 흔히 우리가 요청을 보낼 때 말하는 endpoint를 의미합니다 즉
    "http://localhost:8000/hello" 라는 주소가 있을 경우 "/" 뒤의 "hello"부분을 의미합니다

  6. path를 "/" 로 설정할경우 rootPath에 대한 응답으로 설정되어서, 아무런 엔드포인트를 붙이지 않았을 경우 보내는 response을 정의할 수 있습니다.

  7. 그러면 루트 path에 대한 응답을 한번 추가해보겠습니다.

    var express = require("express");
    var app = express();
    
    app.get("/", function (req, res) {
      res.send("<h1>Root</h1>");
    });
    
    app.get("/hello", function (req, res) {
      res.send("<h1>Hello World</h1>");
    });
    
    app.listen(8000);
  • 위와 같이 코드를 수정한 후 localhost:8000로 들어갔을 경우와 localhost:8000/hello로 들어갔을 경우에 각자 다른 응답을 볼 수 있습니다. 이렇게 Routing기능을 구현하여서 하나의 서버에서 엔드포인트에 따라서 각자 다른 response를 보내줄 수 있습니다.

에러핸들링

  • 에러를 핸들링하는 로직을 추가해보겠습니다.
  1. 먼저 error를 발생시켜보겠습니다.

    app.get("/error", function(req, res){
      throw new Error("ERROR!!!!!!!");
    })
  2. 위 코드는 error라는 엔드포인트로 접근할 시 Error메세지를 보내줍니다.

  3. 그 뒤 error를 핸들링하는 로직을 추가해줍니다.

    app.use(function (err, req, res, next) {
      console.log(err.stack);
      res.status(500).send("Something wrong");
    });
  4. 이렇게 하면 error가 발생했을 시 console.log(err.stack) 에러의 스택이 콘솔에 찍히고
    500이라는 statuscode를 가진 response로 "Someting Wrong"이라는 텍스트를 send해줍니다.

  5. 이 경우 error가 발생하면 즉시 errorhandling로직으로 넘어가기에, error가 발생한 후의 로직에 아무리 추가해도 해당하는 로직은 작동하지 않습니다.

    app.get("/error", function (req, res) {
      throw new Error("ERROR!!!!!!!!");
      res.send("I'm Error Page");
    });
  • 즉 위와 같은 코드를 작성했을때 res.send("I'm Error Page")부분은 Error가 발생하고 난 뒤의 로직인데 에러가 발생할 경우 바로 에러핸들링 로직으로 전환되기에 res.send("I'm Error Page")는 실행되지 않습니다.

Routing 분리

  • Restful API는 많은 엔드포인트를 가지고 있는데 한 파일안에서 이런 모든 라우팅기능을 구현하게되면 코드가 방대해지고 복잡해지기에 모듈화를 통해서 Router파일을 분리시키는게 좋습니다.

  • 어떻게 위에서는 root path"/hello" path에 대해서만 라우팅기능을 구현했는데 bye라는 엔드포인트에 여러개의 response를 추가해보도록 하겠습니다.

  1. 먼저 route.bye.js 파일을 하나 만들어주고 아래의 코드를 쳐줍니다.

    var express = require("express");
    var router = express.Router();
    
    router.get("/", function (req, res) {
      res.send("This is bye's rootPath");
    });
    
    router.get("/world", function (req, res) {
      res.send("Bye World");
    });
    
    module.exports = router;
  2. 위의 코드를 보면 똑같이 express를 import하고 , express안의 router함수를 통해서 router객체를 만들어줍니다.

  3. router개체에다가 rootPath, "/world" path에 접근할 때에 각각 다른 응답을 주도록 설정해놨습니다.

  4. 그 다음 다시 server.express.js 파일로 이동해서

    var bye = require("./router.bye");
    
    app.use("/bye",bye);
  • 위의 코드를 추가해줍니다.

  • 코드의 내용은 router.bye파일에서 export한 router객체를 import 해온뒤 bye라는 변수명에 할당합니다.

  • 그 뒤 bye객체를 app.use를 통해서 "/bye"엔드포인트에 할당시켜줍니다.

  • 그러면 localhost:8000/bye에 접속하면, "This is bye"라는 리스폰을 확인할 수 있습니다.

  • 그 뒤에 localhost:8000/bye/world에 접속하면 "Bye World"라는 리스폰을 확인할 수 있습니다.

  • 이렇게 routing을 분리시켜서 파일들을 깔끔하게 관리하고 연관된 엔드포인트끼리 처리할 수 있습니다.

미들웨어 적용시키기

  • 미들웨어란 서버로 요청이 왔을때 response를 보내주기 전에 어떠한 로직을 처리를 하고나서 response를 해주는 중간다리 역할입니다.

  • 많은 미들웨어 기능을 해주는 라이브러리들이 있으며, 원하는 기능이 있을경우 직접 만들어서 사용할 수도 있습니다.

  • 여러분이 실제 서버를 운영하고 있는 입장이라면 들어오는 요청들에 대한 정보를 보고싶을 경우가 있을겁니다.

  • 이러한 log를 기록해주는 여러 미들웨어가 패키지형태로 존재하는데 저희는 morgan이라는 middleware를 적용시켜 보겠습니다.

  1. 먼저 install을 해줍니다 yarn add morgan --save

  2. 그 뒤 server.express.js파일에서 morgan을 import해와서 적용시키는 코드를 추가해줍니다.

    var logger = require("morgan")
    
    app.use(logger("dev"))
  3. 위 코드를 보면 morgan을 require해서 logger라는 변수에 할당해줍니다.

  4. 그리고 logger("dev")형태로 인자를 전달해서 호출한 return값을 app.use를 통해서 전역에 추가해줍니다.

  5. 개별적으로 원하는 엔드포인트에만 추가해줄수도 있습니다. 그 경우 원하는 엔드포인트에 메소드를 추가할때 인자로 전달해주면 됩니다.

    app.get("/",logger("dev"),function(req,res){
      res.send("<h1>Root</h1>")
    })
  6. 그러고 서버에 요청을 하면, 서버를 켜놓은 터미널에 GET / 304 2.405ms - -와 같은 로그가 요청이 들어올 때 마다 찍히는 것을 알 수 있습니다.

  7. logger(parameter)의 매개변수에 할당되는 인자가 뭔지 궁금할텐데, 이 부분에는
    "dev" | "common" | "combined" | "short" | "tiny" 가 들어갈 수 있습니다.

  8. 위의 인자를 통해서 log가 찍히는 형태를 조정할 수 있습니다.

  9. 현재 "dev"인자를 통해서 server.express.js파일에 적용시켜 놓은 상태인데 bye엔드포인트에는 log가 찍히지 않는 것을 볼 수 있을 것입니다.

  10. bye엔드포인트에도 morgan을 적용시켜주려면, route.bye.js파일에서 따로 middleware를 적용시켜줘야 합니다.

  11. route.bye.js 파일에서 다음과 같은 코드를 추가해줍니다.

    var logger = require("morgan")
    
    router.use(logger("combined"));

    위와 같은 코드를 적어준 뒤 http://localhost:8000/byehttp://localhost:8000로 접속했을 때 각자 로그가 어떻게 다르게 찍히는지 관찰하면

    GET / 304 0.374 ms - -
    ::1 - - [28/Sep/2020:12:54:23 +0000] "GET /bye HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
  • 위와 같이 log가 다른 형태로 찍히는 것을 볼 수 있습니다.
  • line1이 dev모드에서의 log형태이며 line2가 combined모드에서 찍히는 log형태입니다.

middleWare 만들어보기

  • 이제 미들웨어가 뭔지 알고, 적용도 시켜봤으니 간단한 미들웨어를 하나 만들어서 실습해보겠습니다.
  1. middleware.simple.js 파일을 만들어줍니다.

    var middleware1 = function (req, res, next) {
      console.log("Hello this is Simple middleWare");
      next();
    };
    
    module.exports = middleware1;
  • 위의 코드로 우리는 이제 간단한 미들웨어를 만들었습니다.

  • 위의 코드를 해석해보면 middleWare라는 변수에 익명함수를 할당해줬습니다.

  1. middleWare에 할당된 함수는(req, res, next)라는 parameter를 가지고 있습니다.
    (여기에 실질적으로 할당될 인자들은 app.use()라는 메소드 내부에서 자동적으로 할당해주는 것으로 추측됩니다)

  2. 함수의 body부분을 보면 console.log("Hello this is simple middleWare")를 동작하고

    • 이 부분이 실질적인 미들웨어에서 처리하는 로직입니다.
  3. 인자로 받은 next함수를 호출해줍니다.

    • 미들웨어는 중간다리라고 했습니다. 중간다리에서 처리해야 할 로직을 다 처리한 뒤에는 실질적으로 응답을 보내는 부분으로 이동을 시켜줘야 하기에 next함수를 호출해서 다음으로 넘겨주는 것입니다.
    app.get("/", logger("dev"), function (req, res) {
      res.send("<h1>Root</h1>");
    });    
    • 즉 rootpath로 요청이 들어오면 logger미들웨어에서 중간처리를 다 한뒤에 실질적으로 res을 전달하는

      function (req, res) {res.send("<h1>Root</h1>");

      부분으로 턴을 넘겨주기 위해서 인자로 받은 next함수를 호출하는 것입니다.

  4. 그럼 미들웨어를 만들었으니 실질적으로 한번 적용시켜 봅시다.

  5. server.express.js에 아래와 같으 코드를 추가시켜줍니다.

    var simpleMiddleWare = require("./middleware.simple");
    
    app.use(logger("dev"),simpleMiddlWare)
    • 우리가 만든 simpleMiddleware를 Import 시켜주고, 기존에 logger만 적용시켜놨던 app.use메소드에 simpleMiddleWare를 추가해줬습니다.
  6. 이제 다시 서버에 요청을 보낸후 로그가 찍힌것을 보면

    This is simple middleWare
    GET / 304 5.136 ms - -
  • 위와 같은 log가 기록된 것을 알 수 있습니다.(미들웨어는 여러개를 중첩해서 적용시킬 수 있습니다.)

  • 제가 기대한 동작은 morgan으로 찍은 log가 먼저 찍히고, 그 다음 simpleMiddleWare log가 찍히는 것을 예상했는데 morgan이 가장 마지막에 찍히는 이유는 모르겠습니다.. 아시는 분은 댓글 달아주시면 감사하겠습니다.

느낀점

  • Node.js로 프레임워크를 사용하지 않고 간단한 서버를 만들어보고

  • 그 후에 express 프레임워크를 이용해서 서버를 구축하고, Routing을 분리하고, 미들웨어를 적용시키고 간단한 미들웨어를 만들어보는 것 까지 진행했습니다.

  • 기본적인 정의와 request를 받고 response를 보내는 것 까지는 개념을 잡았는데 추후 실제 프로젝트에 적용시킬 수 있을 수준까지 공부해야겠습니다.

  • 현재 Redux미들웨어 관련해서도 공부해보고자 하는데 Node.js에서 사용하는 미들웨어도 중간다리라는 개념이 비슷한 것 같아서 시야가 좀 더 넓어진 것 같은 기분을 느낍니다.

  • 이것을 접목해서 Redux 미들웨어에 대해서도 학습해봐야겠습니다.

  • 또한 Typescript에도 관심을 가지고 배워보고자 하는데 이 블로깅을 포스팅하면서 좀 더 정확한 정보를 전달하고자 실질적으로 타입을 정의해논 파일을 보면서 어떠한 메소드가 인자로 어떤 값을 받고, 어떤 형태의 return을 하는지 결과적으로 어떻게 동작하는지 추측할 수 있다는 것을 느끼면서 타입스크립트가 전체적인 가독성을 어떻게 향상시켜주고, 개발을 효율적으로 만들어준다는 것을 실제로 느껴서 뜻밖의 수확을 얻은 공부였습니다.

profile
효율적이고 아름다운 코드를 쓰고싶은 호기심 많은 개발자입니다.

2개의 댓글

comment-user-thumbnail
2020년 10월 14일

morgan 미들웨어는 기본적으로 요청의 정보와 응답의 정보를 로깅하기 때문에 요청이 끝난 시점에 로그가 찍히게 코드가 짜여져 있습니다. 코드를 들여다 봤더니 on-finished 모듈(https://www.npmjs.com/package/on-finished) 을 사용했더군요! response 가 끝날 때 이벤트 리스너를 붙여주는 형식의 모듈입니다.

따라서 미들웨어를 중간에 넣어주어도 예상한대로 먼저 찍히는게 아니라, 가장 마지막에 찍히게 됩니다.

1개의 답글