서버 운영에 있어서 성능과 안정성은 매우 중요한 요소들 중 하나입니다. 이 글에선 Express 서버를 이용할 때 여러 워커를 클러스터 구성을 이용해 서비스하고 NGINX를 이용해 이중화 구성을 하는 방법에 대해 알아보도록 합니다.

 

 

 

0. 사전 준비.

 

이 글에서 웹서버는 React와 Express를 사용해 배포한 서버를 사용할 예정입니다. 실제 배포는  Docker를 사용해 도커 이미지를 생성한 후 실행시키도록 할 예정입니다. 미리 NPM와 Docker를 준비합시다.

마지막으로 모든 작업 결과물을 한번에 구동시키기 위해 Docker compose를 사용할 예정이니 Docker compose도 준비해 주세요.

 

 

 

1. NodeJS

 

 

NodeJS는 구글이 구글 크롬에 사용하려고 제작한 V8 오픈소스 자바스크립트 엔진을 기반으로 제작된 자바스크립트 런타임입니다. NodeJS는 다음과 같은 특징이 있습니다.

  1. 단일 스레드.
  2. 비동기 방식.
  3. 이벤트 루프를 사용.
  4. NPM.

NodeJS는 싱글 스레드이기 때문에 하나의 CPU를 여럿이 나눠 갖는 건 비효율적입니다. 따라서 CPU 숫자에 맞춰서 서버를 띄워보겠습니다.

 

 

 

2. 웹서버 생성.

 

 

우선 프론트를 준비합니다. 다음 명령어로 react app을 생성해 주세요.

 

$ npx create-reac-app my-react

 

이 앱을 Express 서버로 서비스할 예정입니다. 폴더로 들어가서 다음 패키지를 설치하세요.

 

$ cd my-react

 

$ npm i express express-favicon 

 

이제 express 서버를 실행할 코드를 작성해야 합니다. server 폴더에 index.js파일을 만드신 후 다음과 같이 코딩해 주세요.

 

//// ./server/index.js

const express = require('express');
const favicon = require('express-favicon');
const path = require('path');

const app = express();
const port = 3000;
const server = app.listen(port, () => {
    console.log(`Server is listening on port ${server.address().port}.`);
});

app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, '../build')));

app.get('/ping', (req, res) => {
    return res.send('pong');
});
app.get('/*', (req, res) => {
    return res.sendFile(path.join(__dirname, '../build', 'index.html'));
});

 

이제 기본으로 생성된 react를 빌드해 줍시다.

 

$ npm run build

 

여기까지 수행하셨으면 폴더 구조가 다음과 같을 겁니다.

 

 

우리가 작성한 express서버가 react app을 잘 서빙하는지 확인해 봅시다. localhost:3000으로 이동해 react 앱을 확인해 보세요. 그리고 localhost:3000/ping으로 이동해 pong 메시지를 잘 수신하는지도 확인합시다.

 

 

 

3. 클러스터 구성.

 

앞서 설명한 대로 NodeJS는 단일 스레드입니다. 이제 우린 서버의 CPU개수에 맞춰서 여러 워커를 띄어서 서버를 운영해 보도록 하겠습니다.

 

여러 워커가 돌아갈 때 워커가 동작하는 서버를 구분하기 위해 uuid 패키지를 설치합니다.

 

$ npm i uuid

 

그리고./server/index.js를 다음과 같이 수정해주세요.

 

//// ./server/index.js
const cluster = require('cluster');
const os = require('os');
const uuid = require('uuid');

const port = 3000;
const instance_id = uuid.v4();

//// Create worker.
const cpu_count = os.cpus().length;
const worker_count = cpu_count / 2;

//// If master, create workers and revive dead worker.
if(cluster.isMaster){
    console.log(`Server ID: ${instance_id}`);
    console.log(`Number of server's CPU: ${cpu_count}`);
    console.log(`Number of workers to create: ${worker_count}`);
    console.log(`Now create total ${worker_count} workers ...`);

    //// Message listener
    const workerMsgListener = (msg) => {
        const worker_id = msg.worker_id;
        //// Send master's id.
        if(msg.cmd === 'MASTER_ID'){
            cluster.workers[worker_id].send({cmd: 'MASTER_ID', master_id: instance_id});
        }
    }

    //// Create workers
    for(var i = 0; i < worker_count; i ++){
        const worker = cluster.fork();
        console.log(`Worker is created. [${i +1}/${worker_count}]`);
        worker.on('message', workerMsgListener);
    }

    //// Worker is now online.
    cluster.on('online', (worker) => { console.log(`Worker is now online: ${worker.process.pid}`); });

    //// Re-create dead worker.
    cluster.on('exit', (deadWorker) => {
        console.log(`Worker is dead: ${deadWorker.process.pid}`);
        const worker = cluster.fork();
        console.log(`New worker is created.`);
        worker.on('message', workerMsgListener);
    });
}
//// If worker, run servers.
else if(cluster.isWorker){
    const express = require('express');
    const favicon = require('express-favicon');
    const path = require('path');

    const app = express();
    const worker_id = cluster.worker.id;
    const server = app.listen(port, () => { console.log(`Server is listening on port ${server.address().port}.`); });

    let master_id = "";

    //// Request master's id to master.
    process.send({worker_id: worker_id, cmd: 'MASTER_ID'});
    process.on('message', (msg) => {
        if(msg.cmd === 'MASTER_ID'){
            master_id = msg.master_id;
        }
    });

    app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
    app.use(express.static(__dirname));
    app.use(express.static(path.join(__dirname, '../build')));

    app.get('/ping', (req, res) => { return res.send('pong'); });
    app.get('/where', (req, res) => { return res.send(`Running server: ${master_id} \n Running worker: ${worker_id}`); });
    app.get('/kill', (req, res) => { cluster.worker.kill(); return res.send(`Called worker killer.`);})
    app.get('/*', (req, res) => { return res.sendFile(path.join(__dirname, '../build', 'index.html')); });
}

 

소스가 길어졌으니 주석을 확인하면서 차근차근 코딩해 주세요. 간단히 설명해드리자면 다음과 같습니다.

  1. 우리는 한 서버의 CPU개수의 절반만큼의 워커를 생성할 겁니다.
  2. 만약 현재 생성된 클러스터가 마스터라면 워커 클러스터를 생성하고 관리하는 역할을 수행합니다.
  3. 마스터는 미리 정해진 개수만큼 워커를 생성하고 만약 죽은 워커가 발견된 경우 새 워커를 생성시켜 항상 일정 개수의 워커가 서버에서 동작할 수 있도록 합니다.
  4. 워커는 express 서버를 구동합니다.
  5. 워커에는 어느 서버에서 수행되고 있는지 확인할 수 있는 기능과 현재 워커를 죽일 수 있는 기능이 추가되었습니다.

코딩이 끝났다면 다시 서버를 실행시켜 보도록 합시다.

 

$ node server

 

콘솔에서 다음과 같은 메시지를 확인할 수 있습니다.

 

 

이제 localhost:3000/where로 이동해 보세요. 현재 우리가 접속한 서버와 워커의 번호를 확인할 수 있습니다. 여기서 localhost:3000/kill로 이동하면 워커를 죽일 수 있으며 이때 마스터는 죽은 워커를 확인 해 새 워커를 생성합니다. 

 

 

이후 다시 localhost:3000/where로 이동하면 워커의 번호가 변경된 것을 확인할 수 있습니다.

 

 

 

4. Docker를 이용한 서버 구동

 

 

이제 우리가 작성한 서버를 도커를 이용해 배포해봅시다. 간단히 배포하기 위해 Dockerfile을 사용해 배포할 예정입니다. 

 

Dockerfile 파일을 생성 한 뒤 다음과 같이 작성해 주세요.

 

# ./Dockerfile

FROM node:slim

# app 폴더 생성.
RUN mkdir -p /app

# 작업 폴더를 app폴더로 지정.
WORKDIR /app

# dockerfile과 같은 경로의 파일들을 app폴더로 복사
ADD ./ /app

# 패키지 파일 설치.
RUN npm install

# 환경을 배포 환경으로 변경.
ENV NODE_ENV=production

#빌드 수행
RUN npm run build

ENV HOST=0.0.0.0 PORT=3000
EXPOSE ${PORT}

#서버 실행
CMD ["node", "server"]

 

아주 간단한 Dockerfile을 작성했습니다. 주석을 참고하시면 모두 이해하실 수 있을 겁니다. 이제 다음 명령어를 통해 작성한 Dockerfile을 이용해 도커 이미지를 생성합니다.

 

$ docker build . -t my-react:0.0.1

 

 

도커 이미지가 정상적으로 생성된 것을 확인할 수 있습니다. 다음 명령어를 통해 직접 실행시켜 주세요.

 

$ docker run -itd -p 8080:3000 my-react:0.0.1

 

이제 localhost:8080로 이동하면 모든 기능을 정상적으로 사용할 수 있습니다.

 

 

 

 

 

 

 

Storybook 패키지에 대해 알아봅니다.

 

 

 

1. Storybook이란?

 

최근 프론트엔드 개발은 페이지 단위가 아닌 컴포넌트 단위로 개발된 다는 것을 들어본 적이 있을 겁니다. 프로젝트 내에서 컴포넌트를 개발하다 보면 초기에는 별 문제가 없지만 프로젝트의 규모가 커질수록 컴포넌트 개발에 독립성을 유지하기 힘들어집니다. 이런 경우 스토리북을 이용하면 보다 편하게 컴포넌트를 개발할 수 있습니다.

 

Build bulletproof UI components faster
Storybook is an open source tool for developing UI components in isolation for React, Vue, and Angular. It makes building stunning UIs organized and efficient.

 

스토리북의 공식 홈페이지를 방문하면 가장 먼저 우리를 반겨주는 문구입니다. 위의 설명대로 스토리북은 독립된 환경에서 컴포넌트를 개발하고 이를 확인해 볼 수 있는 오픈소스 라이브러리 입니다. 이러한 환경에서 개발된 컴포넌트는 독립성이 유지되기 때문에 자연히 재사용성이 좋아집니다. 또한 스토리북은 개발뿐 아니라 문서화, 테스팅에 대해서도 좋은 기능을 제공해 줍니다.

 

 

 

2. Storybook 사용해보기.

 

먼저 스토리북을 사용하기 위해 예시 프로젝트를 생성합니다. 이 글에서는 create-react-app을 통해 예시 프로젝트를 생성하겠습니다.

 

> create-react-app storybook-ex

> cd storybook-ex 

> npx -p @storybook/cli sb init --type react_scripts

 

여기까지 진행한 뒤 package.json파일을 확인합니다. script항목에 보면 전에 없던 스토리북에 관련된 스크립트를 확인할 수 있습니다. 

 

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "storybook": "start-storybook -p 9009 -s public",
    "build-storybook": "build-storybook -s public"
  },

 

이제 스토리북을 실행시켜 9009번 포트로 접속해 봅시다.

 

> npm run strorybook

> http://localhost:9009/로 이동.

 

 

정상적으로 스토리북이 실행된 것을 확인할 수 있습니다. 좌측에 "Welcom"과 "Button"이 보이시나요? 이것들을 스토리북에선 스토리라고 부릅니다. 스토리란 결국 우리가 컴포넌트를 구현하게 될 독립적 공간이 되는것 입니다. 

 

이제 다시 프로젝트로 돌아가서 위의 스토리들이 어디에 위치하는지 확인해 봅시다. ../.storybook 폴더 내의 main.js 파일을 열어보세요.

 

module.exports = {
  stories: ['../src/**/*.stories.js'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
};

 

스토리북에서 사용하는 애드온들과 스토리에 대한 설정이 정의되어 있습니다. stories에 설정된 값을 확인해보면 src 폴더 내에 있는 .stories.js로 끝나는 모든 파일을 스토리 파일로 인식하도록 설정되어 있습니다. 실제로 위의 "Welcom"과 "Button" 스토리들은 src폴더 내의 stories폴더에 존재하고 있습니다.

 

새로운 스토리를 생성하기 위해서는 src폴더 내 임의 폴더에 파일이름.stories.js로 파일을 생성 한 뒤 컴포넌트를 작성하면 됩니다.

 

 

 

 

 

Loopback4를 이용해 Postgresql과 연결하는 방법에 대해 알아봅니다.

 

 

 

 

1. Loopback4 및 Postgresql 준비

 

이 글을 참고해 Loopback4를 미리 준비합니다.

 

이 글을 참고해 Postgresql을 미리 준비합니다.

 

 

 

2. Model 정의하기.

 

우선 먼저 모델을 정의해 봅시다. 여기서 정의한 모델은 Postgresql의 테이블과 같은 개념이 된다고 보시면 됩니다. 다음 명령어를 통해 User 모델을 생성합니다.

 

> lb4 model

 

? Model 클래스 이름: User
? 모델 기본 클래스를 선택하십시오. Entity (A persisted model with an ID)
? 추가(자유 양식) 특성을 허용합니까? No
Model User will be created in src/models/user.model.ts

특성을 User에 추가합니다.
완료되면 빈 특성 이름 입력

? 특성 이름 입력: id
? 특성 유형: number
? ID 특성이 id입니까? Yes
? id이(가) 자동으로 생성되었습니까? Yes

다른 특성을 User에 추가합니다.
완료되면 빈 특성 이름 입력

? 특성 이름 입력: user_email
? 특성 유형: string
? 필수입니까? Yes
? 기본값 [leave blank for none]: 

다른 특성을 User에 추가합니다.
완료되면 빈 특성 이름 입력

? 특성 이름 입력: user_name
? 특성 유형: string
? 필수입니까? Yes
? 기본값 [leave blank for none]: 

다른 특성을 User에 추가합니다.
완료되면 빈 특성 이름 입력

? 특성 이름 입력: 
   create src/models/user.model.ts
   update src/models/index.ts

Model User was created in src/models/

 

위의 명령을 따라 User 모델을 생성하면 .\src\models\user.model.ts파일이 자동으로 생성됩니다. id에 대해 우린 id값을 y로 선택해 id속성이 ture가 된 것을 확인할 수 있으며 자동생성 옵션을 켰기 때문에 generate도 true로 설정된 것을 확인할 수 있습니다. 이는 User테이블의 PK는 id가 될 것이며 이 값은 별도로 받지 않아도 자동으로 증가하는 값을 가질 것을 의미합니다.

 

 

 

3. Datasource 생성하기.

 

모델을 만들었으니 이제 우리가 사용할 DB가 Postgresql임을 알리는 Datasource를 생성해 봅니다. 다음명령어를 통해 ds라는 이름을 가지는 datasource를 생성합니다.

 

> lb4 datasource

 

? Datasource 이름: ds
? Select the connector for ds: PostgreSQL (supported by StrongLoop)
? Connection String url to override other settings (eg: postgres://username:password@localhost/database): postgresq://smoh:{PASSWORD}@{DBHOST}:{DBPORT}/lb4
? host: {DBHOST}
? port: {DBPORT}
? user: smoh
? password: [hidden]
? database: lb4
   create src/datasources/ds.datasource.config.json
   create src/datasources/ds.datasource.ts

 

위와같이 값을 입력해 ds를 생성해 주시면 됩니다. 가려진 위치에는 미리 준비한 postgresql의 정보를 채워 주시면 됩니다. 이 예시에서는 lb4라는 db를 사용할 예정입니다.

 

생성된 datasource는 ./src/datasources/ds.datasource.* 파일입니다. 만약 정보를 잘못 기입하셨어도 config.json에서 정보를 수정할 수 있습니다.

 

 

 

4. Repository 생성

 

Model과 Datasource가 준비되었으니 이제 Repository를 생성할 수 있습니다. 다음 명령어를 통해 DefaultCrudRepository를 생성해 보도록 하겠습니다.

 

> lb4 repository

 

? 데이터 소스를 선택하십시오. DsDatasource
? 저장소를 생성할 모드 선택 User
? 저장소 기본 클래스를 선택하십시오. DefaultCrudRepository (Legacy juggler bridge)
   create src/repositories/user.repository.ts
   update src/repositories/index.ts

Repository UserRepository was created in src/repositories/

 

그 후 ./src/repositories 로 이동하면 ds와 User을 이용해 user.repository.ts가 자동으로 생성된 것을 확인할 수 있습니다.

 

 

 

5. Controller 생성

 

Repository까지 준비되었으니 남은건 실제 동작을 구현하는 것입니다. 다음 명령어로 Controller를 추가합니다.

 

> lb4 controller

 

? Controller 클래스 이름: User
Controller User will be created in src/controllers/user.controller.ts

? 생성할 제어기는 어떤 유형입니까? REST Controller with CRUD functions
? 이 CRUD 저장소에서 사용할 모델 이름은 무엇입니까? User
? CRUD 저장소의 이름은 무엇입니까? UserRepository
? ID 특성의 이름은 무엇입니까? id
? ID의 유형은 무엇입니까? number
? 인스턴스를 새로 작성할 때 ID가 생략되었습니까? Yes
? CRUD 오퍼레이션의 기본 HTTP 경로 이름은 무엇입니까? /users
   create src/controllers/user.controller.ts
   update src/controllers/index.ts

Controller User was created in src/controllers/

 

이제 ./src/controllers/user.controller.ts에서 자동으로 생성된 컨트롤러를 확인할 수 있습니다. 여기에 엔드포인트와 원하는 작업을 구현하시면 됩니다.

 

 

 

6. DB Migrate

 

잠깐 뒤돌아봅시다. 우리가 지금까지 DB작업을 한적이 있었나요? 모델을 만들었는데 DB에 User테이블은 있나요? 물론 직접 DB에 붙어서 수동으로 테이블을 만들어 줄 수도 있습니다만 Loopback은 그럴 필요 없이 자동으로 모델에 맞게 테이블까지 만들어줍니다!

 

다만 이 기능은 공식 문서에도 불안정하다고 적혀있으므로 사용에 주의가 필요합니다. 직접 DB의 데이터를 건드려 모델과 다른점을 수정하기 때문에 데이터가 유실되거나 심지어 테이블 정보가 변경될 수 있습니다.

 

마이그레이션을 하는 방법은 여러가지가 있습니다. 프로그램이 시작될 때 자동으로 진행되게 하는 방법이 있고 명시적으로 개발자가 직접 진행하는 방법이 있습니다. 실무에선 어떻게 사용될지 모르겠지만 일단 DB작업이므로 개발자가 직접 수행하는 방법에 대해서 설명하도록 하겠습니다.

 

다음 명령어를 차근차근 수행해 주세요

 

> npm run clean

 

> pg-app@1.0.0 clean /Users/smoh/Documents/Dev/Loopback/pg-app
> lb-clean dist *.tsbuildinfo .eslintcache

 

위 명렁을 수행하고 나면 ./dist 폴더가 사라진 것을 확인할 수 있습니다.

 

> npm run build

 

> pg-app@1.0.0 build /Users/smoh/Documents/Dev/Loopback/pg-app
> lb-tsc

 

다시 dist 폴더에 파일이 생성된 것을 확인할 수 있습니다. 클린 후 다시 빌드함으로써 지금까지 우리가 작업한 내용을 반영시켜 줍니다.

 

진짜 마이그레이션을 진행하기 전에 ./dist/migrate.js파일을 확인해 봅시다.

 

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const application_1 = require("./application");
async function migrate(args) {
    const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter';
    console.log('Migrating schemas (%s existing schema)', existingSchema);
    const app = new application_1.PgAppApplication();
    await app.boot();
    await app.migrateSchema({ existingSchema });
    // Connectors usually keep a pool of opened connections,
    // this keeps the process running even after all work is done.
    // We need to exit explicitly.
    process.exit(0);
}
exports.migrate = migrate;
migrate(process.argv).catch(err => {
    console.error('Cannot migrate database schema', err);
    process.exit(1);
});
//# sourceMappingURL=migrate.js.map

 

중간에 매개변수를 확인하는 로직이 보이시나요? existingSchema의 값을 정할 때 "--rebuild" 옵션의 여부에 따라 이미 테이블이 존재할 때 동작을 결정할 수 있습니다. 

 

진짜 마이그레이션을 수행해봅시다.

 

> npm run migrate -- --rebuild 혹은 >npm run migrate

 

> pg-app@1.0.0 migrate /Users/smoh/Documents/Dev/Loopback/pg-app
> node ./dist/migrate "--rebuild"

Migrating schemas (drop existing schema)

 

--rebuild 옵션을 주어 이미 테이블이 존재했으면 drop후 재생성 한다는 메시지가 출력되었습니다. 이제 실제 DB로 가 보면 테이블이 모델에 맞게 자동으로 생성되었음을 확인할 수 있습니다.

 

 

이제 준비가 끝났습니다. 다음 명령어를 통해 Loopback을 실행시킵시다.

 

> npm start

 

 

 

7. 테스트

 

실행시킨 후 http://localhost:3000/explorer/ 로 이동하게 되면 꽤나 깔끔한 페이지가 보입니다. 우리가 생성한 컨트롤러에 맞춰서 Loopback이 자동으로 이 페이지를 만들어 줍니다.

 

 

 

postman을 통해 테스트를 진행해도 되지만 이 페이지에 보이는 컨트롤러를 직접 클릭해 테스트해 볼수도 있습니다.

 

 

동작중 하나를 클릭 후 Try it out을 눌러보세요.

 

 

그러면 위와같이 where 조건을 수정할 수 있게 됩니다. 동작에 맞게 적당히 수정 후 아래의 Execute 버튼을 눌러보세요.

 

 

아래의 Server response 영역에 처리 결과를 바로 확인해 볼 수 있습니다.

 

 

 

 

 

루프백을 사용하는 방법에 대해 알아봅니다.

 

 

 

 

1. Loopback

 

루프백을 한마디로 설명하자면 다음과 같습니다.

 

LoopBack은 Node.js에서 API 및 마이크로 서비스를 구축하기 위한 플랫폼입니다.

 

또한 공식 문서에서 추가적으로 루프백에 대해 다음과 같이 설명하고 있습니다.

 

LoopBack은 Express를 기반으로하는 확장 성이 뛰어난 오픈 소스 Node.js 및 TypeScript 프레임 워크로, 데이터베이스 및 SOAP 또는 REST 서비스와 같은 백엔드 시스템으로 구성된 API 및 마이크로 서비스를 신속하게 작성할 수 있습니다.

 

아래 다이어그램은 어떻게 LoopBack이 들어오는 요청과 나가는 결과들 사이에 다리 역할을 하고 있는지 보여줍니다. 또한 루프백이 제공하는 다양한 기능들도 보여줍니다.

 

 

이 글을 작성하는 시점에서 최신 버전인 Loopback4를 사용하도록 하겠습니다. Loopback3은 현재 여전히 마이그레이션 중이며 몇몇 기능은 루프백 4에서 동작하지 않을 수 있습니다. 이러한 차이점에 대해선 별도로 다루지 않으며 이 글을 참고해 주시기 바랍니다.

 

 

 

2. LoopBack4 시작하기

 

루프백4는 NodeJs 버전 10 이상을 요구합니다. 미리 설치해 줍시다.

 

NodeJs를 설치하고 난 뒤 아래의 명령어를 통해 LoopBack4 CLI를 설치합니다.

 

> npm i -g @loopback/cli

 

이 LoobBack CLI 도구는 프로젝트를 스캐폴딩 하고 TypeScript 컴파일러를 구성하며 필요한 모든 종속성을 설치합니다. 이제 다음과 같이 명령어를 입력해 프로젝트를 생성합니다.

 

> lb4 app

 

 

옵션을 선택해 프로젝트를 구성합니다. 구성이 완료되면 만들어진 프로젝트 폴더로 이동해 루프백을 실행합니다.

 

> cd loopback

> npm start

 

정상적으로 실행되었는지 확인해보기 위해 http://localhost:3000/ping 로 이동합니다. "greeting":"Hello from LoopBack"이 포함된 json 응답이 보이면 정상적으로 실행된 것입니다.

 

 

 

3. 컨트롤러 추가하기.

 

이제 간단하게 새로운 컨트롤러를 추가해 봅시다. 일단 Ctrl + C를 눌러 실행 중인 루프백을 먼저 종료해 줍니다. 그런 뒤 다음 명령어를 통해 새로운 컨트롤러를 추가합니다.

 

> lb4 controller

 

 

안내에 따라 컨트롤러 이름을 정한 뒤 유형을 선택합니다. 예시에서는 빈 컨트롤러를 선택하였습니다.

 

이제 디렉터리에 우리가 생성한 컨트롤러가 추가된 것을 확인할 수 있습니다. .\src\controllers\hello.controller.ts 파일을 열어보세요.

 

// Uncomment these imports to begin using these cool features!

// import {inject} from '@loopback/context';


export class HelloController {
  constructor() {}
}

 

빈 컨트롤러를 선택했기 때문에 아무런 동작을 하지 않습니다. 이제 여기에 hello에 대한 응답으로 "Hello world!"를 보내주도록 코드를 작성해 봅시다.

 

import { get } from "@loopback/rest";

export class HelloController {
    @get("/hello")
    hello(): string {
        return "Hello world!";
    }
}

 

이제 다시 루프백을 실행 후 http://localhost:3000/hello 로 이동합니다. 

 

 

정상적으로 보이시나요? 이렇게 원하는 컨트롤러를 추가해줄 수 있습니다.

 

 

 

 

 

 

Express 서버에서 Graphql을 사용하는 방법에 대해 알아봅니다.

 

 

 

1. express-graphql 설치

 

이 글에서는 express 서버에서 graphql을 사용하는 방법에 대해 알아보기로 했습니다. GraphQL을 따로 사용할 수도 있지만 이 경우에 어울리는 패키지가 이미 존재합니다. 

 

다음 명령어를 통해 Graphql과 espress-graphql을 설치합니다.

 

> npm install graphql express-graphql express

 

이제 다음과 같이 코딩한 뒤 직접 접속해 봅시다.

 

const express = require('express');
const graphqlHTTP = require('express-graphql');
const app = express();

app.use('/graphql', graphqlHTTP({
    schema: MyGraphQLSchema,
    graphiql: true
}));

app.get('/', function(req, res){
    res.send('Hello Express!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

접속이 되시나요? MyGraphQLSchema 때문에 실행이 안된다면 정상입니다. express-graphql 공식 가이드에 따르면 schema 항목에 대해 다음과 같은 설명이 있습니다.

 

schema: A GraphQLSchema instance from GraphQL.js. A schema must be provided.

 

이제 진짜 스키마를 제공하기 위해 직접 작성해 봅시다.

 

 

 

2. Schema 작성 및 사용.

 

이제 GraphQL의 buildSchema를 사용해 스키마를 만들고 사용해 보도록 하겠습니다.

 

다음과 같이 코드를 수정합니다.

 

const express = require('express');
const graphqlHTTP = require('express-graphql');
const { graphql, buildSchema } = require('graphql');
const app = express();

var mySchema = buildSchema(`
  type Query {
    hello: String
  }
`);

app.use('/graphql', graphqlHTTP({
    schema: mySchema,
    graphiql: true,
}));

app.get('/', function(req, res){
    res.send('Hello Express!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

이제 실행이 되시나요? graphiql 페이지로 이동한 후 직접 쿼리를 날려봅시다

 

 

슬프게도 null이 리턴됩니다. 아직 우리의 작업이 다 끝나지 않았다는 말이죠. 생각해 봅시다. 우리가 지금까지 hello에 어떤 값을 리턴해줘라 라는 코드를 작성한 적이 있나요?

 

이제 hello가 어떤 값을 리턴해야 하는지에 대한 코드를 작성해 보겠습니다. 겸사겸시 다른 스키마도 정의해서 수정해 보도록 합시다.

 

const express = require('express');
const graphqlHTTP = require('express-graphql');
const { graphql, buildSchema } = require('graphql');
const app = express();

var mySchema = buildSchema(`
  type Query {
    hello: String
    users: [User]
  }
  type User{
    email: String
    name: String
  }
`);

var root = { 
    hello: () => 'Hello world!',
    users: () => { 
        return [
            {email: 'A@A.com', name: 'A'},
            {email: 'B@B.com', name: 'B'},
            {email: 'C@C.com', name: 'C'},
        ];
    },
};

app.use('/graphql', graphqlHTTP({
    schema: mySchema,
    graphiql: true,
    rootValue: root,
}));

app.get('/', function(req, res){
    res.send('Hello Express!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

이제 다시 Graphiql로 이동해 다음 쿼리를 수행해 봅시다.

 

 

 

정상적으로 데이터가 출력되는 것을 확인 할 수 있습니다.

 

 

 

 

 

Express 서버와 pg 패키지를 이용해 PostgreSql 커넥터를 만들고 DB에서 데이터를 가져옵니다.

 

 

 

0. Postgre DB 준비.

 

코딩에 앞서 먼저 DB를 준비합니다. 이 글에서 별도로 Postgre DB를 설치하는 방법에 대해 작성하진 않습니다.

 

이 글을 참조하셔서 미리 DB를 준비해 주시기 바랍니다.

 

 

 

1. Express서버 준비.

 

express 패키지를 설치 한 뒤 다음과 같이 코딩해 express서버를 준비합니다.

 

> npm intall express

 

var express = require('express');
var app = express();

app.get('/', function(req, res){
    res.send('Hello World!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

 

 

2. PostgreSql 커넥터 작성.

 

pg 패키지를 설치한 뒤 다음과 같이 코딩해 Postgresql 커넥터를 준비합니다.

 

> npm install pg

 

const vals =  require('./const.js');
const { Pool, Client } = require('pg');

const client = new Client({
    user: vals.user, password: vals.pass,
    host: vals.host, port: vals.port,
    database: vals.db
});

function GetUserList() {
    client.connect();
    client.query('SELECT * FROM users', (err, res) => {
        console.log(res);
        client.end();
    });
};
 
module.exports = {
    getUserList: GetUserList
}

 

** const.js에는 DB 접속에 필요한 정보가 담겨있습니다.

** 별도의 js 파일이 아닌 해당 값을 직접 user, password, hose, port, databse에 넣어주셔도 됩니다.

 

 

 

3. Express에서 커넥터 호출하기

 

작성한 커넥터를 호출하기 위해 Express의 코드를 수정합니다.

 

var express = require('express');
var app = express();

var pgDBConn = require('./pgDBConn.js');

pgDBConn.getUserList();

app.get('/', function(req, res){
    res.send('Hello World!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

이제 Express 서버를 실행시키면 콘솔 창에 정상적으로 데이터를 가져오는 것을 확인할 수 있습니다.

 

 

 

 

 

이 글은 Velopert의 "리액트를 다루는 기술"을 참고하였습니다.

 

 

1. 불변성 관리

 

리액트를 다루면서 불변성을 유지하면서 상태를 업데이트하는 것이 중요하단 걸 모든 개발자는 알 것입니다.

이를 위해 전개 연산자 및 배열의 내장 함수를 사용하면 배열이나 객체를 복사하고 새로운 값을 덮어쓸 수 있습니다.

그러나 객체의 구조가 커지고 깊이가 깊어지면 불변성을 유지하면서 업데이트하기가 매우 힘들어집니다. 값 하나를 업데이트하기 위해 수많은 코드를 작성해야 할 수도 있습니다.

 

이러한 상황에서 immer 라이브러리를 이용하면 구조가 복잡한 객체도 짧은 코드를 사용해 불변성을 유지하면서 업데이트를 할 수 있습니다.

 

 

 

2. immer

 

Immer은 보다 편리한 방식으로 불변의 상태로 작업을 가능하게 해주는 패키지입니다. copy-on-write 메커니즘을 기반으로 합니다.

기본 아이디어는 모든 변경사항을 currentState의 프록시인 임시 draftState에 반영하는 것입니다. 

이후 모든 변경이 끝이 나면 Immer는 draftState에 대한 변경내용을 기반으로 nextState를 생성합니다.

 

 

즉, Immer는 현재 상태를 가져와서 변경 사항을 기록하기 위해 draft를 만들고 작업이 끝나면 draft를 토대로 nextState를 만듭니다.

 

기본적으로 Immer는 다음과 같이 사용합니다.

import produce from 'immer';
const nextState = produce(orgState, draft => {
  //값 바꾸기
  draft.somewhere.deep.inside = changedValue;
});

procude 함수는 두 파라미터를 받습니다. 첫 번째 파라미터는 수정하고 싶은 상태이며 두 번째 파라미터는 상태를 어떻게 업데이트할지 정의하는 함수 합니다.

draft내부에서 값을 변경하면 produce 함수가 불변성을 유지하면서 상태를 업데이트해 줍니다.

 

 

 

3. 불변성 유지를 위해 Immer를 사용하지 않을 때

 

다음 예시 코드는 Immer를 사용하지 않고 불변성을 유지할 때의 예시 코드입니다.

 

import React, {useRef, useCallback, useState} from 'react';

const App = () =>{
  const nextId = useRef(1);
  const [form, setForm] = useState({name: '', userName: ''});
  const [data, setData] = useState({
    array: [],
    uselessValue: null
  });

  const onChange = useCallback(e=>{
    const {name, value} = e.target;
    setForm({...form, [name]: [value]});
  }, [form]);

  const onSubmit = useCallback(e=>{
    e.preventDefault();
    const info = {
      id: nextId.current,
      name: form.name,
      userName: form.userName
    };
    setData({...data, array: data.array.concat(info)});
    setForm({name: '', userName: ''});
    nextId.current += 1;
  }, [data, form.name, form.userName]);

  const onRemove = useCallback(id=>{
    setData({...data, array: data.array.filter(info=>info.id !== id)});
  }, [data]);

  return(
    <div>
      <form onSubmit={onSubmit}>
        <input name="userName" placeholder="ID" value={form.userName} onChange={onChange}/>
        <input name="name" placeholder="Name" value={form.name} onChange={onChange}/>
        <button type="submit">Register</button>
      </form>
      <div>
        <ul>
          {data.array.map(info=>(
            <li key={info.id} onClick={()=>onRemove(info.id)}>
              {info.userName} ({info.name})
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export default App;

위와 같이 전개 연산자와 배열의 내장 함수를 사용하면 어렵진 않지만 상태가 복잡해지면 귀찮은 작업이 필요하게 될 수 있습니다. 

 

 

 

4. 불변성 유지를 위해 Immer를 사용할 때

 

다음 예시 코드는 Immer를 적용한 코드입니다.

 

import React, {useRef, useCallback, useState} from 'react';
import produce from 'immer';

const App = () => {
    const nextId = useRef(1);
    const [form, setForm] = useState({ name: '', userName: '' });
    const [data, setData] = useState({
      array: [],
      uselessValue: null
    });

  const onChange = useCallback(e=>{
    const {name, value} = e.target;
    setForm(
      produce(form, draft=>{
        draft[name] = value;
      })
    );
  }, [form]);

  const onSubmit = useCallback(e=>{
    e.preventDefault();
    const info = {
      id: nextId.current,
      name: form.name,
      userName: form.userName
    };
    setData(
      produce(data, draft=>{
        draft.array.push(info);
      })
    );
    setForm({name: '', userName: ''});
    nextId.current += 1;
  }, [data, form.name, form.userName]);

  const onRemove = useCallback(id=>{
    setData(
      produce(data, draft => {
        draft.array.splice(draft.array.findIndex(info=>info.id === id), 1);
      })
    );
  }, [data]);

  return(
    <div>
      <form onSubmit={onSubmit}>
        <input name="userName" placeholder="ID" value={form.userName} onChange={onChange}/>
        <input name="name" placeholder="Name" value={form.name} onChange={onChange}/>
        <button type="submit">Register</button>
      </form>
      <div>
        <ul>
          {data.array.map(info=>(
            <li key={info.id} onClick={()=>onRemove(info.id)}>
              {info.userName} ({info.name})
            </li>
          ))}
        </ul>
      </div>
    </div>
  );    
};

 

Immer는 필수적으로 사용해야 하는 라이브러리는 아닙니다. 사용하는 게 더 불편하다면 사용할 이유가 없습니다. 단지 복잡한 객체의 상태를 업데이트해야 할 때 많은 도움이 되며 생산성을 높여줍니다.

 

 

 

 

 

1. Redux란 무엇인가.

 

한 문장으로 말하자면 "Redux는 상태를 좀 더 효율적으로 관리할 수 있게 해주는 라이브러리"입니다.

 

 

 

2. Flux 패턴

 

Redux 공식 홈페이지의 소개에 따르면 "Redux는 Flux의 중요한 특징들로부터 영감을 얻었습니다. Flux와 마찬가지로 Redux에서는 애플리케이션의 특정 레이어에 있을 모델 업데이트 로직에 집중할 수 있도록 해줍니다"라고 명시되어 있습니다.

그렇다면 Flux란 무엇일까요?

 

Flux 패턴은 MVC 패턴의 양방향 통신이 가진 복잡함을 제거하기 위해 컴포넌트 간 통신을 단일화시킨 패턴을 말합니다.

위의 그림과 같이 데이터는 디스패처에 의해 단방향으로 흐르게 되는 것이 Flux 패턴의 핵심입니다.

 

 

 

3. Redux

 

다시 Redux로 돌아와서 결국 Redux는 Flux패턴의 구현체 중 하나란 것이다.

물론 공식 홈페이지에 따르면 "Redux를 Flux의 구현 중 하나라고 생각할 수 있을까요? 그렇기도 하고, 아니기도 합니다."라고 적혀있습니다. 

Redux는 Flux패턴의 철학을 추구한 구현체가 맞습니다만 정확히 같지는 않습니다. 가장 큰 차이점은 Flux와 달리 Redux에는 디스패처라는 개념이 존재하지 않는다는 것입니다.

좀 더 명확히 하자면  "Flux는 (state, action) => state 형식으로 묘사되곤 합니다. 따라서 Redux 역시 Flux 아키텍처라고 이야기할 수 있지만, 순수 함수를 통해 이를 더 간단하게 만듭니다"라고 설명되어 있습니다.

 

 

 

 

jqGrid를 사용하고 있는데 패치 이후부터 컬럼 순서를 변경하면 데이터 표시가 이상하게 된다는 이슈가 왔다.

 

이상하게 꼬였다.

 

위와 같이 헤더 컬럼 이동 후 실제 데이터의 위치가 잘 안맞는 경우 다음과 같은 코드로 확인해보자.

//(grid 선언부의 옵션설정 코드)
....
sortable: function (permutation) { console.log(permutation.join(',')); },
...

sortable에 위와같은 함수를 입력하고 컬럼을 드래그 해보자

 

디버깅 창에 찍힌 결과를 보면 

뭔가 이상한점을 발견할 수 있었다.

colModel에서 문제를 수정후 정상 작동한다.

 

References: Choosing between Babel and TypeScript

 

Choosing between Babel and TypeScript - LogRocket Blog

Babel 7 shipped about six months ago with built-in TypeScript syntax support. This means that projects using Babel can now use TypeScript, without ever needing to complicate their builds with the TypeScript compiler. But what are the differences between us

blog.logrocket.com

 

다음의 내용은 위의 글을 번역한 내용입니다.

 

 

 

바벨과 타입 스크립트 중 어떤 걸 사용해야 할까?


Babel 7은 약 6 개월 전에 TypeScript 구문 지원이 내장되어 출하되었습니다. 즉, Babel을 사용하는 프로젝트는 이제 TypeScript 컴파일러로 빌드를 복잡하게 할 필요 없이 TypeScript를 사용할 수 있습니다.
그러나 Babel과 TypeScript 컴파일러를 사용하는 경우의 차이점은 무엇이고 당신의 프로젝트에 Babel 또는 TypeScript중 어떤 걸 사용해야 할까요?

 

 

Babel과 TypeScript의 차이점

TypeScript를 사용하고 TypeScript를 Babel과 사용하는 데는 몇 가지 중요한 차이점이 있습니다.
이 글에서는 가장 중요한 네 가지 차이점을 살펴 보겠습니다.

1. 타입 검사 없음

Babel은 당신의 화려한 TypeScript의 타입에 대해서는 신경 쓰지 않습니다. 타입들이 옳다는 것을 확인하지 않고 그냥 쓰레기통에 버립니다. 아래 예제는 Babel을 사용하여 오류나 경고 없이 컴파일되지만 TypeScript에서는 컴파일되지 않습니다.

 

const myCoolString : string = 9;

 

9는 확실히 문자열이 아닙니다.
바벨을 사용하면 타입이 정확하지 않더라도 코드를 컴파일할 수 있는 빠른 프로토 타이핑에 탁월한 방법이 될 수 있습니다.
타입스크립트를 사용해 당신이 타입을 정하는 데에 노력을 기울이는 중이라면, 어느 시점에 당신은 아마 그것이 옳았다는 것을 확인하기를 원할 것입니다.  운 좋게 큰 문제가 아닙니다. 에디터가 처리하도록 하거나, "tsc --noEmit"를 실행하여 아무것도 컴파일하지 않고 프로젝트를 typecheck 할 수 있습니다.

2. Const enums

기본적으로 TypeScript는 한 번에 전체 프로젝트를 컴파일하지만 Babel은 한 번에 하나의 파일 만 컴파일합니다.

즉, Babel은 TypeScript의 기능중 여러 파일을 읽어야 하는 기능을 지원하지 않습니다. 좋은 소식은 그리 많은 기능이 포함되는 건 아니라는 것입니다. 가장 널리 퍼진 것은 아마도 enum입니다.

const enum에 대해서는 TypeScript가 아무것도 컴파일 하지 않습니다. 그것을 사용하는 방법과 트랜스 파일 된 코드가 무엇인지 살펴보겠습니다.

 

const enum FRUITS {
   APPLE = 'APPLE',
   PEAR = 'PEAR',
}

if (someString === FRUITS.APPLE){
   console.log("This is an apple!");
}


다음의 짧은 문장이 트랜스 파일된 코드입니다.

 

if (someString === "APPLE" /* APPLE */) {
   console.log("This is an apple!");
}


빵! 전체 enum 구조가 없어졌으며 TypeScript는 FRUITS.APPLE을 "APPLE"값으로 간단히 설명합니다.

하지만 이 const 열거 형을 내보내고 다른 파일에서 사용하려고 하면 어떻게 될까요? Babel은 한 번에 하나의 파일에만 액세스 할 수 있기 때문에 다른 파일에서 FRUITS.APPLE을 인라인 할 수 없습니다. 대신 단순히 오류가 발생합니다.

이것은 그리 심각하지 않습니다. Const enum은 대개 Babel이 잘 지원하는 일반 열거 형의 성능을 최적화하는 데에만 사용됩니다.

3. 데코레이터 및 메타 데이터

데코레이터들에 대해서 TypeScript는 조금 이릅니다. TypeScript가 데코레이터를 구현한 후에 데코레이터 제안이 여러 번 변경되었으며 아직 마무리되지 않았습니다.

이것이 의미하는 바는 현재 ECMAScript 스펙과 TypeScript가 데코레이터의 작동 방식을 직접 눈으로 보지 못한다는 것입니다. Babel의 플러그인은 ECMAScript 사양을 따르므로 Babel은 TypeScript와 동일한 방식으로 데코레이터를 컴파일하지 않습니다. 다행스럽게도 바벨은 legacy모드가 있어 예전의 동작으로 데코레이터를 컴파일할 수 있습니다.

바벨 플러그인 "@ babel / plugin-proposal-decorators"를 추가하고 레거시 옵션을 true로 설정하면 됩니다.

그러나 Babel은 제공하지 않지만 TypeScript에서 제공하는 데코레이터 기능 중 하나로 emitDecoratorMetadata가 있습니다.

TypeScript는 일반적으로 모든 타입 정보를 지우므로 런타임에 존재하지 않습니다. emitDecoratorMetadata는 데코레이터가 적용된 클래스 및 메서드에 대해 타입을 유지하는 기능입니다.

런타임에 타입을 사용하면 Dependency Injection과 TypeScript 타입을 SQL 데이터베이스의 타입에 매핑하는 등 모든 종류의 멋진 작업을 수행할 수 있습니다.

이 기능은 TypeORM, TypeGoose, inversifyJS 및이 기능에 따라 Angular의 종속성 주입 시스템과 같은 라이브러리와 함께 사용하면 상당히 유용합니다.

emitDecoratorMetadata의 부재는 Babel을 사용할 때 가장 큰 문제 일 것입니다. 이 기능에 의존하는 라이브러리 중 일부는 대단히 유용하지만 Babel과 함께 사용할 수는 없습니다.

4. 사용자 정의 변환

Babel은 TypeScript보다 훨씬 확장성이 뛰어납니다. 코드를 최적화하는 많은 플러그인이 있으며, 사용되지 않은 import, 인라인 상수 등을 제거 할 수 있습니다.

TypeScript에는 사용자 정의 변형을 허용하는 자체 Transformer API가 있지만 Babel의 환경에선 플러그인 선택의 폭이 넓으며 액세스가 훨씬 용이합니다.

사용자 정의 변환이 필요한 경우 Babel을 사용해야합니다. 다행스럽게도 대부분의 TypeScript 도구를 사용하면 TypeScript를 사용하고 나중에 바벨을 통해 코드를 실행하여 두 가지 장점을 모두 활용할 수 있습니다. 그러나 이것은 분명히 빌드 체인에 추가 복잡성을 가져옵니다.

기타 비 호환성 및 불일치

주로 문법 제약 및 레거시 TypeScript 기능과 관련된 몇 가지 다른 비 호환성이 있습니다. 누구에게나 방해물이 되어서는 안 되지만 여기에 발표되어 있습니다

 

 

성능

TypeScript와 Babel 7 모두로 React 앱을 컴파일하려했는데, 라이브 리로딩과 웜 캐시에서 중요한 차이점을 알 수 없었습니다. 벤치 마크는 ts-loader에 대한 것이었습니다. ts-loader는 TypeScript 용 두 개의 webpack 로더 중 가장 느린 것이었습니다 (다른 하나는 awesome-typescript-loader입니다).

물론, webpack을 구성하려고 시도하는 사람은 누구나 알고 있듯이 JavaScript 툴체인은 엄청나게 복잡합니다. 소스 맵 플러그인, 캐싱, 사용할 스레드 수 사이의 선택들이 계속됩니다. 단순한 벤치 마크는 전체 이야기를 고려할 수는 없지만, TypeScript 컴파일러에 비해 Babel을 사용하면 여러 배로 증가할 것으로 예상되는 경우 다른 곳에서 성능 향상을 찾아야 합니다.

 

 

당신은 무엇을 선택해야 합니까?

많은 자바 스크립트 개발자들처럼, 나는 반짝이고 새로운 것을 좋아합니다. 이 기사를 쓰기 시작했을 때 나는 Babel 로의 전환이 빌드 체인을 간소화하고 적은 단점이 있는 더 효과적인 빌드를 제공하기를 바랐습니다.

불행히도, 내 결론은 꽤 반대였습니다. 눈에 띄는 성능 향상은 없으며 단점도 원래 예상했던 것보다 훨씬 큽니다.

특히, 데코레이터 메타 데이터를 방출하지 않는 Babel은 내게 매우 적합하지 않습니다. 그 이유 하나만으로도 TypeScript 프로젝트가 있다면 Babel로 전환하는 것을 권장하지 않습니다.

Babel은 TypeScript를 처음 사용하거나 프로젝트를 점진적으로 마이그레이션 하려는 경우 교육용 바퀴로 유용할 수 있습니다. 어느 시점에서, 당신은 아마 훈련 바퀴를 벗어나고 싶을 것입니다.

Babel만 제공하는 사용자 정의 변형이 필요한 경우, 최상의 빌드 파이프 라인은 TypeScript 파일을 TypeScript 컴파일러로 전달한 다음 나중에 Babel로 전달하는 것입니다.

다소 복잡합니다. 하지만 누구도 자바 스크립트 빌드 툴체인이 쉽지 않다고 말한 적이 없습니다.

 

 

 

+ Recent posts