IMMERSIVE #14 - Node.js

GunW·2019년 8월 12일
1

이번 스프린트는 서버를 다룹니다.

서버를 다루는데 있어서 가장 인기있는 Node.js에 대해 알아보겠습니다!

Node.js는 JavaScript만으로 서버를 다룰 수 있어서 유명합니다.

Express 참고하실 분들은 우측 배너에서 바로 내려가시면 됩니다 ! 👉🏻👉🏻👉🏻


Node.js

Node 공식 홈페이지에는 "Node.js는 Chrome V8 JS 엔진으로 빌드된 JS런타임이다."

라고 나와있습니다.

간단히 말하자면, 구글에서 개발한 V8은 Javascript를 기계어로 컴파일 해줍니다.

🧱 non-blocking

JavaScript언어는 non-blocking, 비동기식 언어입니다.

즉, 함수가 하나 실행되면, 다음 함수가 이전 함수의 실행 완료를 기다려주지 않습니다.

갑자기 이 얘기가 왜 나왔을까요?

Node.js는 이벤트 기반 및 non-blocking I/O Model입니다.

유저의 클릭이나 네트워크에 리소스를 요청하는 이벤트가 non-blocking으로 이루어지는

Input /Output Model 이라는 의미입니다.

그래서, 속도가 빠른 것이 장점입니다!

💡 Node Core Module

따로 설치하지 않아도 node 내에 존재하는, 번들링되어있는 모듈입니다.

대표적인 예로는 fs, http, url, path ... 등이 있습니다.

간단한 예시 코드를 볼까요?

const fs = require('fs');
const http = require('http');

fs.readFile('./something.json', (err, data) => {
	console.log(data);
});

http.get('http://localhost:8000/api', (res) => {
  console.log(res)
})

fs는 file-system, http는 client로부터 요청하거나, 서버를 생성하는 등의 역할을 합니다.

지금은 아, 그런게 있구나, 하고 지나가도 괜찮습니다.


URL

Node.js를 시작하기 전에, 잠시 확인할 게 있습니다.

생활코딩의 NodeJS강좌에서 가져온 그림입니다.

URL에서 각각의 의미를 다시 한번 생각해보고 지나가겠습니다.

  • protocol : 웹 브라우저와 웹 서버가 데이터를 주고 받기 위한 통신 규약입니다.
  • host(domain) : 인터넷에 접속된 각각의 컴퓨터를 가리킵니다.
  • port : 컴퓨터의 특정한 포트로 연결시켜주기 위한 번호입니다.
  • path : 어떤 디렉토리의 어떤 파일인지를 가리킵니다.
  • query string : 웹 서버에 전달하고 싶은 데이터를 나타냅니다.

ChatterBox-Server

먼저, express 모듈을 사용하지 않고 서버를 구성해보겠습니다.

Client는 이전에 배웠던 ChatterBox-client를 사용하겠습니다!

1. CORS

서버를 구현하다보면 CORS 에러를 많이 보셨을 거에요.

CORS 에러는 다른 도메인 서버의 URL 을 호출하여 데이터를 가져오는 경우 발생합니다.

일종의 보안 경고라고 할 수 있죠!

제가 진행하는 스프린트의 경우 http://127.0.0.1:5500/client/index.html 에서

http://127.0.0.1:3000/clients/messages 를 호출하니까 경고를 하는거죠!

물론 단순히 이런 것만 경고를 내지 않고, 몇 가지 조건이 더 있습니다.

자세한건 링크! https://developer.mozilla.org/ko/docs/Web/HTTP/Access_control_CORS

요약하면, 지정한 simple Request가 아니라면 prefilght로 OPTIONS 요청을 먼저 보내어

허가를 요청하는 것입니다.

신경쓰이신다면 먼저 Postman으로 서버부터 구현해보시는게 좋습니다!

2번에서 살짝 살펴볼게요!

그래서 이번 프로젝트는 아래처럼 간단한 CORS 회피 기법을 사용하여 진행합니다.

// request-handler.js
const defaultCorsHeaders = {
  "access-control-allow-origin": "*",
  "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
  "access-control-allow-headers": "content-type, accept",
  "access-control-max-age": 10 // Seconds.
};

2. Default Message

먼저 보낼 메세지의 default값을 지정해줍니다.

아래 result에 담아주고, username, text가 있고...다 테스트 케이스에 맞춘겁니다!

roomname은 client에 지정되어있어서 코드스테이츠로 맞췄습니다.

// request-handler.js
const message = {
  results: [
    {
      username: "Jono",
      text: "Do my bidding!",
      roomname: "코드스테이츠"
    }
  ]
};

💡postman을 활용한 테스트?

처음 들어보는데 사용까지 하기 부담스러우실 수 있습니다!

그럴땐 일단 결과를 띄워보고 사용법을 천천히 익히는 것도 좋습니다.

GET 요청에 URL을 입력하면 서버를 제대로 생성하셨다면 아래같은 데이터가 나옵니다!

POST는 어떻게 보낼까요?

아래처럼 Body에서 타입(JSON)을 지정하고 raw상태로 입력하고, Send!

정상적으로 데이터가 들어간다면 기본적인 서버구현은 완료입니다!

3. requestHandler

이제 requestHandle함수를 작성해봅시다.

미리 상태코드, 헤더를 만들어 놓고 각 method에 따라 동작을 설정합니다.

POST요청 시 BODY에 담는 부분은 NodeJS 공식 홈페이지에 자세히 나와있습니다.

url같은 경우는 req.url로 대체할 수 있지만 다양한 모듈 경험을 위해 사용해보았습니다!

이런 함수 작성 시에 가장 실수 없는 방법은 콘솔입니다!

각 METHOD를 보시면 아래 콘솔이 적혀있습니다.

그래서, 각 경로에 들어올 때마다 어떤 요청에, 어떤 url로 들어오는지 알 수 있죠!

따라하실 필요는 없지만, 권장합니다!

// request-handler.js
const url = require("url");
const requestHandler = function(req, res) {
  // MARK: required constant
  const statusCode = 200;
  const headers = defaultCorsHeaders;
  headers["Content-Type"] = "application/json";

  let pathname = url.parse(req.url).pathname;

  // MARK: except OPTIONS
  if (req.method === "OPTIONS") {
    console.log("[OPTIONS] :", req.method);
    res.writeHead(statusCode, headers);
    res.end();
  }
  // MARK: case GET && /classes/messages
  else if (req.method === "GET" && pathname === "/classes/messages") {
    console.log("[GET] :", pathname);
    res.writeHead(statusCode, headers);
    res.end(JSON.stringify(message));
  }
  // MARK: case POST && /classes/messages
  else if (req.method === "POST" && pathname === "/classes/messages") {
    console.log("[POST] :", pathname);
    let body = [];
    req
      .on("error", err => console.error(err))
      .on("data", data => (body = [...body, data]))
      .on("end", () => {
        body = Buffer.concat(body).toString();
        message.results.push(JSON.parse(body)); // 이 부분이 헷갈리시면 그냥 넣고 비교해보세요!
        res.writeHead(201, headers);
        res.end(JSON.stringify(message));
      });
  } else {
    res.writeHead(404, headers);
    res.end();
  }
};

이제 기존의 클라이언트 부분을 살짝만 수정해주면 됩니다.

4. client.js

fetch를 하는 부분에서 더이상 json을 콜백에 담는 것이 아닌,

받은 jsonData의 results를 콜백에 넣어줍니다.

넣어줄 때의 message의 형태를 생각해보세요! 간단합니다 :)

// client.js
window
      .fetch(app.server)
      .then(res => {
        return res.json();
      })
      .then(data => callback(data.results));

5. 결과

기존의 데이터가 나오고, 두 가지의 채팅을 입력해보았습니다.

간단한 서버구현이 끝났고, 이제 express모듈을 사용해볼 차례입니다!


🚄 Express

express는 서버 구현을 편리하게 해주는 모듈입니다.

Node의 기본 내장 모듈은 아니고, npm 또는 yarn으로 설치해주셔야 합니다!

설치하는 김에 필요한 모듈부터 미리 같이 설치해볼까요?

1. Install module

yarn add express cors body-parser

cors는 위에서 배웠던 CORS 해결을 위한 모듈이고,

body-parser는 POST요청 시 body를 간편하게 불러올 수 있습니다.

이전에는 배열 만들고...Buffer에서 toString하고...복잡했죠? 아래에서 써볼게요!

2. Data & Router

basic-server.js / route.js / data.json 파일을 생성합니다.

먼저 data.json은 기존의 default값을 넣어주겠습니다.

// data.json
{
  "results": [
    { "username": "Jono", "text": "Do my bidding!", "roomname": "코드스테이츠" }
  ]
}

이제 route.js를 작성해보겠습니다. 코드를 간단히 보시고 넘어가주세요!

설명은 아래쪽에 있습니다 .👇🏻

// route.js
const express = require("express");
const fs = require("fs");
const router = express.Router();

router
  // route로 동일한 url에서 GET/POST를 처리할 수 있습니다!
  .route("/classes/messages")
  .get((req, res) => {
    fs.readFile(__dirname + "/data.json", "utf8", function(err, data) {
      let messages = JSON.parse(data);
      res.send(messages);
    });
  })
  .post((req, res) => {
    fs.readFile(__dirname + "/data.json", "utf8", (err, data) => {
      let messages = JSON.parse(data);
      messages.results.push(req.body);
      res.statusCode = 201;
      res.send(JSON.stringify(messages));
      // write data to data.json
      fs.writeFile(
        __dirname + "/data.json",
        JSON.stringify(messages),
        "utf8",
        err => console.error(err)
      );
    });
  });

router객체는 각 라우팅(경로)처리를 모듈로 분리하여 사용할 수 있게 합니다.

그래서 server.js에서 모든 작업을 하지 않아도 되는 것입니다.

fs 모듈은 파일을 읽고 쓰는 것을 도와주는 모듈입니다.

fs.readFile("/data.json") 만 해도 되지만, __dirname으로 경로를 추가하면

URL에 상세 디렉토리 경로가 표시되지 않아서 보안상 좋다고 합니다!

나머지 부분은 문법적인 부분을 제외하곤 다 이해가 가실거라 생각합니다 :)

이제 뒷 부분을 보죠!

// MARK: ERROR
router.get("/error", (req, res) => {
  res.status(404).send("404 ERROR");
});
// MARK: Clear All
router.delete("/reset", (req, res) => {
  const RESET_DATA = {
    results: [
      {
        username: "Jono",
        text: "Do my bidding!",
        roomname: "코드스테이츠"
      }
    ]
  };
  fs.readFile(__dirname + "/data.json", "utf8", (err, data) => {
    fs.writeFile(
      __dirname + "/data.json",
      JSON.stringify(RESET_DATA),
      "utf8",
      (err, data) => {
        res.json(RESET_DATA);
        return;
      }
    );
  });
});

module.exports = router;

ERROR부분은 '/error'로 요청 시 에러가 나도록 임의로 설정해주었습니다.

router.delete는 데이터가 계속 쌓이고 청소가 안되서 추가하였습니다.

'/reset'으로 delete요청 시 defaultData로 다시 변경해줍니다.

readFile로 먼저 json을 읽고, writeFile로 data를 써주시면 됩니다.

module로 사용할 거니까 마지막 exports 빼먹으시면 안 됩니다!

3. server

이제 express 를 사용하여 더 간편하게 서버를 구성해보겠습니다.

먼저, PORT와 id를 설정합니다.

// basic-server.js
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = "development";
}
const PORT = process.env.NODE_ENV === "development" ? 3000 : 3001;
const ip = "127.0.0.1";

process.env.NODE_ENV는 서버의 환경 변수를 뜻합니다.

만약 기본적으로 development또는 production이 설정되어 있다면

따로 설정해주지 않아도 PORT가 정해지게 됩니다.

원래는 서버에서 내부 환경변수를 미리 설정해두어서 조건만 처리해도 true가 됩니다.

// basic-server.js
// MARK: Add Express Module
const express = require("express");
const fs = require("fs");
const path = require("path");
const bodyParser = require("body-parser");
const cors = require("cors");
const route = require("./route.js");
const app = express();

추가된 부분은 bodyParser와 불러온 router입니다.

bodyParser를 사용하면 POST요청 시 힘들게 가져오던 body를

그냥 request.body (req.body) 만으로 가져올 수 있습니다.

(업데이트로 이제 express 내장 모듈에 기본적인 기능은 포함되어 있다고 합니다.)

4. middleware

미들웨어는 요청과 응답의 중심에 있다고 해서 middleware 라고 합니다...;

조금 전에 사용한 라우터도 미들웨어의 한 종류입니다.

미들웨어는 응답과 요청을 조작하여 기능을 추가하거나 거절하는 등의 역할을 합니다.

말보다는 사용해보시는 것이 더 이해가 빠를 듯 하네요!

// basic-server.js

// ... 생략 ...
// MARK: Set middleware
app.use(cors());
app.use(bodyParser.json()); // you can change it to `app.use(express.json());`
app.use(express.static(path.join(__dirname, "./../client")));
app.use((req, res, next) => {
  console.log(`REQEUST URL => [${req.url}]`); // custom middleware
  next();
});
app.use("/", route);
app.use((req, res, next) => {
  res.status(404).send("404 ERROR");
});
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send("500 SERVER ERROR");
});

// MARK: SHOW IP & PORT
app.listen(PORT, () => {
  "Listening on http://" + ip + ":" + PORT;
});

module.exports = app;

app.use() 안에 미들웨어를 등록하시면 됩니다.

cors를 추가하여 CORS 에러를 막아줍니다.

bodyParser.json()은 express.json()으로 사용하셔도 무관합니다.

express.static을 사용하면 정적 폴더를 지정할 수 있습니다.

그래서 localhost:3000 으로 접속 시 client가 바로 실행됩니다!

custom middleware는 임의로 만들어보려고 콘솔을 찍어보았습니다.

app.use("/", route)로 이전에 작성한 라우터를 불러오면 모든 기능이 동작합니다.

뒤의 404, 500 에러는 요청 실패 / 서버 에러 시 실행됩니다.

POSTMAN이나 client로 바로 확인해보면 되겠네요!

아래 사진을 보면, 'http://127.0.0.1:3000' 으로 GET요청 시 바로 client가 나옵니다!

GET / POST / DELETE도 하나씩 확인해보세요!

express를 사용하고나니 확실히 이해도 쉽고, 코드 작성도 간단하네요!

좀 더 다양한 모듈들을 추가하여 연습해 보면 좋을 것 같습니다! 👏🏻👏🏻👏🏻

profile
ggit

2개의 댓글

comment-user-thumbnail
2020년 2월 3일

좋은글 감사합니다! 포스트맨사용이랑 서버구현에 도움이 많이되었습니다..!!

1개의 답글