타입스크립트로 몽구스 구성하기

00_8_3·2020년 12월 29일
0
post-thumbnail

타입스크립트란 무엇인가?

  • 타입스크립트는 MS에서 개발한 언어로 자바스크립트 위로 개발된 오픈소스 언어이며 정적 타입 정의를 추가한 세계에서 가장 많이 사용된 툴중 하나입니다.

  • 타입은 더 좋은 문서를 제공하거나 당신의 코드가 맞게 작동하는지 판단하게 함으로 써 객체의 모양을 설명하는 방법을 제공합니다.

  • 타입스크립트는 TSC(타입스크립트 컴파일러)의 도움으로 객체지향프로그래밍을 따릅니다.

왜 타입스크립트?

  • 타입스크립트는 자바스크립트 코드를 간편화하여 읽기 쉽게 하고 디버깅을 할 수 있습니다.
  • 기존 자바스크립트 IDE를 통해 개발을 할 수 있습니다.
  • ES6의 장점을 얻을 수 있습니다.
  • 정적 타이핑을 지원합니다.
  • 간지.

주요 타입들

interface

TypeScript의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞추고 있다는 것입니다. 이를 "덕 타이핑(duck typing)" 혹은 "구조적 서브타이핑 (structural subtyping)"이라고도 합니다. TypeScript에서, 인터페이스는 이런 타입들의 이름을 짓는 역할을 하고 코드 안의 계약을 정의하는 것뿐만 아니라 프로젝트 외부에서 사용하는 코드의 계약을 정의하는 강력한 방법입니다.

interface Test1 {
	arr?: string;
	brr: number;
}

function Test(test: Test1) {
	console.log(test.arr);
}

const obj = {str: "kkkk", rts: 1}
Test(obj)
>>> "kkkk"

union type

유니언 타입은 여러 타입 중 하나가 될 수 있는 값을 의미합니다. 세로 막대 ( | )로 각 타입을 구분하여, number | string | boolean은 값의 타입이 number, string 혹은 boolean이 될 수 있음을 의미합니다.

쉽게 OR이라고 생각하면 됩니다.

//test는 number거나 string 타입니다.
type my_test = number | string

function Test(input : my_test) {
	console.log(typeof input)
}
Test(123) // 성공
Test("arr") // 성공
Test(undefined) // undefined은 number string 타입이 아니라 에러

typeof

자바스크립트는 이미 typeof를 갖고 있습니다.

let s = "hello";
let n: typeof s;
//  ^ = let n: string

type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
//   ^ = type K = boolean

keyof

타입스크립트 2.9에서는 index타입과 mapped 타입에서 number과 symbol 프로퍼티를 지원을 시작했습니다. 이전 버전에서는 해당 operator와 mapped 타입은 string에서만 지원 되었습니다.

타입 값의 모든 프로퍼티의 값을 Union 형태로 리턴

interface Todo {
    id: number;
    text: string;
    due: Date;
}

// TodoKeys의 타입 = "id" | "text" | "due"
type TodoKeys = keyof Todo;

Record<K, T>

K 타입을 키값으로, T타입을 밸류값으로 갖는 "타입"

interface Car {
    name: string,
    price: number
}

const productList: Record<"SONATA" | "AVANTE", Car> = {
    SONATA: {name: "SONATA", price: 10000},
    AVANTE: {name: "SONATA", price: 10000}
}

const nextProductList: Record<string, Car> = {
    SONATA: {name: "SONATA", price: 10000},
    AVANTE1: {name: "SONATA", price: 10000},
    AVANTE2: {name: "SONATA", price: 10000},
    AVANTE3: {name: "SONATA", price: 10000},
}

const assertions

타입스크립트 3.4 이후로 리터럴 값을 위한 새로운 구성이 도입 되었습니다.
문법은 유형 이름 대신에 const가 있는 Type 단언 (aswwertion) 입니다.

  • 해당 표현식의 리터럴 타입은 확장 되지 않아야 합니다.
  • 객체리터럴은 readonly 프로퍼티를 갖습니다.
  • 배열 리터럴은 readonly 튜플을 갖습니다.
let str = "test1" as const

const arr = ["test1", "test2"] as const

const obj = {name: "test1"} as const

jsx 파일에서는 다음과 같이 사용도 가능합니다.

let str = <const>"test1"

const arr = <const>["test1", "test2"]

const obj = <const>{name: "test1"}

👉🏻 주의 사항

간단한 리터럴 표현식에만 적용 가능합니다.
enum members, string, number, boolean, array, object literal

✔ 응용 assertions와 typeof로 Union type 만들기

let obj = ["asdf", "asdfff","adfsdf"] as const;
type arr = typeof obj[number]; 

//obj[0]은 "asdf", obj[number]란 obj 모든 인덱스에 접근?한다는거 같음,
//map과 비슷한거 같음

>>> type arr = "asdf" | "asdfff" | "adfsdf"

Partial< T>

타입 T의 모든 프로퍼티를 Optional로 바꾸어 준다.

예1

interface User {
    name: string;
    age: number;
}

let user1: User = {name: 'harry', age: 23} //OK
let user2: User = {age: 23} // 에러발생

let user2: Partial<User> = {age: 23} // OK

예2

interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    // todo, fieldToUpdate 두개 모두 값이 있을경우 오른쪽 값이 할당됩니다.
    return {...todo, ...fieldsToUpdate}
}

const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
}

const todo2 = updateTodo(todo1, {
    description: 'throw out trash',
})

declare와 namespace

타입스크립트에는 내부 모듈과 외부모듈이 있다.
이 중 내부모듈을 namespace라고 부른다.

declare 키워드는 타입스크립트 컴파일러에게 특정한 변수가 있다고 선언하는 키워드로 전역변수를 사용하거나 .d.ts 파일을 만들때 사용한다.

타입스크립트로 전환하면서 많은 파라미터 값에 타입을 지정해주어야 했는데 그 과정에서 오류가 발생한 적이 있다.

import {Request, Response, NextFunction} from "Express"
(req:Request, res:Response) =>{
user = req.user
}

위와 같이 Express의 타입을 불러온다 해도 req.user의 user의 프로퍼티가 정의가 안되어 있기 때문에 오류가 발생한다.
이럴 경우 .d.ts파일을 만들어 커스텀 interface를 만들어주면 해결된다.

  • index.d.ts
declare namespace Express {
    export interface Request {
        user?: User
    }
}

.d.ts 파일을 선언해주면 controller 파일에서 따로 import를 안해도 custom interface를 사용 할 수 있다.

스택오버플로우-extend 라이브러리


타입스크립트 공식문서
유틸리티-클래스-파헤치기


MongoDB ?? Mongoose??

MongoDBNoSQL를 대표하는 DB로 RDBMS의 한계를 극복하기 위해 만들어 졌습니다.

Mongoose는 MongoDB 및 Node.js를위한 ODM(Object Data Modeling) 라이브러리입니다. 데이터 간의 관계를 관리하고 스키마 유효성 검사를 제공하며 코드의 객체와 MongoDB의 객체 표현 사이를 변환하는 데 사용됩니다.

Mongoose Schema의 methods와 static

methods는 데이터 인스턴스(테이블 안 특정한 것)를 가르키고
static은 모델 자체(테이블 전체)를 가르킨다.

타입스크립트를 이용한 methods && static

  • before
const mongoose = require('mongoose');
const Memo = require('./Memos');

const UserSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    trim: true,
  },
  googleId: {
    type: String,
    unique: true,
  },
  naverId: {
    type: String,
    unique: true,
  },
  name: {
    type: String,
    trim: true,
    required: true,
  },
  date: {
    type: Number,
    default: Date.now,
  },
  tokens: [
    {
      token: {
        type: String,
      },
    },
  ],


});

UserSchema.virtual('memos', {
  ref: 'Memo',
  localField: '_id',
  foreignField: 'userId',
});

UserSchema.methods.generateToken = async function (secret_key, expiresTime) {
  const user = this;
  const token = jwt.sign({ _id: this._id.toHexString() }, secret_key, {
    expiresIn: expiresTime,
  });

  user.tokens = user.tokens.concat({ token: token });

  await user.save();
  return token;
};

UserSchema.statics.findByToken = function (token, secret_key) {
  const user = this;
  return jwt.verify(token, secret_key, function (err, decoded) {
    return user
      .findOne({ _id: decoded, 'tokens.token': token })
      .then(user => user)
      .catch(err => err);
  });
};

module.exports = mongoose.model('User', UserSchema);
  • after
import {Schema, Document, model, Model, Types} from "mongoose";
import jwt from "jsonwebtoken";

interface IUser {
    email: string;
    name: string;
    googleId?: string;
    naverId?: string;
    kakaoId?: string;
    tokens?: [string];

}

interface IUserDoc extends IUser, Document{ // methods
    generateToken : (secret_key:string, expiresTime:string)=> Promise<string>
}
interface IUserModel extends Model<IUserDoc>{ // statics
    findByToken : (token:string, secret_key:any) => Promise<void>
}
const userSchema :Schema = new Schema({
    email:{
        type: String,
        required: true,
        unique: true,
        lowercase: true,
        trim: true
    },
    name:{
        type: String,
        required: true
    },
    date:{
        type: Number,
        default: Date.now()
    },
    googleId: {
        type: String,
    },
    naverId: {
        type: String,
    },
    kakaoId: {
        type: String,
    },

    tokens:[
        {
            token:{
                type:String,
            }
        }
    ]
});

userSchema.virtual('memos', {
    ref: 'Memo',
    localField: '_id',
    foreignField: 'userId',
  });


userSchema.methods.generateToken = async function (secret_key:string, expiresTime:string) : Promise<string>{
    const user = this;
    const token = jwt.sign({ _id: this._id.toHexString() }, secret_key, {
      expiresIn: expiresTime,
    });
  
    user.tokens = user.tokens.concat({ token: token });
  
    await user.save();
    return token;
  };
  
  userSchema.statics.findByToken = function (token:string, secret_key:any) :void {
    const user = this;
    return jwt.verify(token, secret_key, function (err:any, decoded :any) {
      return user
        .findOne({ _id: decoded, 'tokens.token': token })
        .then((user:IUser) => user)
        .catch((err:any) => err);
    });
  };

const User = model<IUserDoc, IUserModel>("User", userSchema);
export {User, IUser};

populate와 virtual

아직 타입스크립트에서는 populate를 지원하지 않습니다.

populate

몽고DB를 사용하다보면 하나의 다큐먼트가 다른 다큐먼트의 ObjectId를 쓰는 경우가 있습니다. 그럴 때 그 ObjectId를 실제 객체로 치환하는 작업이 필요합니다.

👉🏻 주의 사항

하지만 populate를 맹신해서는 안 됩니다. populate는 $oid로 모두 조회를 해서 자바스크립트 단에서 합쳐주는 것이지 JOIN처럼 DB 자체에서 합쳐주는 것이 아닙니다.

export const getUsers =async (req: Request, res:Response, next:NextFunction)=>{
    
    console.log("connect users router")
    try {
        const user = await User.find().populate("memos");
        return res.json(user);
    } catch (err) {
        return res.json({message:err})
    }
}

(populating) virtual

다큐먼트에는 없지만 객체에는 있는 가상의 필드를 만들어줍니다.(DB 저장 X)

일반 virtual와 다르게 다른 collections을 참조(ref)하여 해당 collections에 나타나게 해준다.

userSchema.virtual('memos', {
    ref: 'Memo',
    localField: '_id',
    foreignField: 'userId',
  });
  
userSchema.set('toObject', { virtuals: true });
userSchema.set('toJSON', { virtuals: true });

JSON상에서 보고 싶다면 toJSON 또는 toObject virtauls: true로 set 해주어야한다.

  • JSON

몽고DB 내 users 테이블(collection)안에는 37번째 줄의 memos가 저장되지 않지만 populate와 virtual을 이용해 JSON상에서 확인 할수 있다.

마침

TS의 위세가 날이 갈 수록 높아지면서 "나도 언젠가는 한번 써봐야지" 하는 마음으로 있었는데 마침내 행동으로 옮기게 되었다.

아직 모두 안 것은 아니지만 계속 블로깅을 해보면서 TS 공부를 해보겠다.

TS && React 동향

몽구스 toJSON and toObject
몽구스 공식문서

10개의 댓글

comment-user-thumbnail
2020년 12월 29일

populate 자주 쓰는거같아요? 전 거의 안쓰게 되던데말이죠?

2개의 답글
comment-user-thumbnail
2021년 1월 3일

타입스크립트 공부를 언젠간 해봐야겠다 생각했는데
간략하게 정리된 특징을 보니까 좋네요!

혹시 인터페이스 정의할 때 ?가 붙은 건 어떤 의미인가요?
interface Test1 {
arr?: string;
brr: number;
}

1개의 답글
comment-user-thumbnail
2021년 1월 3일

타입을 만들때 type이랑 interface랑 어떤 차이가 있나요???

1개의 답글
comment-user-thumbnail
2021년 1월 3일

예제에서 googleId, naverId, kakaoId를 각 필드로 구성하신 이유가 무엇인가요? Mongo의 비정형 특성을 활용해서 social_Id와 같이 정의하는 것에 비해 장점이 있는지 궁금합니다

1개의 답글