토이프로젝트 - 서버/클라이언트/DB 구현하기

창현·2021년 2월 12일
3


클라이언트 / 서버 / DB 서버가 데이터를 교환하는 과정을 단순한 구조로 구현해봤습니다.

위 구조를 구현하려면 많은 부분에서 Promise를 사용하게 되는데요, 좋은 토이 프로젝트 주제인 것 같아 소개해봅니다😀

결과물은 코드샌드박스에서 확인할 수 있습니다.




먼저 Internet을 구현해보겠습니다.

1. Internet

인터넷 네트워크 망에 있는 Client.jsServer/js를 간단하게 만들어봤습니다.

이 둘은 http.js로 통신하게 됩니다.

1.1 Client.js

인터넷 network에 포함될 Client 생성자입니다.

network는 빈 배열로 선언되어있습니다.

import network from "./network";

function Client(origin) {
  this.origin = origin; // 

  this.catch = (message) => {
    console.log("client catch response", message);
    return message;
  };

  network.push(this);
	// Client를 생성할 경우 network.push(this)를 통해 network 망에 노드를 push합니다.

  console.log(`client ${origin} is connected to network`);
}

export default Client;

클라이언트 구성자가 갖는 속성과 메소드입니다.

  • origin : http 통신 시 필요한 클라이언트 호스트 정보입니다. // ex) http://localhost:3000
  • catch : 서버가 http를 통해 origin과 message를 전송할 경우, origin과 일치하는 Client는 catch 메소드를 통해 메세지를 전달받습니다.

구성자 함수가 인스턴스를 생성할 경우 network에 인스턴스(this)를 push해줍니다.



1.2 Server.js

서버 구성자가 갖는 속성과 메소드입니다.

  • domain : 서버의 domain 정보 ex) localhost:3000
  • scheme : http/https와 같은 네트워크 요청 프로토콜
  • listen : server에서 port를 listen
  • Router : 라우터 구성자 함수
  • route : server에서 라우터로 라우팅하기 위한 메소드
  • get : route 메소드를 통해 GET 요청을 라우팅
  • post : route 메소드를 통해 POST 요청을 라우팅
  • put
  • delete
  • patch

1) Server를 생성할 경우

입력으로 들어오는 요청 프로토콜 scheme과 domain 정보를 속성으로 저장합니다.

또한 여러 메소드를 생성합니다.

function Server({ scheme = "http", domain }) {
  this.domain = domain;
  this.scheme = scheme;
	// listen, Router, route, ...
}

2) Server를 listen할 경우

서버는 scheme과 domain , port를 통해 origin( ex http://localhost:3000 )을 저장하고 routers 객체를 생성합니다. (server가 router를 생성할 경우 routers에 저장할 예정입니다!)

인스턴스를 network로 push해주고, callback을 실행해줍니다.

listen 메소드는 화살표 함수로 선언되었습니다. 따라서, this는 여전히 Server 구성자 함수를 가리킵니다. (babel repl을 통해 확인할 수 있습니다.)

function Server(){
	this.listen = (port, callback) => {
    this.origin = `${scheme}://${domain}:${port}`;
    this.routers = {};

    network.push(this);
    callback();

    console.log(`server ${this.origin} is connected to network`);
  };
}

3) Server는 여러 라우터를 통해 path에 대한 요청을 라우팅합니다.

하나의 path에 여러 메소드의 요청이 전달되므로 routers 객체는 메소드에 따라 router를 관리하도록 구현했습니다.

route 메소드는 server가 갖는 Router 구성자(후술)를 사용해 routers 객체에 라우터 인스턴스를 생성합니다. 이 때 this.route는 화살표 함수이므로 this는 Server를 가리킵니다.

클라이언트가 요청을 전달할 경우, Server는 라우터를 통해 메세지를 전달받도록 구현했습니다. 따라서 Router는 server 정보를 가지고 있습니다.

this.route = (method, path, cb) => {
    if (!this.routers[method]) {
      this.routers[method] = {};
    }

    const server = this.origin;
    this.routers[method][path] = new this.Router({ server }, path, cb);
  };
  this.get = (path, cb) => this.route("GET", path, cb);
  this.post = (path, cb) => this.route("POST", path, cb);
  this.put = (path, cb) => this.route("PUT", path, cb);
  this.delete = (path, cb) => this.route("DELETE", path, cb);
  this.patch = (path, cb) => this.route("PATCH", path, cb);

// usage 위 메소드는 아래처럼 사용될 예정입니다.
app.get('cards', (req, res) => {
	console.log(req)
	res.status.json({})
})

4) Router 구성자 함수

클라이언트의 요청을 전달받을 경우 router를 통해 요청을 전달받게끔 구현했습니다. (실제도 이렇게 동작하는지는 모르겠지만요..)

먼저 서버의 GET 라우터를 정의하는 get 메소드를 살펴보겠습니다.

// 위의 route 메소드를 통해 server.get을 통해 라우터를 정의하게 되면
server.get('cards', (req,res) => {
	// 비동기 처리 ( db로부터 데이터 요청 등등 )

	res.status(200).json({data: [1,2,3]})
})

// 아래처럼 routers 객체에 router가 저장될 것입니다.
server.routers = {
	GET: {
		cards:{
			server : 'localhost:3000',
			path : 'cards',
			callback : (req,res) => {
				// 비동기 처리 ( db로부터 데이터 요청 등등 )

				res.status(200).json({data: [1,2,3]})
			},
			catch : async (request) => {}
		}
	}
}

/*

 저장된 router는 message를 catch하게 될 경우 

 server에서 정의한 (req,res)=>{
		// async functions...

	  // res.status(200)
		// res.json({}) or res.send('response message')

 } 
    callback을 실행하고, callback은 응답 메세지를 생성하게 됩니다. 
		생성된 응답 메세지는 catch 함수에서 http를 통해 client로 메세지를 전송하게 되구요.
*/

그렇다면 Router의 catch함수를 살펴보겠습니다.

catch 함수는

  1. request 메세지를 전달받고
  2. 비동기 처리를 포함하는 callback을 실행하고
  3. callback이 무사히 실행될 경우 리턴되는 응답 message를 client에 http를 통해 전송해줍니다.

위의 코드에서 res.json을 실행하는 순간, 응답 message를 생성할 것입니다.

한 편 catch 함수에서는 callback 내에서 res.jsond이 생성한 response message를 이용해 http를 전송하는데요. 이 때, catch함수는 res가 response message를 생성하게 되는 시점에! http를 전송해야만 합니다.

그렇다면 catch 함수는 catch 함수 내에서 실행되지 않는 비동기 과정을 어떻게 기다릴 수 있을까요?

이 때 Promise가 유용하게 사용됩니다.

이제부터 callback으로 전달되는 res를 resolver라 정의하겠습니다.

resolver는 status/setHeaders와 같은 메소드를 이용해 응답 message의 헤더, 상태를 입력하고

send/json을 통해 message를 resolve해줍니다. 비동기 처리가 진행되는 동안 Promise는 pending상태가 유지되며, 비동기 처리를 마치고 message가 resolve될 때 message를 리턴하며 Promise는 fulfilled 상태가 됩니다.

catch문에서는 Promise가 resolve 될 경우 message를 전달받고, 이 때 http를 요청할 수 있게 됩니다.

  this.catch = async (request) => {
    console.log("server catch request", request);
    const client = request.headers.host;

    request.body = JSON.parse(request.body);

    const resolveResponsePromise = () =>
      new Promise((resolve, reject) => {
        const server = this.server;
        const message = { headers: { server } };
        const resolver = {
          send: (sentence) => {
            message.body = sentence;
            message.headers.server = server;
            resolve(message);
          },
          json: (json) => {
            message.body = JSON.stringify(json);
            message.headers.server = server;
            resolve(message);
          },
          status: (code) => {
            message.status = code;
            return resolver;
          },
          setHeaders: (headers) => {
            message.headers = { ...message.headers, ...headers };
            return resolver;
          }
        };

        try {
          this.callback(request, resolver);
        } catch (e) {
          reject(e);
        }
      });

    const response = await resolveResponsePromise();

    return http(client, response);
  };

1.3 http

client와 server는 http 프로토콜을 이용해 서로 통신합니다.

구현을 아주아주 간소화해 server와 client는 origin과 message만을 이용해 network에서 수신자를 찾아 message를 전달합니다.(수신자에서 catch 메소드를 실행해 message를 전달받습니다.)

http에서 사용되는 util을 잠시 살펴보겠습니다.

TimeoutPromise

일정 시간 후에 Promise를 리턴하는 함수로 네트워크 요청 시 소요되는 시간을 구현하기 위해 사용됩니다.

export const TimeoutPromise = (cb, sec) =>
  new Promise((res, rej) => {
    setTimeout(() => res("resolved"), sec);
  }).then(() => new Promise(cb));

http 비동기 함수는 network에 존재하는 nodes 중 origin(destination)이 일치하는 node를 찾고, 해당 node(receiver)의 catch 메소드를 이용해 일정 시간 후에 message를 전달받도록 구현했습니다.

http는 message의 헤더 정보를 통해 client와 server를 구분합니다.

client일 경우 receiver가 곧바로 message를 catch하고

server일 경우 method, path에 일치하는 router에서 message를 catch합니다.

import network from "./network";
import { TimeoutPromise } from "../utils";
import { CLIENT_SERVER_SPEED } from "../variable";

const http = async (destination, message) => {
  try {
    // network에서 receiver를 찾고
    const receiver = await TimeoutPromise((res, rej) => {
      const findedReceiver = network.find(
        (node) => node.origin === destination.origin
      );

      if (!findedReceiver) {
        rej(new Error("url is not defined"));
      }
      res(findedReceiver);
    }, CLIENT_SERVER_SPEED);

    // 전송하는 메세지가 서버가 전송하는 응답 message일 경우
    if (message.headers.server) {
      return receiver.catch(message);
    }

    // 전송하는 메세지가 클라이언트가 전송하는 요청 message일 경우
    if (message.headers.host) {
      if (!receiver.routers[message.method]) {
        throw new Error(
          `${message.method} method is not possible in ${destination}`
        );
      }

      const receiverRouter = receiver.routers[message.method][destination.path];
      return receiverRouter.catch(message);
    }
  } catch (e) {
    throw new Error(e);
  }
};

export default http;

사용법

Server와 Client를 생성해줍니다.

Server는 4000 port를 listen하고 /nums로 요청이 들어올 경우, [1,2,3,4]를 json 형태로 응답합니다.

client를 브라우저에서 이용하기 위해 window 속성에 client를 저장해두고, fetch 함수를 정의해줍니다.

// example
import { Server, Client, http } from "./internet";
import { TimeoutPromise } from "./utils";

const exampleServer = new Server({ domain: "localhost", scheme: "http" });
exampleServer.listen(4000, () => console.log("listening 4000 port..."));
exampleServer.get("/nums", async (req, res) => {
  try {
    const nums = await TimeoutPromise((res) => res([1, 2, 3, 4]), 1000);
    res.json(nums);
  } catch (e) {
    res.json({ status: 400, message: "error" });
  }
});

const exampleClient = new Client("http://localhost:6000");
window.exampleClient = exampleClient;

exampleClient.fetch = async (server, options = {}) => {
  const { method = "GET", headers = {}, body = {} } = options;
  const host = {
    origin: window.exampleClient.origin
  };
  const message = {
    method,
    headers: {
      host,
      // browser default headers...
      ...headers
    },
    body: JSON.stringify(body)
  };

  const { origin, path } = server;
  try {
    const response = await http({ origin, path }, message);
    return {
      raw: response,
      json: () => JSON.parse(response.body)
    };
  } catch (e) {
    throw new Error(e);
  }
};

window.exampleClient
  .fetch(
    {
      origin: "http://localhost:4000",
      path: "/nums"
    },
    {
      method: "GET"
    }
  )
  .then((res) => res.json())
  .then((data) => console.log(data, "asdasdsa"))
  .catch((e) => {
    throw new Error(e);
  });
profile
띵가띵가

0개의 댓글