[Node.js] 리팩터링

2023. 3. 27. 22:00공부 중/Node.js


 

Node.js - MySQL - 생활코딩

수업소개 이 수업은 Node.js와 MySQL을 이용해서 웹애플리케이션을 만드는 방법에 대한 수업입니다.  수업대상 예를들어 1억 개의 페이지로 이루어진 웹사이트에서 필요한 정보가 파일에 하나하나

opentutorials.org

 

생활코딩 Node.js - MySQL 강의를 듣고서 작성한 글입니다. 그냥 그렇다고요.

 


1. 현재 상황

 

 

대충 이렇다.

 

겁나 복잡하다.

 

학습과 구현을 병행했기 때문에 아주 이상한 프로그램이 되었다.

 

보다 유지보수가 쉽도록 리팩토링을 수행한다.

 


2. 개선 방안

 

리팩터링의 핵심 포인트를 짚어보자.

 


가. DB 사용

 

template.js에 위치한 메서드들이 readAndRes를 거치지 않고 바로 DB에 접근할 수 있도록 해보자.

 

 

우선 db.js를 만들어서 어디서든 db에 접속할 수 있도록 해주는 모듈을 만든다.

 

//db.js
const mysql = require("mysql2");
const db = mysql.createConnection({
  host: process.env.MYSQL_HOST,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  port: process.env.MYSQL_PORT,
});
db.connect();
module.exports = db;

main.jsconst db =require('./lib/db.js');template.jsconst db = require('./db.js);를 추가한다.

 


가. 클래스

 

 

http get method로 실행되는 작업들이 가지고 있는 패턴이 있다.

 

 

위와 같이 크게 4개의 값을 readAndRes함수로 전달한다.

 

4개의 정보를 속성으로 가지는 클래스 Ready를 생성하고 이를 상속해서 각각의 작업들이 수행되도록 해보자.

 

 

/** new Ready(title, des, control, author)   
   * 속성 title, description, contol, author, html
   * method : makeHtml, response
  */
 class Ready{
  constructor(title, des, control, author){
    this.title = title;
    this.decscription = des;
    this.control = control;
    this.author = author;
    this.html = '';
  }
  makeHtml() {
    this.html = template.HTML(this);
  }
  response(){
    response.writeHead(200);
    response.end(this.html);
  }
 }

이에 맞춰서 template.js도 수정한다.

 

//template.js
module.exports={
    HTML: function (Ready) {
    const list = this.List();
    return `...생략...`;
    },List: function () {
    db.query(`SELECT * FROM topic`,(error, topics)=>{
      if(error){throw error}
      let list = "<ul>";
      topics.forEach(topic => {
        list += `<li><a href="/?id=${topic.id}">${topic.title}</a></li>`;
      });
      list += "</ul>";
      return list;
    });
    },authorSelect:function(author_id){
    db.query(`SELECT * FROM author`,function(error, authors){
      if(error) throw error;
      let tag = '';
      authors.forEach(author => {
        let selected ='';
        if (author.id === author_id){
          selected = 'selected';
        }
        tag += `<option value="${author.id}" ${selected}>${author.name}</option>`;
      });
      return `
        <select name="author">
          ${tag}
        </select>
      `
    });
  }
};

main.js도 수정한다.

 

//main.js의 일부분. create와 udpate에도 동일한 방식으로 수정함.
        ...
        if (queryData.id === undefined) {
      const title = "Welcome :)";
      const description = "Here is for to test node.js server :)";
      const control = `
        <input type="button" value="create" onclick="redirect(this, '')"/>
      `;
      const author = '';
      const undefinedCase = new Ready(title, description, control, author);
      undefinedCase.makeHtml;
      undefinedCase.response(response);
    } else {
        ...

 


나. readAndRes

 

readAndRes 함수가 지나치게 복잡한 역할을 수행하고 있었다.

 

한 번에 한 가지 기능만 수행하는 몇 가지 함수로 나누고 Ready 클래스의 메서드로 만들었다.

 

template.HTML, template.List, template.authorSelect도 DB를 조회할 수 있도록 했다.

 


3. db.query 비동기 처리 정상화하기

 

가. bug 원인 분석

 

 

정상적으로 동작하지 않는다.

 

텅 빈 페이지만 보인다.

 

아마 Ready.makeHtml이 완료되지 않고서 Ready.response가 실행되는 것 같다.

 

 

정확하게는 list를 생성하고 있지 못하고 있다.

 

template.List에는 문제가 없음을 확인했다.

 

List: async function () {
    db.query(`SELECT * FROM topic`, (error, topics) => {
      if (error) { throw error;}
      let list = "<ul>";
      topics.forEach(topic => {
        list += `<li><a href="/?id=${topic.id}">${topic.title}</a></li>`;
      });
      list += "</ul>";
      return list;
    });
  },authorSelect:function(author_id){

template.Listdb.query가 실행되는 것만 확인하고 template.List는 동작을 모두 수행했다고 template.HTML에게 전달하고 실제 db.query의 콜백함수인 (error, topics)=>{...}의 동작이 마무리될 때까지 기다리지 않고 template.HTML이 다음 동작을 수행해서 발생한 문제다.

 

 

결론적으로 db.query의 비동기 작업이 완료될 때까지 기다려줄 필요가 있다.

 


나. 해결법

 

 

mysql2

fast mysql driver. Implements core protocol, prepared statements, ssl and compression in native JS. Latest version: 3.2.0, last published: 24 days ago. Start using mysql2 in your project by running `npm i mysql2`. There are 3527 other projects in the npm r

www.npmjs.com

 

MySQL2의 공홈을 살펴보자.

 

(1) Promise API

 

MySQL2 also support Promise API. Which works very well with ES7 async await.

(MySQL2는 Promise API도 지원합니다. 이는 ES7 async await와 매우 잘 작동합니다.)

 

async function main() {
  // get the client
  const mysql = require('mysql2/promise');
  // create the connection
  const connection = await mysql.createConnection({host:'localhost', user: 'root', database: 'test'});
  // query database
  const [rows, fields] = await connection.execute('SELECT * FROM `table` WHERE `name` = ? AND `age` > ?', ['Morty', 14]);
}

MySQL2 use default Promise object available in scope.

(MySQL2는 사용 가능한 기본 Promise 객체를 사용합니다.) ← JS의 기본 문법에 충실한 사용법인 듯.

 

  • .execute.query의 차이점 : prepare statement 사용 유무.

 

But you can choose which Promise implementation you want to use.

(하지만 원하는 Promise 구현체를 선택할 수 있습니다.)(Bluebird는 JavaScript promises 라이브러리입니다.)

 

// get the client
const mysql = require('mysql2/promise');

// get the promise implementation, we will use bluebird
const bluebird = require('bluebird');

// create the connection, specify bluebird as Promise
const connection = await mysql.createConnection({host:'localhost', user: 'root', database: 'test', Promise: bluebird});

// query database
const [rows, fields] = await connection.execute('SELECT * FROM `table` WHERE `name` = ? AND `age` > ?', ['Morty', 14]);

 

(2) pool

 

MySQL2 also exposes a .promise() function on Pools, so you can create a promise/non-promise connections from the same pool.

(MySQL2는 또한 풀에서 .promise() 함수를 노출시켜 동일한 풀에서 프로미스 또는 논-프로미스 연결을 만들 수 있습니다.)

 

async function main() {
  // get the client
  const mysql = require('mysql2');
  // create the pool
  const pool = mysql.createPool({host:'localhost', user: 'root', database: 'test'});
  // now get a Promise wrapped instance of that pool
  const promisePool = pool.promise();
  // query database using promises
  const [rows,fields] = await promisePool.query("SELECT 1");

MySQL2 exposes a .promise() function on Connections, to "upgrade" an existing non-promise connection to use promise.

(MySQL2는 연결(Connections)에 .promise() 함수를 노출시켜 기존의 non-promise 연결을 promise를 사용하도록 "업그레이드"할 수 있습니다.)

 

// get the client
const mysql = require('mysql2');
// create the connection
const con = mysql.createConnection(
  {host:'localhost', user: 'root', database: 'test'}
);
con.promise().query("SELECT 1")
  .then( ([rows,fields]) => {
    console.log(rows);
  })
  .catch(console.log)
  .then( () => con.end());

 


다. 적용

 

공식 홈페이지의 설명을 따라서 db.query를 처리를 기다리기 위해서 다음과 같이 코드를 수정한다.

 

//main.js
class Ready{
  constructor(title, des, control, author){
    this.title = title;
    this.description = des;
    this.control = control;
    this.author = author;
    this.html = '';
  }
  async makeHtml() {
    this.html = await template.HTML(this);
  }
  response(response){
    response.writeHead(200);
    response.end(this.html);
  }
}
  • async makeHtml() {this.html = await template.HTML(this);}
    : template.HTML이 비동기 함수라서 makeHtml도 비동기 선언

 

//main.js
        ...
        if (queryData.id === undefined) {
      ...
      const undefinedCase = new Ready(title, description, control, author);
      undefinedCase.makeHtml()
      .then(()=>{undefinedCase.response(response)});
    } else {
  • undefinedCase.makeHtml().then(()=>{undefinedCase.response(response)});
    : Promise chaining. 비동기 함수인 .makeHtml이 완료된 후에 .response 실행

 

//template.js
module.exports ={
    HTML: async function (Ready) {
    const list = await this.List();
    console.log(list);
    return `...`;
    },List: async function () {
    try {
      const promiseDB = db.promise();
      const [topics, fields] = await promiseDB.execute(`SELECT * FROM topic`);
      let list = "<ul>";
      topics.forEach(topic => {
        list += `<li><a href="/?id=${topic.id}">${topic.title}</a></li>`;
      });
      list += "</ul>";
      return list;
    } catch (error) {
      throw error;
    }
    } ...
  • HTML: async function (Ready) { const list = await this.List();
    : 비동기 함수 this.List()를 기다려준다.
  • const promiseDB = db.promise();
    : const promiseDB = await mysql.createConnection({...});와 동일한 효과.
    : 그냥 db는 안된다!!!!!!!!!!!!! 허흑..
  • const [topics, fields] = await promiseDB.query(SELECT * FROM topic);
    : destructuring operator 형식으로 작성되었다.
    : 실행 결과를 기다려준다.

 

 

출처 : https://riverblue.tistory.com/20

출처 : https://surprisecomputer.tistory.com/31

출처 : https://ljtaek2.tistory.com/142


라. 동기, 비동기

 

자바스크립트는 기본적으로 싱글 스레드를 기반으로 동기 방식으로 작동한다.

 

  • 동기

자바스크립트의 엔진의 주요 구성 요소(Memory Heap, Call Stack) 사용.

Memory Heap : 변수와 객체의 메모리 할당을 담당하는 곳

Call Stack : 함수가 호출이 되면 쌓이는 곳이다. LIFO.

 

  • 비동기

자바스크립트 실행 환경(Runtime)에서는 자바스크립트 엔진 자체가 제공하지 않는 일부 기능

 

  • 비동기를 구현하는 3가지 방법(콜백함수, Promise 객체, async&await)

 

 

[Jest] Testing Asynchronous Code

0. 참고자료 Testing Asynchronous Code · Jest It's common in JavaScript for code to run asynchronously. When you have code that runs asynchronously, Jest needs to know when the code it is testing has completed, before it can move on to another test. Je

ramen4598.tistory.com