Typescript2 서버 튜토리얼 - 3/4
2017년 04월 29일 작성TL;DR
코드는 여기1
시작하며
본 글은 typescript 를 이용하여 mongodb 를 쓰는 것이 목적이므로 일단 mongodb 의 기본은 안다고 가정한다. mongodb 기본 사용법은 공식 홈페이지의 문서로도 충분한 것 같다.
본 글에서는 mongodb 를 이용하여 밀짚모자 해적단 등장인물의 정보를 다룰(CRUD) 수 있는 클래스를 작성해본다.
프로젝트 생성
본 글은 ts-tutorial v12의 프로젝트 구조를 기반으로 진행하겠다.
$ git clone git@github.com:haandol/ts-tutorial.git
$ cd ts-tutorial
$ git checkout v1.0
프로젝트 구조는 다음과 같다.
.
├── LICENSE
├── README.md
├── package.json
├── src
│ └── index.ts
└── tsconfig.json
MongoDB 서버 준비
일단 homebrew 로 mongodb를 설치하고 /data/db 폴더(기본폴더)를 생성해준다.
$ brew install mongodb
$ sudo mkdir -p /data/db
$ mongod
mongo 명령어로 서버에 접속이 되면 정상적으로 실행된 것이다.
$ mongo
typescript, mongoose 설치
이제 typescript 와 mongodb 드라이버인 mongoose 패키지를 설치해보자. @types/패키지명 으로 타입정의를 함께 설치하는 것도 잊지 말자.
$ npm install --save mongoose typescript
$ npm install --save-dev @types/mongoose @types/node
스키마 정의
mongodb 는 아래와 같이 Schema 를 이용해 Model을 생성하고, 이를 이용하여 DB 에 Document 를 추가한다.
var mongoose = require('mongoose');
var schema = new mongoose.Schema({
name: {type: String, required: true},
age: {type: Number, required: true}
});
var User = mongoose.model('User', schema);
var robin = new User({name: 'robin', age: 30});
function printAge(pirate) {
console.log(pirate.age);
}
printAge(robin); // 30
printAge({name: 'robin'}); // this prints 'undefined'
printAge 함수의 경우 User 모델의 age 필드를 참조하여 처리를 하지만 javascript 특성상 아무 객체나 다 던져줘도 처리할 수 있다.
Typescript 를 사용하면, 아래처럼 파라미터가 스키마에 맞는 객체가 아니면 컴파일타임에 에러를 낸다.
// src/index.ts
import * as mongoose from 'mongoose';
interface User extends mongoose.Document {
name: string;
age: number;
}
const schema = new mongoose.Schema({
name: {type: String, required: true},
age: {type: Number, required: true}
});
const UserModel = mongoose.model<User>('User', schema);
let robin = new UserModel({name: 'robin', age: 30});
function printAge(pirate: User) {
console.log(pirate.age);
}
printAge(robin); // 30
printAge({name: 'robin'}); // CompileError: can not convert to User
Typescript 를 쓰면서 추가된 사항은 아래와 같다.
- mongoose.Document 를 상속받은 Pirate 인터페이스를 정의하고
- 모델 생성시 Pirate 인터페이스를 제너릭 파라미터로 전달해준다.
IPirate 정의하기
당연하게도 타입체크를 하기 위해서 먼저 인터페이스나 클래스를 만들어야 한다.
해적 정보를 저장하기 위해 src/domain/pirate.ts 를 만들고 Pirate 인터페이스를 추가하자
참고로 많은 인터넷 예제들이 IPirate
이런 식으로 I
를 앞에 붙여서 인터페이스를 명명하는데
공식 스타일가이드는 인터페이스 앞에 I
를 붙이지 않도록 권장한다.
$ mkdir -p src/domain
$ touch pirate.ts
// src/domain/pirate.ts
import * as mongoose from 'mongoose';
export interface Pirate extends mongoose.Document {
name: string;
bounty: number;
isEsper: boolean;
};
const pirateSchema = new mongoose.Schema({
name: {type: String, required: true},
bounty: {type: Number, required: true},
isEsper: {type: Boolean, required: true}
});
export const PirateModel = mongoose.model<Pirate>('Pirate', pirateSchema);
DB 클래스 만들기
mongodb 의 CRUD 기능을 쉽게 쓸 수 있도록 wrapper 클래스를 하나 만들자.
먼저 src/domain/db.ts 를 만들고 아래 내용을 추가하자.
// src/domain/db.ts
import * as mongoose from 'mongoose';
import { Pirate, PirateModel } from './pirate';
export class DB {
constructor() { }
}
이제 DB 클래스에 기능을 하나씩 추가해보자.
데이터 추가하기(Create)
// src/domain/db.ts
import * as mongoose from 'mongoose';
import { Pirate, PirateModel } from './pirate';
export class DB {
constructor() { }
create(pirate: Pirate): Promise<Pirate> {
let p = new PirateModel(pirate);
return p.save();
}
}
PirateModel 은 mongoose.Model 을 상속받아 만들어지며, Model.save() 는 해당 모델을 이용해 mongodb 에 Document 를 생성한다.
이 때 반환된 값은 Promise<T>
의 제너릭 형태인데,
<T>
제너릭은 런타임에 타입을 지정할 수 있게 해주는 기법으로
처음 PirateModel 생성시 Pirate 로 지정해줬었다.
Promise 를 쓰지 않고 콜백(callback) 방식으로 호출 할 수도 있는데 아래와 같이 해주면 된다.
let p = new PirateModel(pirate);
p.save((err, raw) => {
console.log('Document is created successfully');
});
콜백 방식은 여러 비동기 요청을 다룰 때 복잡한 코드를 만들게 되므로 가급적 Promise 와 친해지는 것이 좋다. 최근에는 generator 나 async/awaits 를 이용한 코루틴 기법도 많이 사용되고 있으니 참고하기 바란다.
본 글에서는 CRUD 비동기 요청을 위해 Promise 방법을 사용하기로 한다.
데이터 가져오기(Read)
생성한 데이터를 읽어 들이는 read 함수를 추가해보자
// src/domain/db.ts
...
export class DB {
constructor() { }
read(query: any): mongoose.DocumentQuery<Pirate[], Pirate> {
return PirateModel.find(query);
}
create(pirate: Pirate): Promise<Pirate> { ... }
}
Model.find 함수는 mongoose.DocumentQuery<Pirate[], Pirate>
를 반환하며
mongoose.DocumentQuery 인터페이스는 Promise 타입을 상속받는다.
따라서 실제로 반환하는 것은 Promise<Pirate[], Pirate>
형태라고 생각하면 편하다.
마찬가지로 read 함수도 아래와 같이 콜백 방식으로 구현할 수도 있다.
// src/domain/db.ts
...
export class DB {
constructor() { }
read(query: any): void {
PirateModel.find(query, (err, pirates) => {
console.log(pirates.length);
});
}
create(pirate: Pirate): Promise<Pirate> { ... }
}
데이터 수정하기(Update)
계속해서 데이터를 수정하는 update 함수를 추가해보자
// src/domain/db.ts
...
export class DB {
constructor() { }
update(pirate: Pirate): mongoose.Query<number> {
return PirateModel.update({name: pirate.name}, {...pirate});
}
read(query: any): mongoose.DocumentQuery<Pirate[], Pirate> { ... }
create(pirate: Pirate): Promise<Pirate> { ... }
}
Model.update 함수는 mongoose.Query<number>
를 반환하며
Query 인터페이스는 DocumentQuery 를 상속받으므로, 역시 Promise 타입을 상속받는다.
따라서 실제로 반환하는 것은 Promise<number>
형태라고 생각하면 된다.
데이터 삭제하기(Delete)
마지막으로 데이터를 수정하는 delete 함수를 추가해보자
// src/domain/db.ts
...
export class DB {
constructor() { }
delete(pirate: Pirate): mongoose.Query<void> {
return PirateModel.remove({name: pirate.name});
}
update(pirate: Pirate): mongoose.Query<number> { ... }
read(query: any): mongoose.DocumentQuery<Pirate[], Pirate> { ... }
create(pirate: Pirate): Promise<Pirate> { ... }
}
Model.delete 함수의 반환값은 update 와 같은데 대신 삭제한 개수를 반환하지 않는다.
따라서 실제로 반환하는 것은 Promise<void>
형태라고 생각하면 편하다.
테스트
마지막으로 우리가 만든 DB 클래스를 이용하여 Document 를 다뤄(CRUD) 보자.
// src/index.ts
import * as mongoose from 'mongoose';
import { Pirate, PirateModel } from './domain/pirate';
import { DB } from './domain/db';
let uri = 'mongodb://localhost/onepiece';
const connection: mongoose.MongooseThenable = mongoose.connect(uri);
const db = new DB();
let luffy = <Pirate>{name: 'luffy', bounty: 0.3, isEsper: true};
connection.then(() => {
return db.create(luffy)
})
먼저 mongodb 에 mongoose.connect 를 이용하여 연결하고
반환되는 Promise Thenable 은 then 을 포함하는 인터페이스 를 connection 변수에 저장해둔다.
그리고 luffy 객체를 만들고 Pirate 로 타입캐스팅 한다.
해당 객체를 mongodb 에 저장하기 위해 db.create() 에 파라미터로 전달하고 Promise 를 반환받아서 리턴한다. create가 Promise 를 리턴했기 때문에 해당 Promise 를 이용하여 작업을 이어나갈 수 있다.
그럼 데이터가 잘 저장되었는지 확인하기 위해 mongoDB 에서 name 이 luffy 인 Document 를 가져와보자.
connection
.then(() => {
return db.create(luffy)
})
.then((raw) => {
db.read({name: raw.name}).then((pirates) => {
console.log('Created');
console.log(pirates);
});
})
db.create 는 Model.save 를 바로 리턴하고 Model.save 는 Promise<Pirate>
을 리턴한다.
이때 Pirate 은 저장된 Document 를 반환하므로
다음 then 의 콜백에서 raw 를 이용해 해당 Document 를 사용할 수 있다.
db.read 의 경우 Promise 를 리턴하지만 출력만 하면 되기 때문에 read 의 Promise 는 따로 리턴하지 않는다.
Promise 콜백(resolve) 는 리턴하지 않으면 Promise<void>
를 자동으로 리턴한다.
계속해서 db.update 함수를 이용해 luffy 의 현상금을 3천만에서 5억으로 올려보자.
.then(() => {
luffy.bounty = 5;
return db.update(luffy);
})
.then((n) => {
db.read({name: 'luffy'}).then((pirates) => {
console.log('Updated');
console.log(pirates);
});
})
db.update 도 Promise 를 리턴하는데 반환값은 Promise
마지막으로 db.delete 함수를 이용해 luffy 를 현상수배명단에서 삭제하자.
.then(() => {
return db.delete(luffy);
})
.then(() => {
db.read({name: 'luffy'}).then((pirates) => {
console.log('Deleted');
console.log(pirates);
});
})
db.delete 도 Promise 를 반환하는데 Promise
모든 작업을 마쳤으면 mongodb 커넥션을 닫아주자.
.then(() => {
mongoose.connection.close();
});
실행결과
실행은 여느때와 마찬가지로 npm start
해주면 되겠다.
$ npm start
> ts-tutorial@1.0.0 start /Users/haandol/git/ts-tutorial
> tsc; node ./build/index.js
Created
[ { _id: 5905f3efb1b5611171a5bccd,
name: 'luffy',
bounty: 0.3,
isEsper: true,
__v: 0 } ]
Updated
[ { _id: 5905f3efb1b5611171a5bccd,
name: 'luffy',
bounty: 5,
isEsper: true,
__v: 0 } ]
Deleted
[]
참고로 mongoose.mpromise 관련 warning 이 나오는데
그냥 무시해도 상관없음typescript 에서는 Promise 를 global.Promise 로 대체할 수가 없어서 해결을 못했다.
마치며
넣고 싶은 내용은 많았지만 다 빼고 핵심적인 부분만 담으려고 했다. 이상한 내용은 댓글이나 티켓을 보내주시면 빠르게 수정하겠다.
다음은 typescript + socket.io 를 이용하여 아주 간단한 채팅서비스를 만들어보자.