0%

TypeScript Compiler

Compliation Context

논리적인 그룹핑과 어떤 방법으로 컴파일 할것인지에 대한 맥락.
보통 tsconfig.json 파일로 관리한다.

tsconfig schema

최상위 프로퍼티 중 주요한 항목

  • compileOnSave
  • extends
  • compileOptions
  • files
  • include
  • exclude
  • references

compileOnSave

  • true/false(default fasle): true 로 설정하면 저장하면 컴파일 해줌.
  • 누가?
    • Visual Studio 2015 with TypeScript 1.8.4 이상
    • tom-thypescript 플러그인

extends

상속 받을 때 사용함.

  • 파일 (상대) 경로명 : string
1
2
3
4
5
{
"compilerOptins": {
"strict": true
}
}
1
2
3
{
"extends": "./base.json"
}

tsconfig/base 저장소로 이동하면 다양한 base 설정을 받을 수 있다.

npm install --save-dev @tsconfig/deno

1
2
3
{
"extends": "@tsconfig/deno/tsconfig.json"
}

files, include, exclude

어떤 파일을 컴파일 할것인지 결정함.

files 가 가장 강한 설정이다. 따라서 exlucde를 통해서 제외 했더라도 files 안에 있으면 컴파일 된다.

셋다 설정이 없으면 전부 컴파일 하려고 한다.

  • files
    • 상대 혹은 절대 경로의 리스트 배열이다.
    • exclude 보다 쎄다.
  • include, exclude
    • glob 패턴 (마치 .gitignore)
    • include
      • exclude 보다 약하다.
    • exclude
      • 설정 안하면 4가지 (node_modules, bower_components, jspm_packages, <outDir>) 를 dafault로 제외 함.
      • <outDir> 은 항상 제외한다. (include에 있어도)

compileOptions - typeRoots, types

만약 외부 라이브러리를 사용할 경우 타입 검사를 어떻게 수행할까?

예를 들어 react를 설치하면 js로 만든 것이기 때문에 오류가 발생한다. 따라서 npm i --save-dev @types/react를 하게 되면 node_modules/@types안에서 파일을 찾아서 오류가 나지 않게 된다.
이것이 default 설정인데 typeRoots와 types 를 통해 설정을 변경할 수 있다.

typeRoots로 @types로 사용할 폴더를 설정한다. 유명하지 않는 라이브러리 같은 경우 @types가 없을 수 있고 내가 작성한 패키지는 내가 따로 만들어야 하는 경우도 있다.

@types

  • 내장 type definition 시스템이다.
  • 아무 설정을 하지 않으면 node_modules/@types 라는 모든 경로를 찾아서 사용한다.
  • typeRoots를 사용하면? -> 배열 안에 들어있는 경로들 아래서만 가져온다.
  • types 를 사용하면 -> 배열안에 모듈 혹은 ./node_module/@types/ 안에 모듈 이름에서 찾아온다. [] 빈 배열을 넣는다는 건 이 시스템을 이용하지 않겠다는 것이다.
  • typeRoots와 types를 같이 사용하지는 않는다.

compileOptions - target과 lib

target - 자바스크립트 버전을 설정한다. 기본값은 es3이다. 컴파일 할때 해당 버전으로 변경한다.

lib - lib로 사용할것들을 배열로 넘겨줌. target에 따라 default로 저장되는 lib가 있다.

compileOptions -outDir, outFil, rootDir

  • rootDir : 컴파일 대상이 되는 파일이 있는 폴더

따로 설정하지 않으면 ts 파일이 있는 가장 최상위 폴더를 rootDir로 설정한다.

  • outDir : 컴파일 한 파일이 저장될 폴더
  • outFile: 지정된 경우 모든 전역 파일이 지정된 단일 출력 파일로 연결된다.

compileOptions - strict

무조건 strict를 true로 설정하는 것이 기본이다.

--noImplicitAny any로 추정되는데 any로 명시하지 않았다면 에러가 발생함.
suppressImplicitAnyIndexErrors 인덱스 객체도 any로 추론될 경우 에러를 발생시키는 옵션인데 이것은 너무 까다로우니 따로 에러가 되지 않게 하는 설정도 있음.

--noImplictThis this 가 any 타입으로 추정될 경우 . this에 타입을 명시해야 한다고 에러를 발생 시킴, 함수 매개변수에 첫번째에 this를 넣고 타입일 지정해줌. js에서는 오류 이지만 ts에서는 가능함.
마치 call /applyu/ bind 와 같이 this를 대체하여 함수 콜을 사용하는 용도. class에서는 오류가 나지 않음 왜냐하면 class 자체로 타입 체크가 가능하기 때문에

--strictNullChecks 이것을 적용히지 않으면 모든 타입은 null 과 undefined가 될 수 있다. 꼭 설정해주어야 함. 한가지 예외는 undefined 에 void 할당이 가능하다.

--strictFunctionTypes 반환 타입은 공변적, 인자 타입은 반 공병적. 그런데 타입스크립트에서 인자 타입은 공변적이면서, 반공변적인게 문제. 함수의 파라미터 타입이 반공변적으로 동작핟조록 변경한다. 반공변 이란 공변에 반대라고 생각하면 된다.

1
2
3
4
5
6
7
8
9
type Logger<T> = (param: T) => void;
let log: Logger<string | number> = (param) => {
console.log(param);
};
let logNumber: Logger<number> = (param) => {
console.log(param);
};
log = logNumber; // Error
logNumber = log; // OK

공변이라고 생각하면 logNumber 가 서브 타입이기대문에 log에 할당하는 것이 가능할 것이다. 하지만 --strictFunctionTypes를 적용하면 파라미터에 대해서 반공변적으로 작동하기 때문에 반대로 작동한다.
사실 logNumber는 숫자 밖에 처리하지 못하기 때문에 string 도 처리할 수 있는 log에 할당하는게 맞지 않다.

--stcitPropertyInitialization 정의되지 않은 클래스의 속성이 생성자에서 초기화되었는지 확인한다. 만약 비동기로 생성되는 클래스일 경우에 생성자에 async를 사용하는것이 불가능하기 때문에 에러가 발생한다.
이럴때에는 async 함수로 initialize 같은 함수를 만들어 할당해 주고 클래스 내부 변수로 이름 뒤에 !를 붙여주면 된다. private _name!: string;

--strictBindCallApply bind, call, apply 에 대한 검사를 엄격히 수행한다.

--alwaysStrict strict 모드로 코드를 분석함.

type system

작성자와 사용자의 관점으로 코드 바라보기

타입 시스템

컴파일러가 자동을 타입을 추론하는 시스템이 있고 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템이 있다.

타입 스크립트의 타입 시스템

두가지 모두 가능하다.

타입이란 해당 변수가 할 수 있는 일을 결정한다.

1
2
3
function f1(a) {
return a;
}

a 가 할 수 있는 일은 a 의 타입이 결정한다.

함수 사용법에 대한 오해를 야기한다.

1
2
3
4
5
6
function f2(a) {
return a * 38;
}

console.log(f2(10)) // 380
console.log(f2('Mark')) // NaN

사용자가 직접 f2를 까보지 않는 이상 함수 인수로 뭘 넣어야 하는지 모를 수 있다.

이는 타입 스크립트의 추론에만 의지하는 경우도 마찬가지이다.

noImplicitAny 옵션

타입을 명시적으로 작성하지 않아 any 로 추론되면 에러를 발생 시켜서 타입을 명시적으로 작성하도록 함.

nuber 타입으로 추론된 리턴 타입

1
2
3
4
5
6
7
8
function ft(a: number) {
if (a > 0) {
return a * 38;
}
}

console.log(f4(5)); // 190
console.log(f4(-5) + 5); // NaN

a에 음수를 넣었기 때문에 undefined 가 리턴되는데 typescript 에서는 undefined도 number안에 속한 타입으로 추론한다.

strictNullChecks 옵션을 켜면

모든 타입에 자동으로 포함되어 있는 null 과 undefined를 제거 한다.
무조건 켜야 함.

옵션을 켜게 되면 f4의 결과는 더이상 number 가 아니라 number | undefined 가 된다. 따라서 f4의 결과와 number를 더하기 할때에 오류가 발생한다.

그런데 5를 넣은 경우에도 이는 마찬가지이다. 런타임에서 두가지 모두 가능하기 때문에 typescript 에서는 undefined 일때는 에러를 던진다던지 하는 처리를 하는것을 권장한다.

리턴을 명시적으로 작성

1
2
3
4
5
function f4(a: number): number {
if (a> 0) {
return a* 38;
}
}

이렇게 해도 오류가 발생 else인 겨웅에 리턴은 undefined 이기 때문이다.

noImplicitReturns

옵션을 켜면 함수 내에서 모든 코드가 값을 리턴하지 않으면 컴파일 에러를 발생 시킨다.

1
2
3
4
5
function f4(a: number) {
if (a> 0) {
return a* 38;
}
}

에러 발생 return이 code path 존재.

매개 변수가 object 가 들어오면 object literal

1
2
3
function f7(a: {name: string, age: number}): string {
return `${a.name}이고 나이는 ${a.age}`;
}

나만의 타입을 만드는 방법

1
2
3
4
5
6
7
8
9
interface PersonInterface {
name: string;
age: number;
}

type PersonTypeAlias = {
name: string;
age: number;
};

Structural Type System vs Nominal Type System

nominal type system 은 이름이 다르면 다른 타입으로 여긴다. 반면에
타입 스트립트는 structural type system 이다.
따라서, 구조가 같으면 같은 타입으로 여긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IPerson {
name: string;
age: number;
speak(): string;
}

type PersonType = {
name: string;
age: number;
speak(): string;
};

let personInterface: Iperson = {} as any;
let personType: PersonType = {} as any;

personInterface = personType;
personType = personInterface;

type 호환성 (Type Compatibility)

서브타임과 슈퍼타입

슈퍼 타입 안에 서브타입이 있기 때문에 서브타입은 슈퍼 타입으로 넣을 수 있다. 하지만 서브타입 변수에 슈퍼타입에 변수를 할당하려고 하면 할수 없다.

array 는 object 의 서브 타입이다. 따라서 object 타입에 변수에 array 타입 변수를 할당하는것은 가능하지만 서브타입인 array 변수에 object 타입을 할당하는 것은 불가능하다.

number 배열과 number로 이루어진 튜플이 있다고 할때 number 배열은 number 튜플에 슈퍼 타입이 되고 튜플은 서브 타입이 된다.
따라서 number 배열에 튜플 타입을 할당할수 있지만 반대로 튜플 타입에 number 배열 을 할당할 수 는 없다.

any는 모든 타입의 슈퍼 타입이다. 하지만 이친구는 이상하게도 서브 타입에 할당할 수 도 있다.

never 타입은 number 타입의 서브 타입이다. number 타입에 never 타입에 변수를 할당하는 것은 가능하지만 never 타입의 변수에 number 타입에 변수를 할당하는 것은 불가능하다.

상속관계에서 상속 받는 관계에 있는 클래스는 서브 타입이 된다. 상속해 주는 클래스에 상속 받는 클래스를 할당할 수는 있지만 상속 해주는 클래스를 상속 받는 클래스로 할당하려면 오류가 발생한다. 사실 확장한 메서드가 없기 때문에 구조도 다르다.

공변

같거나 서브 타입인 경우, 할당이 가능하다.

반병

함수의 매개변수 타입만 같거나 슈퍼타입인 경우, 할당이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person{}
class Developer extends Person {
coding() {}
}
class StartupDevloper extends Developer {
burning() {}
}

function tellme(f: (d: Developer) => Developer) {}

tellme(function dToD(d: Developer): Developer {
return new Developer();
})

tellme(function pToD(d: Person): Developer {
return new Developer();
})

tellme(function sToD(d: StartupDeveloper): Developer {
return new Developer();
})

strictFunctionTypes 옵션을 켜면 함수를 할당할 시에 함수의 매개변수 타입이 같거나 슈퍼타입인 경우가 아닌 경우, 에러를 통해 경고한다.

이 옵션을 키면 마지막 줄에 tellme 는 에러일 것이다.

타입 별칭 (Type Alias)

만들어진 타입의 refer로 사용함. 타입을 만드는것은 아니다.

Aliasing Primitive

1
2
3
4
type myStringType = string;
const str = 'world';
let myStr: MyStringType = 'hello';
myStr = str;

Aliasing Union Type

1
2
3
4
5
6
7
let person: string | number = 0;
person = 'Mark';

type StringOrNumber = string | number;

let another: StringOrNumber = 0;
another = 'Anna';

타이핑 해야 하는 야이 줄어들 수 있다.

Aliasing Tupele

1
type PersonTuple = [string, number];

Aliasing Function

1
type EatType = (food: string) => void;

어떤 타입이 타입으로서의 존재가치와 목적이 명확하면 interface를 사용하고 단지 가르킬 뿐이라면 type를 사용함.

물론 기술적으로도 차이가 있음.

interface

데이터 타입을 인터페이스로 만들기

인터페이스로 타입을 정의할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person1 {
name: string;
age: number;
}

function hello1(person: Person1): void {
console.log(`안녕하세요! ${person.name} 입니다.`);
}

const p1: Person1 = {
name: "Mark",
age: 27,
};

hello1(p1);

optional Property

값으로 받을 수도 있고 아닐 수도 있는 속성을 정의할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
interface Person2 {
name: string;
age?: number;
}

function hello2(person: Person2): void {
console.log(`안녕하세요! ${person.name} 입니다.`);
}

hello2({ name: "Koo", age: 39 });
hello2({ name: "Koo" });

age? 에서 ? 를 붙히면 받을수도 있고 안받을 수 도 있는 타입이 된다. age: number | undefined와 같아 진다.

인덱서블 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Person3 {
name: string;
age?: number;
[index: string]: any; // 어떤 이름에 프로퍼티가 와도 괜찮음.
}

function hello3(person: Person3): void {
console.log(`안녕하세요! ${person.name} 입니다.`);
}

const p31: Person3 = {
name: "koo",
age: 37,
};

const p32: Person3 = {
name: "Anna",
systers: ["Sung", "Chan"],
};

const p33: Person3 = {
name: "Bokdaengi",
father: p31,
mother: p32,
};

hello3(p33);

인덱스에 타입을 지정해 주어서 어떤것도 받을 수 있도록 하려면 [index: string] 과 같이 사용한다.

function interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Person4 {
name: string;
age: number;
hello(): void;
}

const p41: Person4 = {
name: "Koo",
age: 27,
hello: function (): void {
console.log(`안녕하세요. ${this.name} 입니다.`);
},
};

const p42: Person4 = {
name: "Koo",
age: 27,
hello(): void {
console.log(`안녕하세요. ${this.name} 입니다.`);
},
};

// const p43: Person4 = {
// name: "Koo",
// age: 27,
// hello: (): void => {
// console.log(`안녕하세요. ${this.name} 입니다.`);
// }
// }

p41.hello();
p42.hello();

함수로 선언하는 방법에는 3가지 정도 있다.

  • hello: function(): void {}
  • hello(): void {}
  • hello: (): void => {} : 화살표 함수를 사용할 경우 this를 사용하지 못한다.

class implements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface IPerson1 {
name: string;
age?: number;
hello(): void;
}

class Person implements IPerson1 {
name: string;
age?: number | undefined;
constructor(name: string) {
this.name = name;
}
hello(): void {
console.log(`안녕하세요 ${this.name} 입니다.`);
}
}

const person: IPerson1 = new Person("Koo");
person.hello();

객체지향 언어에서 구현하듯이 클래스에서 interface를 구현할 수 있다. interface를 구현하는 class 는 interface 안에 요소를 구현해야 한다. 반드시 받아야 하는 변수에 경우 생성자를 통해 받지 않으면 에러를 발생시킨다.

interface extends

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IPerson2 {
name: string;
age?: number;
}

interface IKorean extends IPerson2 {
city: string;
}

const k: IKorean = {
name: "Koo",
city: "부천",
};

interface 에 다른 interface를 상속받을 수 있다.

function interface

1
2
3
4
5
6
7
8
9
10
11
12
13
interface HelloPerson {
(name: string, age?: number): void;
}

const helloPerson: HelloPerson = (...args) => {
if (typeof args[1] === "number") {
console.log(`안녕하세요. ${args[0]} 입니다. 나이는 ${args[1]} 입니다.`);
return;
}
console.log(`안녕하세요. ${args[0]} 입니다.`);
};

helloPerson("mark", 39);

실제 타입 검사는 구현하는 함수에서가 아니라 인터페이스에서 한다. 다음과 같은 경우 에러를 발생시킨다.

1
2
3
4
5
6
7
interface HelloPerson {
(name: string, age?: number): void;
}

const helloPerson: HelloPerson = (name: string, age: number) => {
console.log(`안녕하세요. ${name} 입니다.`);
};

잘 생각해보면 helloPerson은 interface HelloPerson에 의해서 age를 인수로 받을 수도 있고 안받을수도 있다. 그런데 함수를 구현하는 부분에서 무조건 age를 받도록 구현했기 때문에 논리적으로 맞지 않다.

Readonly Interface Properties

읽기 전용 속성으로 만들면 값을 생성하고 나서 수정할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
interface Person8 {
name: string;
age?: number;
readonly gender: string;
}

const p81: Person8 = {
name: "Mark",
gender: "male",
};

// p81.gender = "female";

주석 부분에 p81의 gender 를 바꾸려고 하면 에러가 발생한다.

type alias vs interface

몇가지 차이점이 있다.

함수

1
2
3
4
5
type EatType = (food: string) => void;

interface IEatType {
(food: string): void;
}

array를 만들때

1
2
3
4
5
type PersonList = string[];

interface IPersonList {
[index: number]: string;
}

intersetction

1
2
3
4
5
6
7
8
9
10
11
12
interface ErrorHandling {
success: boolean;
error?: { message: string };
}

interface ArtistsData {
srtists: { name: string }[];
}

type ArtistsResponseType = ArtistsData & ErrorHandling;

interface IArtistsResponseType extends ArtistsData, ErrorHandling {}

unoon type

유니온 타입은 인터페이스에서 상속 받을 수 없다. 또한 클래스에서 구현하는것도 불가능하다.

Declaration Merging

interface에 경우 같은 이름에 인터페이스를 만들면 머지 되서 사용할때는 하나의 인터페이스 처럼 사용할 수 있음.

type alias는 같은 이름으로 생성하면 에러가 발생한다.

Classes

What a Class

object를 만드는 설계도이다. ex6 이전에는 function을 사용해서 object를 만들었다. 오브젝트는 new 키워드를 사용해서 만들 수 있고 타입스크립트를 사용하면 oop에 맞게 클래스를 작성할 수 있다. 타입스크립트에서는 class 자체도 어떤 타입이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
name: string = "Mark";
age: number;

constructor(age?: number) {
if (age === undefined) {
this.age = 20;
} else {
this.age = age;
}
}
}

const p1 = new Person(39);
const p2 = new Person();
console.log(p1);
console.log(p2);

생성자를 통해서 인수를 받을 수 있다. TS에서는 생서자 오버라이드도 지원하지만 생성자에서 받을수도 있고 안받을 수 도 있는 경우엔 ?를 사용해서 생성자를 작성한다.
이때 받는 인수에 타엡에 대한 처리를 따로 해주어야 한다.

생성자는 async를 사용할 수 없다. async 를 사용하기 위해서는 생성자 이외에 함수를 만들어서 async 키워드를 사용해야 한다. 이럴때 클래스에서는 멤버 변수가 할당됬는지 아닌지 알 수 없기 때문에 age!: number 같이 !를 사용한다.

Access Modifiers

클래스 외부에서 접근하는 것을 막기 위한 접근 제어자를 타입스크립트에서는 지원한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public name: string = "Mark";
private _age!: number;

public constructor(age?: number) {
if (age === undefined) {
this._age = 20;
} else {
this._age = age;
}
}
}

const p1 = new Person(39);
const p2 = new Person();
console.log(p1);
console.log(p2);

Js 에서 private 변수를 표현하기 위해서 _ 를 붙혔었는데 TS 에서 관례적으로 private 변수 앞에는 _를 붙힌다. ( 없어도 상관 없음. )

initialization in constructor param

생성자 안에 접근 제어자를 넣음으로서 생성함과 동시에 할당가지 할수 있다.

1
2
3
4
5
6
class Person {
public constructor(public name: string, private age: number) {}
}

const p1 = new Person("koo", 39);
console.log(p1);

위코드는 아래 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
public name: string;
private age: number;

public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

const p1 = new Person("koo", 39);
console.log(p1);

Getter & Setters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
public constructor(private _name: string, public age: number) {}

get name() {
return this._name + "Jayoun";
}

set name(n: string) {
this._name = n;
}
}

const p1 = new Person("koo", 39);
console.log(p1.name); // get 을하는 함수를 getter
p1.name = "Woongjae"; // set 을하는 함수를 setter
console.log(p1.name); // get 을하는 함수를 getter

getter 만 만들고 setter는 만들지 않는 방식으로 읽기만 가능한 프로퍼티를 만들 수 있다.

readonly properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public readonly name: string = "Mark";
private readonly country: string = "Korea";

public constructor(private _name: string, public age: number) {
this.country = "korea";
}

hello() {
// this.country = 'China';
}
}

const p1 = new Person("koo", 39);
console.log(p1.name); // get 을하는 함수를 getter
// p1.name = "Woongjae"; // readonly 이기 대문에 할당할 수 없다.
console.log(p1.name); // get 을하는 함수를 getter

readonly 를 사용하여 get 만 할 수 있는 프로퍼티를 만들 수 있다. 이때 할당은 처음 프로퍼티를 생성하는 부분과 생성자에서만 할 수 있다.

Index signatures in Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// class => object
// {mark: 'male', jade: 'male} Class A
// {chole: 'female', alex: 'male', anna: 'female'} Class B

// 동적이라면?
class Students {
// [index: string]: string; // 어떤 문자열이 와도 쓸 수 있다.
[index: string]: "male" | "female"; // 받을 수 있는 것 정의 할 수 있음.

mark: "male" = "male";
}

const a = new Students();

a.mark = "male";
a.jade = "male";

console.log(a);

const b = new Students();

b.chole = "female";
b.alex = "male";
b.anna = "female";

console.log(b);

동적으로 프로퍼티가 생기는 형식일때 사용할 수 있다. 받을 수 있는 프로퍼티를 정의해 주는 것도 가능하다.

static properties & method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person {
public static CITY = "Seoul";
private static privateCity = "bu";
public static hello() {
console.log("안녕하세요.", Person.privateCity);
}

public normalHello() {
console.log("안녕", Person.CITY);
}

public change() {
Person.CITY = "LA";
}
}

Person.hello();
console.log(Person.CITY);

const p1 = new Person();
// p1.hello(); // 이렇게 사용할 수 없음.
p1.normalHello();

const p2 = new Person();
p2.normalHello();
p1.change();
p2.normalHello();

static 키워드를 붙히면 인스턴스를 생성하지 않아도 클래스 이름으로 접근해서 사용할 수 있다.

Singltons 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ClassName {
private static instance: ClassName | null = null;

// 매개체로 이용해서 객체를 꺼내옴.
public static getInstance(): ClassName {
// ClassName 으로 부터 만든 Object가 있으면 그걸 리턴
// 없으면, 만들어서 리턴
if (ClassName.instance === null) {
ClassName.instance = new ClassName();
}

return ClassName.instance;
}

// new 를 직접 호출 할 수 없게 함.
// 다른 오브젝트 생성 금지
private constructor() {}
}

// 만들어진 단일 오브젝트를 공유하는 개념
const a = ClassName.getInstance();
const b = ClassName.getInstance();

console.log(a === b);

생성자 함수를 private 접근 제어자를 사용해서 밖에서 호출하지 못하도록함. getInstance() 같은 함수를 사용해서 인스턴스가 있다면 반환하고 인스턴스가 없다면 새롭게 생성한후 할당, 프로퍼티로 있는 인스턴스를 넘겨준다.

이렇게 함으로서 클래스로쿠터 단 하나의 오브젝트만 생성해서 사용하는 패턴을 만들 수 있다.

클래스의 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Parent {
constructor(protected _name: string, private _age: number) {}

public print(): void {
console.log(`이름은 ${this._name} 이고 나이는 ${this._age} 입니다.`);
}

protected printName(): void {
// 프로텍티트 접근 제어자를 통해서 부모 클래스의 private 프로퍼티도 접근 가능.
console.log(this._name, this._age);
}
}

class Child extends Parent {
// 접근 제어자도 오버라이드 됨.
// public _name = "Mark Jr.";

public gender: string = "male";

constructor(age: number) {
super("Mark Jr.", age);
this.printName();
}
}

// const p = new Parent("Mark", 39);
// p.print();

// const c = new Child("son", 39);
const c = new Child(5);
c.print();

부모 클래스에서 protected 로 선언한 함수 또는 프로퍼티는 클래스 외에서 접근은 불가능하지만, 상속받은 자식 클래스에서 접근은 가능하다. 자식 클래스에서 기본 생성자가 아니라 따로 만들었다면, super()를 사용하여 부모 클래스이 생성자를 호출해 주어야 한다. 그렇게 해야지 부모 클래스의 값이 할당 되고 this키워드를 사용하여 호출, 사용할 수 있게 된다.

abstract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// new 불가 , 상속후 완전하게 만든다음에 사용 가능.
abstract class AbstractPerson {
protected _name: string = "Mark";

// 구현 하지 않음 // 클래스의 abstract 붙여야 함.
abstract setName(name: string): void;
}

class Person extends AbstractPerson {
setName(name: string): void {
this._name = name;
}
}

const p = new Person();
p.setName("Koo");

완전하지 않은 클래스로서 외형만 만든다고 생각하면된다. 완전하지 않기 때문에 new로 생성할 수 없다. 추상 클래스를 상속 받는 자식 클래스에서는 abstract로 작성한 완전하지 않은 메서드를 완전히 구현해야 new로 생성할 수 있다.

generic

generic 과 any 에 다른점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function helloString(message: string): string {
return message;
}

function helloNumber(message: number): number {
return message;
}

// 타입만 다르고 로직은 같은 함수가 반복된다면 ...

function hello(message: any): any {
return message;
}

// 같은 로직이면 문자로 생각하고 싶지만 any가 나옴
// lnegth 같은 메서드를 사용해도 number 가 아니라 any가 나옴..
console.log(hello("Mark"));
// 같은 로직이면 숫자로 생각하고 싶지만..
console.log(hello(4));

// 만약 string을 넣으면 T 가 string이 되는 것. 마치 변수처럼 사용
function helloGeneric<T>(message: T): T {
return message;
}

// 리턴 타입은 'Mark'로 추정함.. 리터럴 타입으로
console.log(helloGeneric("Mark").length);
// 리턴 타입을 27로 추정함.
console.log(helloGeneric(27));
// true로 추정함.
console.log(helloGeneric(true));

제너릭 타입을 사용하면 함수 안에서 동적으로 받은 타입을 변수처럼 사용할 수 있다. 이를 활용하여 리턴 타입을 명시 하는지에 기능을 사용할 수 있다.

generic basic

1
2
3
4
5
6
7
8
9
function helloBasic<T, U>(message: T, coment: U): T {
return message;
}

// 사용법
// T 는 string U 는 27
helloBasic<string, number>("mark", 27);
// T 는 27 , U 39
helloBasic(27, 39);

사용할 때에는 두가지 방법으로 사용 가능하다.

  1. <> 안에 타입 명시 -> 이렇게 하면 매개변수로 주는 타입을 명시한 터입으로 지정해야 한다.
  2. 평범하게 사용, 이렇게 하면 타입스크립트가 추론해서 타입을 정하게 된다. 일반적으로 27과 같은 숫자를 넣으면 number 라고 생각할 수 도 있지만, 타입 스크립트는 타입을 가능한 좁게 가져가기 때문에 타입은 number 가 아니라 27이 된다.

generics array & tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function helloArray<T>(message: T[]): T {
return message[0];
}

// T는 string으로 추론됨.
helloArray(["hello", "world"]);
// T는 <string | number> 로 추론함. 유니온 타입이됨.
// string과 number 에서 모두 사용할수 있는 메서드만 사용할 수 있음.
helloArray(["Hello", 5]);

function helloTuple<T, K>(message: [T, K]): T {
return message[0];
}

// string 이 리턴 타입
helloTuple(["hello", "world"]);
// 리턴 타입이 정확하게 string으로 추정됨.
helloTuple(["Hello", 5]);

제너릭에서 배열과 튜플을 활용하는 방법은 다음과 같다. 이때 어떤 타입에 데이터가 인수로 들어올지 알 수 있다면 튜플로 사용하는것이 타입을 더 명시적으로 관리할 수 있다.

generic function

1
2
3
4
5
6
7
8
9
10
11
12
13
type helloFunctionGeneric1 = <T>(message: T) => T;

const helloFuncion1: helloFunctionGeneric1 = <T>(message: T): T => {
return message;
};

interface helloFunctionGeneric2 {
<T>(message: T): T;
}

const helloFunction2: helloFunctionGeneric2 = <T>(message: T): T => {
return message;
};

함수에서도 동일하게 generic 을 사용할 수 있다.

generic class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 클래스 전체에서 T는 유효범위를 갖는다.
class Person<T, K> {
private _name: T;
private _age: K;

constructor(name: T, age: K) {
this._name = name;
this._age = age;
}
}

new Person("Mark", 39);

new Person<string, number>("koo", 27);

generic with extends

1
2
3
4
5
6
7
8
9
10
11
class PersonExtends<T extends string | number> {
private _name: T;

constructor(name: T) {
this._name = name;
}
}

new PersonExtends("Mark");
new PersonExtends(27);
// new PersonExtends(true);

generic 에서 extends 는 일반적인 상속과는 다른 개념으로 사용된다. generic 에서 사용하게 되면 타입을 제한하는 역활을 하게 된다. 따라서 코드에서 new PersonExtends(true)는 에러를 인수로 <string | number > 가 아닌 값을 주었기 때문에 에러를 발생시킨다.

keyof & type lookup system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface IPerson {
name: string;
age: number;
}

const person: IPerson = {
name: "mark",
age: 39,
};

// key 이름으로 된 유니온 타입이 반환된다.
type Keys = keyof IPerson;

// 리턴 타이에도 문제가 생김...
// IPerson[keyof IPerson]
// => IPerson["name" | "age"]
// => IPerson["name"] | IPerson["age"]
// => string | number
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

// key 가 name 일 때 string
// key 가 age 일 때 number
getProp(person, "name");

function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
// 경우에 따라서 넣어야 하는 타입이 달라지기 때문에
obj[key] = value;
}

setProp(person, "name", "koo");
setProp(person, "age", 27);

keyof 를 사용하면 인터페이스에 key로 구성된 유니온 타입을 반환한다. 어떤 매개변수끼리 혹은 리턴 값 끼리 서로 관련 성이 있어서 타입이 달라지는 경우 keyofextends를 활용하여 관계성을 정의할 수 있다.

layout

플랫폼, 환경 및 화면 크기 전반에 걸쳐 일관성을 유지하기 위한 균일한 요소와 간격에 대해서..

Principles

  • 예측 가능성. 일관된 UI 영역 및 공간 구성으로 직관적이고 예측 가능으한 레이웃이여야 한다.
  • 일관성
  • 반응성

Layout anatomy

레이아웃은 유사한 기능ㅇ을 공유하는 요서와 구성 요서로 구성된다. 레이아웃 영역은 또한 작은 컨테이너를 그룹화 한다.

큰 화면 레이아웃에는 세 가지 주요 영역이 있다.

  1. App bars
  2. Navigation
  3. Body

반응형을 구현할때는 작은 화면에서 큰 화면으로 만들어 가면서 다양한 form factors 에 적용해 나가는 것이 좋다.

Body region

Body 영역은 대부분의 컨텐트를 표시하는데 사용된다. 일반적으로 목록, 카드, 버튼 및 이미지와 같은 구성 요소를 포함한다.

Body 영역은 세가지 매개변수에 대한 확장 가능한 값을 갖는다.

  1. Vertical and horizontal dimensions
  2. Number of Columns
  3. Margins

Responsive column grid

화면 크기에 따른 break point

네비게이션 서랍이나 목록을 표시하는 영역이다. 네비게이션 영역의 넓이는 확장되었을때는 256dp 접었을때는 72dp (rail)를 유지한다. margin 이 48dp 보다 적은 화면 사이즈에서는 body navigation 영역을 보여주기 위해서 body 영역이 줄어들 수 있다. (600~904dp 사이에 영역)

만약 화면 사이즈가 600dp 보다 작다면 modal 를 사용한다.

App bar

앱바는 사용자가 기본 작업을 수행하거나 본문 영역의 요소에 대해 작업을 수행하는 데 도움이 되는 구성 요소 및 작업을 표시하고 그룹화하는 데 사용된다.

Composition

Visual grouping

유사한 콘텐츠 또는 기능을 가진 레이아웃의 요소는 그룹화하여 다른 요소와 분리한다.

글자 영역에 빈공간을 사용하여 다른 요소와 분리한다.

Containment

관련된 요소끼리 가깝게 배치하고 관련 되지 않는 요소 끼리는 넓게 배치하여 두 그룹을 분리 할 수 있고, 선을 이용하여 분리할 수도 있다.

텍스트를 포함하는 컨텐츠는 사이즈가 쉽게 변해야 하며, 읽기 쉬운 상태로 남아있어야 한다.

Scaling with text

한줄에 40-60 글자가 있는 것이 이상적이다. 텍스트를 포함된 요소는 가독성을 유지하면서 요소 크기에 맞춰 크기를 조정해야 한고 한줄에 너무 길게 확장되지 않도록 해야한다.

가독성을 높이려면 line height 를 조정하는것이 좋다.

Material measurements

시각적 균형을 이루도록 하기 위해 대부분의 간격과 레이아웃 모두에 8dp로 정렬한다. 구성 요소의 크기는 8dp 단위로 조정되어 각 화면에서 일관된 시각적 리듬을 보장한다. 또한 아이콘, 텍스트와 같은 더 작은 요소는 4dp 기준으로 정렬할 수 있다.

Exception filters

Nest 에는 애플리케이션 전체에서 처리되지 않은 모든 예외를 처리하는 레이어가 내장되어 있다. 처리 되지 않은 예외가 여기에서 잡힌다.

기본적으로 이 작업은 HttpException 유형의 예외를 처리하는 내장 전역 예외 필터에 의해 수행된다.

예외가 인식되지 않는 경우(HttpException 도 아니고, HttpException에서 상속한 클래스도 아님) 기본 제공 예외 필터는 다음과 같은 기본 JSON 응답을 새성한다.

1
2
3
4
{
"statusCode": 500,
"message": "Internal server error"
}

http-erros 라이브러를 사용한다. 기본적으로 statusCode와 message 가 채워져서 응답으로 보내진다.

Throwing standard exceptions

Nest 는 HttpException class 를 내장해서 사용한다. 컨틀롤러에서 에러를 발생시키는 예시 코드를 만들어 보자.

1
2
3
4
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

클라이언트가 해당 엔드포인트를 호출하면 다음 json을 반환한다.

1
2
3
4
{
"statusCode": 403,
"message": "Forbidden"
}

HttpException 은 response 와 status 를 받는데 response 는 string이나 객체가 올 수 있고, status 는 HTTP status code 를 사용한다. response에 객체를 넘겨주면, serialize 해서 반환해 준다.

1
2
3
4
5
6
7
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
}

반환하는 JSON :

1
2
3
4
{
"status": 403,
"error": "This is a custom message"
}

Custom exceptions

대부분의 경우 custom exceptions를 사용할 일이 없지만 사용하고 싶다면 HttpException을 상속하는 예외 클래스를 커스텀 할 수 있다.

1
2
3
4
5
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}

HttpException 을 상속하였기 때문에 동일하게 동작하여 동일하게 사용할 수 있다.

1
2
3
4
@Get()
async findAll() {
throw new ForbiddenException();
}

Built-in HTTP exceptions

Nest는 기본 ThhpException에서 상속되는 일련의 표준 예외를 제공한다.

Exception filters

Exception filters 는 에러에대한 컨트롤을 위해서 디자인되었다. 다음은 HttpException 인스턴스 예외를 포착하고 이데 대응하는 사용자 응답을 구현하는 예외 필터를 만들어 본다.

이를 위해서는 Request와 Response가 필요하고 Request에서 url 을 가져와 logging을 만들고 Response.json() 메서드를 사용하여 응답되는 데이터를 직접 제어해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();

response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

모든 예외 필터는 ExceptionFilter 제너릭으로 구현해야 한다. catch에 exception type으로 제너링 타입을 사용한다. catch(exception: T, host: ArgumetnHost)

Guards

Guard 는 CanActivate 인터페이스를 구현하는 @Injectable() 데코레이터가 달린 클래스이다.

Guard 는 request 가 실행 가능한지 여부를 판단한다. 흔히 이것을 authorization이라고 부르며 Express 에서는 대부분 midlleware 에서 처리하여 특정 컨트롤러와 강하게 연결되는것을 막았다.

그러나 middleware 에 가장 큰 문제는 next 이후에 어떤 함수가 실행되는지 모른다는 것에 있다. Guard 는 ExecutionContext 를 알수 있고 request/response 싸이클에 선언적으로 사용할 수 있어서 코드를 더 읽기좋고 선언적으로 만들어 준다.

Guard 는 각 미들웨어 이후에 실행되지만 인터셉터나 파이프 이전에 실행된다.

Authorization Guard

Authorization 은 Guard 에 가장 좋은 예이다. AuthGuard 는 request 에서 토큰을 추출하고 판단한 후 다음 과정을 진행할지 판단하게 할 수 있다.

auth.guard.ts :

1
2
3
4
5
6
7
8
9
10
11
12
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}

Guard 는 canActivate() 를 구현하고 request를 넘길지 여부를 boolean 으로 반환한다. 또한, Promise , Observable를 반환할 수 있기 때문에 동기적으로나 비동기 적으로나 반환하는것이 가능하다.

Execution context

canActivate 는 ExecutionContext 인스턴스를 받고 ExecutionContext 는 ArgumetnsHost 를 상속한다. context 변수를 통하여 request 를 얻을 수 있다.

Role-based authentication

특정 권한을 가진 유저만 허용하는 Guard 를 만들어 보자. 지금은 template 로서 모든 권한을 허용하고 추후에 기능을 추가하면서 role 기반 authentication 을 만들어 본다.

roles.guard.ts :

1
2
3
4
5
6
7
8
9
10
11
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}

Binding guards

guard 는 exception filter 와 마찬가지로 controller-scoped, method-scoped, global-scoped 모두 가능하다. 다음은 @UseGuards() 데코레이터를 이용하여 controller-scoped 로 사용한 예제이다. 해당 데코레이터는 단일 인수나 쉼표로 구분하여 인수를 받을 수 있다.

1
2
3
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

내부 인스턴스를 전달할 수도 있다.

1
2
3
@Controller('cats')
@UseGuards(new RolesGuards())
export class CatsController {}

만약 method-scoped 로 사용하고 싶다면 UseGuards 데코레이터를 메서드 레벨에서 사용한다. 글로벌로 사용하고 싶다면 nest application 인스턴스에 useGlobalGuards() 메서드를 사용한다.

1
2
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

의존성 주입 측면에서 모듈 밖에서 useGlobalGuards() 메서드에 의해서 등록되므로 의존성을 주입할 수 없다. 이런 문제를 해결하기 위해서 module 주입해서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule{}

Setting roles per handler

위에서 작성한 roleguard 는 지금 어떤 권한도 확인하지 않고 있다. 권한을 확인하는 가장 좋은 방법은 metadata를 활용하는 것이다. nest 는 custom meataData 를 첨부하는 @SetMetadata() 데코레이터를 제공한다.

cats.controller.ts :

1
2
3
4
5
6
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catService.create(createCatDto);
}

이것이 작동하는 동안 경로에서 직접 @SetMetadata() 를 사용하는 것은 좋은 습관이 아니다. 대신 custom decorator 를 사용하도록 한다.

roles.decorators.ts :

1
2
3
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

이렇게 하면 읽기 쉽고 타입을 알 수 있는 custom decorator 가 만들어 진다. 사용은 다음과 같이 한다.

cats.controller.ts :

1
2
3
4
5
@Post()
@Roles('admin')
async create(@Body() createCatDto:CreateCatDto) {
this.catService.create(createCatDto);
}

Putting in all together

Guard 에 적용해보자. 먼저 권한 정보를 알아야하는데 우리는 metadata 를 통해서 권한정보를 첨부했다. 권한 정보에 접근 하기 위해서는 Reflector hleper class 를 사용한다.

roles.guard.ts :

Interceptors

NestInterceptor 인터페이스를 구현하고 @Injectable() 데코레이터가 어노테이션으로 붙는 클래스이다.

interceptor 는 AOP 관점에서 탁월한 능력을 몇가지 갖고 있다.

  • method 가 실행되기 전과 후에 외부 로직을 bind 할 수 있다.
  • 함수에 return 을 변형시킬 수 있다.
  • 함수에 예외를 변형시킨다.
  • 함수에 기본동작을 확장한다.
  • 특정 조건에 따라 완전히 함수를 오버라이딩 할 수 있다.

Basics

Interceptor 는 intercept() 메서드를 구현한다. intercept() 메서드는 두개의 매개변수를 갖는다. 하나는 ExecutionContext 인스턴이다( guard 와 정확히 같은 객체). ExecutionContextArgumentsHost 를 상속한다. 두번째 매개변수는 Call handler 이다.

Execution context

ArgumentHost 를 확장했고, ExecutionContext 는 현재 실행 프로세스를 세부적인 사항을 제공하는 헬퍼 메서드를 추가했다.

Call handler

CallHandler 인터페이스는 route handler method 를 interceptor 에 특정 시점에 호출하여 사용할 수 있는 handler() 메서드를 구현한다. 만약 interceptor() 메서드에서 handler() 메서드를 호출하지 않는다면 route handler method 는 실행되지 않을 것이다.

이것은 interceptor() 메서드가 효과적으로 request/response 스트립을 wrap 했다는 것을 의미한다. 결과적으로 route handler method 가 호출되기 전과 후에 호출되는 커스텀 로직을 구현할 수 있게된다. handler() 가 호출되기 전에 로직을 작성하면 route handler method 가 호출되기 전 실행되는 로직을 만들 수 있는 것은 분명한데, 호출된 이후에는 어떻게 알 수 있을까? 답은 handler() 메서드가 Observable 를 반환하기 때문에 알 수 있다. RxJs 기능을 사용하여 응답을 조작하는 작업을 할 수 있다. AOP 관점에서 handler() 메서드는 추가 로직이 추가되어야 하는 지점을 나타내는 Pointcut 이라고 불린다.

handler() 가 호출되는 시점에서 Controller 에 메서드가 트리거 되고 Observable 를 반환하기 때문에 stream 안에서 추가작인 작업을 수행할 수 있다.

Aspect interception

첫번째 use case 는 사용자의 intercation을 로그로 남기는 LoggingInterceptor 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');

const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}

NestInterceptor<T,R> 는 generic interface 이다. T 는 응답 스트림 Observable<T> R 은 Wrapping 된 Observable<R> 을 나타낸다.

interceptors 또한, constructor 를 통해서 주입될 수 있다.

Binding interceptors

인터셉터를 세팅하기 위해서 @UseInterceptors() 를 사용한다. pipesguards 와 마찬가지로 controller-scoped, method-scoped, global-scoped 모두 가능하다.

1
2
@UseInterceptors(LoggingInterceptor)
export class CatsController {}

이제 컨트롤러에 요청마다 다음과 같은 로그를 확인할 수 있다.

1
2
Before...
After... 1ms

위에 예에서 Type을 전달하여 프레임워크에 인스턴스화에 대한 책임을 맡기고 종속성을 주입했다. 또한, 인스턴스를 넘기는 것도 가능하다.

1
2
@UseInterceptors(new LoggingInterceptor())
export class CatsController{}

Controller 레벨에서 설정했기 때문에 모든 메서드마다 Logging 이 실행된다. 만약 특정 함수로 제한하고 싶다면 method 레벨에서 설정해주면 된다.

전역으로 설정하고 싶다면 app 에 useGlobalInterceptors() 를 사용하여 등록해주면 된다.

1
2
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

이렇게 작성하면 dependnecy injection 측면에서 좋지 않기 때문에 module 에 주입하도록 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}

Response mapping

handler()Observable 를 반환한다. 따라서 RxJS 의 map() 기능을 사용할 수 있다.

응답 매핑 기능은 라이브러리별 응답전략에서는 작동하지 않는다. (@Res 에서 작동 안함)

RxJS 의 map 을 사용하여data property 를 응답에 추가해 클라이언트에게 전달하는 TransformInterceptor 를 구현해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}

Nest interceptors 는 동기와 비동기 모두 동작한다. 필요하면 async 키워드를 사용할 수 있다.

interceptor 는 애플리케이션 전체를 가로지르는 좋은 해결책이 된다. 만약 모든 응답에서 null 인 값을 빈 스트링으로 바꿔야 한다고 가정해보자. 이 경우 interceptor 구현하고 전역으로 사용하도록 주입하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}

Exception mapping

RxJS 의 catchError() 를 사용하여 예외를 override 할수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}

Stream overriding

응답 시간을 줄이기 위해 캐시를 사용해 응답을 완전히 바꾸는 등의 작업도 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}

여기서는 하드 코딩 된 isCached 와 [] 를 사용했지만 중요한 점은 RxJS 의 of 를 사용하여 생성된 스트림을 여기에서 반환하므로 route handler 가 호출되지 않는다는 것이다. 만약 CacheInterceptor 를 사용하는 메서드에서는 응답이 빈 리스트로 바로 반환될 것이다.

More operators

route request 에 timouts 를 주고 싶다고 가정해보자. 일정 기간동안 응답 하지 않으면 error 를 반한하게 한다.

Middleware

미들웨어는 라우터 핸들러 전에 호출되는 함수이다. 기본적으로 express 에 middleware 와 동일하다. class 와 function 모두 사용할 수 있고, class 로 선언하는 경우에는 NestMiddleware 인터페이스를 구현해야 한다.

1
2
3
4
5
6
7
8
9
10
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}

Dependency injection

미들웨어는 Provider 와 Controller 와 마찬가지로 모듈 내에서 사용 가능한 종속성으로 주입할 수 있다.

Applying middleware

middleware 를 갖는 module 은 NestModule를 구현해야 한다. 데코레이터에서 따로 등록할 수 는 없고, configure() 메서드를 통해 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}

forRoutes 를 두어서 특정 요청에 대한 미들 웨어로 제한할 수도 있다. 원하는 요청에 유형을 참조하기 위해 RequestMethod.GET 과 같이 열거형을 사용한다. 추가적으로 configure() 메서드는 async 키워드 사용이 가능하다.

Route wildcards

패턴 기반의 경로도 지원한다. forRoutes({ path: 'ab*cd', method: RequestMethod.ALL })

Middleware consumer

MidddlewareConsumer 는 helper class이다. midlleware 를 관리하는 내장 메서드를 재공한다. chanid 형태로 호출할 수 있고, forRoutes 는 path, RouteInfo 객체 복수, 단수의 컨트롤러 클래스를 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';

@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController);
}
}

apply 메서드 또한 여러 인수를 사용하여 여러 middleware 를 지정할 수 있다.

Excluding routes

.exclude() 메서드에 string or RouteInfo 객체를 지정하여 특정 라우터에서는 middleware 를 제거할 수도 있다.

1
2
3
4
5
6
7
8
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);

exclude() 메서드는 path-to-regexp 패키지를 사용한다.

Functional middleware

함수로도 middleware 를 선언할 수 있다.

1
2
3
4
5
6
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};

module 에 적용 :

1
2
3
consumer
.apply(logger)
.forRoutes(CatsController);

다른 의존성이 없다면 funcional middleware 를 대안으로 생각해봐라.

Multiple middleware

apply()메서드 안에 순차적으로 제공하여 여러 middleware를 등록시킬 수 있다.

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController)

Global middleware

전역으로 한번에 모든 라우터에서 middleware 를 적용 시키고 싶다면 INestApplication 인스턴스에 use() 메서드를 사용하여 전역으로 등록시킬 수 있다.

1
2
3
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

글로벌 미들웨어에서 DI 컨테이너에 액세스하는것은 불가능하다. app.use() 를 사용할때 functional middleware 를 사용하거나, AppModule 내에서 .forRoutes(’*’) 와 함께 사용할 수 있다.