타입스크립트 적용기,서버 라우팅 작업

이민택·2020년 6월 30일
0

FINAL 프로젝트

목록 보기
2/4
post-thumbnail

타입스크립트?

타입스크립트는 마이크로소프트사에서 개발한 자바스크립트 상위 호환 언어이다 타입스크립트는 자바스크립트 엔진을 사용하기 때문에 자바스크립트 프로그램을 그대로 가져다 사용할 수 있다 타입스크립의 특징을 타입을 직접 정해 줄 수 있기 때문에 정적 타입언어에서 지원하는 컴파일 과정에서 에러 처리를 할 수도 있고 ide의 지원을 통해 서로 타입이 일치 하지 않는 부분은 일반 정적인 언어처럼 코드 밑에 밑줄을 그어 에러로그를 띄어준다 이런 타입스크립트의 특징 덕분에 서버개발에 객체지향 모델을 적용하기 한결 수월해졌고 이로 인해 추후의 유지보수에도 도움이 된다.

타입스크립트 적용기

위와 같은 타입스크립트의 이점을 활용하기 위해서 이번 파이널 프로젝트에서 서버를 구현에 활용해 보기로 하였다 처음 이를 타입스크립트를 공부하고 어떻게 적용할지 대략적인 감을 잡기 위해서 타입스크립트 express 튜토리얼과 공식문서 예시 코드를 보면서 대략적인 감을 잡아 갔다

클래스를 이용한 app.ts 작성

타입스크립트의 클래스를 활용한 이점을 가져가기 위해서 express application을 생성하는 app 클래스를 이용해서 작성하였다. 대략적인 작성 방식은 아래와 같았다. 아래와 같이 작성한 클래스를 server.ts파일에서 생성해준 다음에 listen메소드를 호출하여 서버를 작동시키는 구조였다 이와 같은 방식을 가져간 이유는 application의 몸집을 줄이고 미들웨어 처리 부분과 서버를 열어주는 listen메소드를 구분해 줄 수 있고 아래에는 없지만 에러 처리 부분 구분지을 수 있는 장점이있다

class App {
  public app: express.Application;

  public port: number;

  constructor(port) {
    if (!this.app) {
      this.app = express();
      this.port = port;
      this.initializeMiddlewares();
    }
  }

  private initializeMiddlewares() {
 	//DB 커넥션
    createConnection(config)
      .then(() => {
        console.log('Database Connected :)');
      })
      .catch((error) => console.log(error));
    this.app.use(cookieParser());
    this.app.use(
      bodyParser.urlencoded({
        extended: false,
      }),
    );
    this.app.use(morgan('dev'));
    this.app.use('/', index.verifyToken, router);
  }

  public listen() {
    this.server = this.app.listen(this.port, () => {
      console.log(`App listening on the port ${this.port}`);
    });
  }
}

export default App;

typeorm 공부 및 적용기

TypeOrm은 타입스크립트에 맞춰서 만들어진 Orm이다 간략하게 Orm의 개념을 알고 넘어가자

ORM

ORM은 데이터 베이스의 정보를 불러와서 객체로 맵핑해주는 기법을 이야기한다 보통 일반적으로 node.js 는 ORM으로 Sequelizer를 사용한다.

이번 프로젝트에서 TypeOrm을 사용하게 된 이유는 Sequelizer를 이용해서 마이그레이션하는 과정에서 TypeScript와 호환이 잘 되지 않아서 선택했다

연동

TypeOrm을 TypeScript에 연동하는 방법은 간단하다 먼저 TypeOrm의 설정을 작성하는 ormconfig.json을 생성한다 이 파일은 js파일로 생성해도 된다 대신 작성방식을 js파일에 맞게 작성하면 된다

{
    "type": "mysql",
    "host": "localhost",
    "port": 3306,
    "username": "test",
    "password": "test",
    "database": "test",
    "entities": ["src/entity/*.js"],
    "logging": true,
    "synchronize": true
  }

여기서 "entities"는 해당 커넥션에서 사용될 entitiy 의 위치가 들어가면된다. 빌드하고 entity가 인식이 안된다면 여기에 빌드 후에 생성되는 entity들의 위치를 추가해주면 된다.

express 앱에서 TypeOrm 커넥션을 불러오기 위해서는 createConnection 메소드를 호출해야한다. 호출하는 방식은 여러가지가 있다

  1. createConnection의 promise 패턴안에서 app을 실행하는 방식
import * as express from "express";
import {Request, Response} from "express";
import {createConnection} from "typeorm";
import {User} from "./entity/User";

// create typeorm connection
createConnection().then(connection => {
    const userRepository = connection.getRepository(User);
  
    const app = express();
    app.use(express.json());
  
    app.get("/users", async function(req: Request, res: Response) {
        const users = await userRepository.find();
        res.json(users);
    });

    // start express server
    app.listen(3000);
});
  1. 클래스 패턴의 app의 미들웨어 선언부에서 createConnection 호출
class App {
  public app: express.Application;

  public port: number;

  constructor(port) {
    if (!this.app) {
      this.app = express();
      this.port = port;
      this.initializeMiddlewares();
    }
  }

  private initializeMiddlewares() {
 	//DB 커넥션
    createConnection(config)
      .then(() => {
        console.log('Database Connected :)');
      })
      .catch((error) => console.log(error));
    this.app.use('/', router);
  }

  public listen() {
    this.server = this.app.listen(this.port, () => {
      console.log(`App listening on the port ${this.port}`);
    });
  }
}
});

2번째 방식은 프로젝트에서 가져간 방식이다 처음에 이렇게 호출해도 연동이 될지 의아했지만 연동이 잘되어 이 방식을 그대로 가져갔다

마이그레이션과 seed

프로젝트 기간동안 데이터 베이스 테이블 핸들링을 간편하게 가져가기 위해 마이그레이션을 이용하였다 TypeOrm에서 마이그레이션을 하기 위해서는 ormconfig에서 synchronize 속성을 false로 해줘야 한다 그렇지 않으면 마이그레이션이 되어도 현재 선언되어있는 entity 의 동기화 되어 테이블이 선언된다.

module.exports = {
  type: 'mysql',
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DBNAME,
  synchronize: false,
  logging: false,
  entities: ['src/entity/**/*.ts', 'entity/*.js'],
  migrations: ['src/migrations/*.ts'],
  subscribers: ['src/subscriber/**/*.ts'],
  cli: {
    entitiesDir: 'src/entity',
    migrationsDir: 'src/migrations',
    subscribersDir: 'src/subscriber',
  },
};

마이그레이션 생성

마이그레이션은 cli을 이용해서 생성할 수 있다 node module에서 typeorm을 typescript를 이용해서 실행해야 하기 때문에 아래와 같이 명령어를 사용해야하는 문제가 있었다 마이그레이션의 이름을 특정한 엔티티의 이름을 가져가지 않는 이상 모든 엔티티에 선언되어 있는 테이블을 가지고 마이그레이션을 생성해준다 이 점이 매우 편리했었다

"./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:generate -n 마이그레이션 이름"

마이그레이션 파일

import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';
import { TrackSeed } from '../seed/track.seed';
import { UserSeed } from '../seed/user.seed';
import { UserTrackSeed } from '../seed/usertrack.seed';

export class allToTalbe1592371462962 implements MigrationInterface {
    name = 'allToTalbe1592371462962'

    public async up(queryRunner: QueryRunner): Promise<void> {
      await queryRunner.query('CREATE TABLE `user_track` (`id` int NOT NULL AUTO_INCREMENT, `bookmark` tinyint NOT NULL DEFAULT 0, `createdAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updatedAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `userId` int NULL, `trackId` int NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB');
      await queryRunner.query('CREATE TABLE `track` (`id` int NOT NULL AUTO_INCREMENT, `trackTitle` varchar(255) NULL, `origin` varchar(255) NULL, `destination` varchar(255) NULL, `route` longtext NULL, `trackLength` double NOT NULL DEFAULT 0, `createdAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updatedAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB');
      await queryRunner.query('CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `email` varchar(255) NOT NULL, `password` varchar(255) NULL DEFAULT NULL, `username` varchar(255) NULL DEFAULT NULL, `loginCount` int NOT NULL DEFAULT 1, `createdAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updatedAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB');
      await queryRunner.query('ALTER TABLE `user_track` ADD CONSTRAINT `FK_0ef9f7bea0ab00b8bb87935dc2a` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION');
      await queryRunner.query('ALTER TABLE `user_track` ADD CONSTRAINT `FK_acdd9f9001b16ff1b37d52c0ba6` FOREIGN KEY (`trackId`) REFERENCES `track`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION');
      await getRepository('track').save(TrackSeed.makeFakeData);
      await getRepository('user').save(UserSeed);
      await getRepository('user_track').save(UserTrackSeed);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
      await queryRunner.query('ALTER TABLE `user_track` DROP FOREIGN KEY `FK_acdd9f9001b16ff1b37d52c0ba6`');
      await queryRunner.query('ALTER TABLE `user_track` DROP FOREIGN KEY `FK_0ef9f7bea0ab00b8bb87935dc2a`');
      await queryRunner.query('DROP TABLE `user`');
      await queryRunner.query('DROP TABLE `track`');
      await queryRunner.query('DROP TABLE `user_track`');
    }
}

Sequelizer 와 비슷하게 migration run 하게 되면 up 메소드가 실행되고 migration revert 하게 되면 down 메소드가 실행된다.

Migration run & revert

"./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:run"
"./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:revert"

위 두 명령어를 이용해서 run과 revert를 할 수 있다

seed

fake 데이터를 생성하기 위해서 여러가지 방법이 있었다 typeorm-seeding이라는 모듈도 사용해 봤지만 해당 모듈을 사용하면 마이그레이션 테이블의 내용을 싹 지워버려서 ... 다른 방식을 찾아봤다 해결방법은 마이그레이션 up 메소드 내부에서 seed데이터를 불러와서 테이블에 삽이하는 방식이었다

아래와 같이 임의로 seed파일에서 데이터를 정의하고 불러와서 getRepository의 save 메소드를 이용해서 저장하는 방식이었다 TypeOrm은 아래와 같이 객체 형식의 데이터를 바로 집어 넣는 것이 가능하다

import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';
import { TrackSeed } from '../seed/track.seed';
import { UserSeed } from '../seed/user.seed';
import { UserTrackSeed } from '../seed/usertrack.seed';

export class allToTalbe1592371462962 implements MigrationInterface {
    name = 'allToTalbe1592371462962'

    public async up(queryRunner: QueryRunner): Promise<void> {
      /*
      * 테이블 선언부분 생략
      */ 
      
      // seed데이터 삽입 방식
      await getRepository('track').save(TrackSeed.makeFakeData);
      await getRepository('user').save(UserSeed);
      await getRepository('user_track').save(UserTrackSeed);
    }
}

seed 파일

export const UserSeed = [
  {
    email: '15min@mail.com',
    password: '1234',
    username: 'lee',
    loginCount: 1,
    createdAt: `${new Date()}`,
    updatedAt: `${new Date()}`,
  },
  {
    email: '571min@mail.com',
    password: '1234',
    username: 'fee',
    loginCount: 2,
    createdAt: `${new Date()}`,
    updatedAt: `${new Date()}`,
  },
  {
    email: '71min@mail.com',
    password: '1234',
    username: 'gee',
    loginCount: 3,
    createdAt: `${new Date()}`,
    updatedAt: `${new Date()}`,
  },
  {
    email: '1min@mail.com',
    password: '1234',
    username: 'kee',
    loginCount: 4,
    createdAt: `${new Date()}`,
    updatedAt: `${new Date()}`,
  },
  {
    email: 'test@gmail.com',
    password: '1234',
    username: 'kee',
    loginCount: 4,
    createdAt: `${new Date()}`,
    updatedAt: `${new Date()}`,
  },
];
profile
데이터에 소외된 계층을 위해 일을 하는 개발자를 꿈꾸는 학생입니다

0개의 댓글