0%

Moudles

@Module() 데코레이터는 어플리케이션 구성에 필요한 metadate를 공급한다.

애플리케이션은 최소 한개 이상의 모듈이 존재햐아 하고 구성 요소를 구성하는 효과적인 방법으로 모듈을 사용한다. 아주 작은 애플리케이션이 아닌 일반적인 애플리케이션은 여러가지 모듈을 가진다.

모듈은 provider 를 캡슐화 한다. 즉 현제 모듈의 일부도 아니고 가져온 모듈에서 내보낸 것도 아닌 provider 를 주입하는것은 불가능하다.

Feature modules

생성한 controller와 service 는 어떤 기능에 연관되 있기 때문에 하나의 모듈로 만들어서 관리하는 것이 좋다. netst g module <이름> 으로 모듈을 생성할 수 있다.

Shared modules

nest 에서 모듈은 기본적으로 singletons 이다. 따라서 같은 인스턴스를 다양한 모듈에서 공유할 수 있다. 여러 다른 모듈 간의 특정 모듈에 속해 있는 서비스를 공유하고 싶다면 @Module 데코레이터에서 exports 하여 특정 서비스를 내보내야 한다.

1
2
3
4
5
6
7
8
9
10
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}

CatService 를 내보내서 다른 모듈에서 같은 인스턴스를 사용하도록 한다.

Module re-exporting

특정 모듈을 임포트하고 다시 내보낼수도 있다. CommonModule은 CoreModule로 가져오거나 CoreModule에서 내보내어 이 모듈을 가져오는 다른 모듈에서 사용할 수 있다.

1
2
3
4
5
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}

Dependency injection

모듈은 Provider 도 주입할 수 있다. (구성 하기 위한 목적으로 )

1
2
3
4
5
6
7
8
9
10
11
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}

그러나 순환 참조가 될수 있기 때문에 모듈 자체를 Porivder로 주입하는 것은 불가능하다.

Global modules

모든 곳에서 import 하는 같은 모듈을 계속 import 하는 것은 바보같은 짓이다. 그러나 nest 에서는 Provider를 캡슐화 하기 때문에 Provider 를 가져오지 않고는 다른 곳에서 사용할 수 없다.

어느 곳에서나 접근 가능한 Provider 집합을 제공하려는 경우, @Global 데코레이터를 사용하여 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}

이렇게 하면 모듈을 global-scope 에 있도록 만들 수 있다. 전역 모듈은 일반적으로 루트 또는 코어 모듈에서 한번만 등록해야 한다. 이렇게 등록한 모듈 내에 있는 Provider 는 어떠한 다른 모듈에서 import 모듈에 등록할 필요 없이 inject 할 수 있다.

Dynamic modules

Nest 에서는 쉽게 등록 가능한 모듈을 동적으로 만들 수 있고, Provider를 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}

forRoot 메서드는 동기식 또는 비동기식으로 모듈을 반환할 수 있다.

DatabaseModule 는 다음과 같은 방법으로 import 될 수 있다.

1
2
3
4
5
6
7
8
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

동적 모듈을 다시 내보내려면 내보내기 배열에서 forRoot() 메서드 호출을 생략 할 수도 있다.

Providers

Nest 에는 provider 간의 관계를 해결하는 inversion of control (IOC)가 내장되어 있다.

Optional providers

때때로 설정 객체와 같은 것을 optional provider 로 받을 필요가 있다. 설정이 등록되지 않으면 기본 값을 갖도록 하면 되기때문에 무조건 받을 필요가 없기 때문이다. 이럴때에는 provider에 @Optional 데코레이터를 붙혀서 사용한다.

1
2
3
4
5
6
import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

Property-based injection

지금까지 한것은 생성자에서 주입되기 때문에 constructor-based injection 이라고 부른다. 때때로 property-based injection 이 유용하다. 최상위 클래스가 여러 proivder 에 의존하는 경우 생성자 기반에 injection을 사용할경우 하위 클래스에서 super 를 사용하여 의존성을 주입해야 하는 귀찮은 경우가 생길 수 있다. 이럴땐 @Inject() 데코레이터를 사용해서 property-based injection을 사용하면 편리하다.

1
2
3
4
5
6
7
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}

이런 경우를 제외한 대부분의 경우에서 constructor-based injection을 선호해야 한다.

Provider registration

컨트롤러가 프로바이더를 소비할 수 있도록 모듈에 등록해 주어야 한다.

1
2
3
4
5
6
7
8
9
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}

함수를 잘 만드는법을 살펴본다. 함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있을까?

작게 만들어라

작은 함수가 좋다. 각 함수는 명백히 하나의 이야기를 표현해야 한다.

블록과 들여쓰기

블록 구조를 갖는 코드는 한줄 이어야 한다. 중첩 구조가 생길만큼 함수가 커져서는 안된다. 들여쓰기 수준을 1단이나 2단 정도로 유지한다.

한가지만 해라

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다. 함수를 판단할때 지정된 함수 이름 아래에서 추상화 수준을 판단한다.

함수를 만드는 이유는 큰 개념을 여러 단계로 나눠 수행하기 위해서이다. 추상화 수준이 둘 이상이라면 더 축소 가능할 수 있다. 따라서 한 가지만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

함수당 추상화 수준은 하나로

함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 한 함수 내의 추상화 수준을 섞으면 코드를 읽는 사람이 근본 개념인지 세부사항인지 구분하기 어려워 진다. 이렇게 뒤섞기 시작하면 점점더 엉망이 된다.

위에서 아래로 코드 읽기 : 내려가기 규칙

프로그램을 위에서 아래로 읽으면서 함수 추상화 수준이 한다계씩 낮아지게 작성해야 한다. TO 문단을 읽듯이 프로그램이 읽혀야 한다는 것인데 각 TO 문단은 이어지는 아래 단계의 TO 문단(~ 하려면) 을 참고한다.

ex )

  • TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.
    • TO 설정 페이지를 포함하려면 , 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
    • TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 “SuiteSetUp” 페이지를 찾아 include 문과 페이지 경로를 추가한다.
    • TO 부모 계층을 검색하려면 …

위에서 아래로 TO 문단을 읽어내려 가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기 쉬워진다.

Switch 문

Switch 문은 기본적으로 N가지를 처리하기 때문에 한가지 일만 하기 어렵다. 하지만 Switch 문을 사용하지 않을 수 도 없다. 하지만 각 switch 문을 저차원 클래스에 숨기고 절대 반복하지 않은 방법이 있다.

직원 유형에 따라 다른 값을 계산해 반환하는 함수이다. 좋지 않은 예시 : ❌

1
2
3
4
5
6
7
8
9
10
11
12
function calculatePay(employee: Employee) {
switch (employee.type) {
case 'COMMISSIONED':
return calculateCommissionedPay(employee);
case 'HOURLY':
return calculateHourlyPay(employee);
case 'SALARIED':
return calculateSalariedPay(employee);
default:
throw new Error(employee.type);
}
}

다음과 같은 문제가 있다.

  1. 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.
  2. 한가지 작업만 수행하지 않는다.
  3. SRP(Single Responsibility Principle) 단일 책임 원칙을 위반한다.
  4. OCP(Open Closed Principle): 개방 폐쇄 원칙을 위반한다. ( 소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.) → 새 직원을 추가할때마다 코드를 변경해야 하기 때문이다.
  5. 가장 큰 문제는 위 함수와 구조가 동일한 함수가 무한정 존재한다는 사실이다.

이문제를 해결하기 위해 switch 을 추상 팩토리(ABSTRACT FACTORY) 에 숨긴다. Switch 문을 통해 적절한 Employee 파생 클래스의 인스턴스를 생성한다. 여러가지 함수는 Employee 인터페이스를 거쳐 호출되게 한다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

좋은 예 : ✅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
abstract class Employee {
abstract isPayday(): boolean;
abstract calculatePay(): Money;
abstract deleverPay(pay: Money): void;
}

interface EmployeeFactory {
makeEmployee(record: EmployeeRecord): Employee;
}

class EmployeeFactoryImpl implements EmployeeFactory {
makeEmployee(record: EmployeeRecord): Employee {
switch (record.type) {
case 'COMMISSIONED':
return new CommissionedEmployee(record);
case 'HOURLY':
return new HourlEmployee(record);
case 'SALARIED':
return new SalariedEmployy(record);
default:
throw new Error(record.type);
}
}
}

Siwtch 문을 다형적 객체를 생성하는 코드로 숨겨 버린다.

서술적인 이름을 사용하라!

길고 서술적인 이름이 길고 서술적인 주석보다 좋다. 함수 이름만 보고 동작을 예측할수 있다면 좋은 코드라고 할수 있다. 단 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사 동사를 사용한다. includeSetupAndTeardownPages, IncludeSetupPages, includeSuiteSetuppage, includeSetupPage 등은 좋은 예이다.

함수 인수

함수의 인수는 적을수록 좋다. 보통 2개 정도까진 괜찮고, 4개 이상부터는 특별한 이유가 필요하다. 인수가 있는것보다 인수가 없는것이 코드를 읽는사람이 이해하기 쉽다. 인수가 많아지면 테스트도 어렵다. 2개이상의 인수를 받는 함수를 테스트 하는것은 테스트를 어렵게 만든다. 최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우이다.

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 가장 흔한 경우는 두가지이다.

  1. 인수에 질문을 던지는 경우이다. fileExists("myfile"): boolean
  2. 인수를 뭔가로 변환해 결과를 반환하는 경우 parseInt("2"): number

이 두경우는 독자가 당여하게 받아들인다.

플래그 인수

플래그 인수는 좋지 않다. 플래그 인수에 따라서 동작이 달라진 다는 의미가 함수에서 두가지를 한다는 의미이기 때문이다. 함수를 분리하는 것이 좋다.

redux 관련해서 작성했던 좋지 않은 예 : ❌

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
 export function fetchThunk<T, I extends {[key: string]: InitialState<T>}>(
fetch: AsyncThunk<T, string, {}>,
key: keyof I,
keep: boolean, // flag 인수
handler?: (state: I, action: PayloadAction<T>) => void, // handler fulfilled 이후에
) {
return {
[fetch.pending.type]: (state: I) => {
state[key].loading = true;
state[key].data = keep ? state[key].data : null; // flag 인수에 사용
state[key].error = null;
},
[fetch.fulfilled.type]: (state: I, action: PayloadAction<T>) => {
state[key].loading = false;
state[key].data = action.payload;
handler && handler(state, action);
},
[fetch.rejected.type]: (
state: I,
action: ReturnType<typeof fetch.rejected>,
) => {
state[key].loading = false;
state[key].error = action.error;
},
};
}

keep : 이라는 flag 인수를 두어서 데이터를 유지할지 선택하였다.함수를 분리하고 이름도 더 길게 서술하도록 한다.

handler 를 받아서 fulfilled 이후에 실행할 함수를 콜백으로 받았는데 이또한 handler가 있는 함수와 핸들러가 없는 함수로 분리한다.

좋은 예 : ✅

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
export function fetchDataKeepingPrev<T, I extends {[key: string]: InitialState<T>}>(
fetch: AsyncThunk<T, string, {}>,
key: keyof I,
) {
return {
[fetch.pending.type]: (state: I) => {
state[key].loading = true;
state[key].data = state[key].data // data kepp
state[key].error = null;
},
[fetch.fulfilled.type]: (state: I, action: PayloadAction<T>) => {
state[key].loading = false;
state[key].data = action.payload;
},
[fetch.rejected.type]: (
state: I,
action: ReturnType<typeof fetch.rejected>,
) => {
state[key].loading = false;
state[key].error = action.error;
},
};
}

export function fetchData<T, I extends {[key: string]: InitialState<T>}>(
fetch: AsyncThunk<T, string, {}>,
key: keyof I,
) {
return {
[fetch.pending.type]: (state: I) => {
state[key].loading = true;
state[key].data = null // null
state[key].error = null;
},
[fetch.fulfilled.type]: (state: I, action: PayloadAction<T>) => {
state[key].loading = false;
state[key].data = action.payload;
},
[fetch.rejected.type]: (
state: I,
action: ReturnType<typeof fetch.rejected>,
) => {
state[key].loading = false;
state[key].error = action.error;
},
};
}

export function fetchDataKeepingPrevWithHandler<T, I extends {[key: string]: InitialState<T>}>(
fetch: AsyncThunk<T, string, {}>,
key: keyof I,
handler?: (state: I, action: PayloadAction<T>) => void,
) {
return {
[fetch.pending.type]: (state: I) => {
state[key].loading = true;
state[key].data = state[key].data // data kepp
state[key].error = null;
},
[fetch.fulfilled.type]: (state: I, action: PayloadAction<T>) => {
state[key].loading = false;
state[key].data = action.payload;
},
[fetch.rejected.type]: (
state: I,
action: ReturnType<typeof fetch.rejected>,
) => {
state[key].loading = false;
state[key].error = action.error;
},
};
}

export function fetchDataWithHandler<T, I extends {[key: string]: InitialState<T>}>(
fetch: AsyncThunk<T, string, {}>,
key: keyof I,
handler?: (state: I, action: PayloadAction<T>) => void,
) {
return {
[fetch.pending.type]: (state: I) => {
state[key].loading = true;
state[key].data = null // null
state[key].error = null;
},
[fetch.fulfilled.type]: (state: I, action: PayloadAction<T>) => {
state[key].loading = false;
state[key].data = action.payload;
},
[fetch.rejected.type]: (
state: I,
action: ReturnType<typeof fetch.rejected>,
) => {
state[key].loading = false;
state[key].error = action.error;
},
};
}

그런데 이렇게 작성하니 인수를 줄이기 위해서 중복된 함수가 너무 많아진 느낌이다. 함수를 작성하는 쪽에 수고로움과 사용하는 쪽에서에 편리함 중에 선택해야 하는 문제 같다.

추가적으로 인수를 더 줄이기 위해서 FetchThunkData 라는 클래스를 만들어 key, fetch, keep 정도는 생성자에서 받아올 수 있을거 같다. 이렇게 하면 인스턴스에 이름으로 데이터를 유지할지를 명시하면 사용하는쪽에서 헷갈리지도 않고 함수도 중복되게 적지 않아도 될거 같다.

개인적으로 생각한 예 : ❇️ FetchThunkData 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type FetchThunkDataConstructor<T, S> = {
fetch: AsyncThunk<T, string, {}>;
keep: boolean;
key: string;
handler?: (state: S, action: PayloadAction<T>) => void;
};

export class FetchThunkData<T, S extends {[key: string]: InitialState<T>}> {
private fetch: AsyncThunk<T, string, {}>;
private keep: boolean;
private key: string;
private handler?: (state: S, action: PayloadAction<T>) => void;

constructor({fetch, keep, key, handler}: FetchThunkDataConstructor<T, S>) {
this.fetch = fetch;
this.keep = keep;
this.key = key;
this.handler = handler;
}

private pendingReducer = (state: S) => {
state[this.key].data = this.keep ? state[this.key].data : null;
state[this.key].loading = true;
state[this.key].error = null;
};

private fuflledReducer = (state: S, action: PayloadAction<T>) => {
state[this.key].loading = false;
state[this.key].data = action.payload;
this.handler && this.handler(state, action);
};

private rejectedReducer = (
state: S,
action: ReturnType<typeof this.fetch.rejected>,
) => {
state[this.key].loading = false;
state[this.key].error = action.error;
};

getFetchThunkReducer() {
return {
[this.fetch.pending.type]: this.pendingReducer,
[this.fetch.fulfilled.type]: this.fuflledReducer,
[this.fetch.rejected.type]: this.rejectedReducer,
};
}
}

사용하는 부분 :

1
2
3
4
5
6
7
8
9
10
11
12
const fetchThunkKeepDataNoHandler = new FetchThunkData({
fetch: fetchChallenges,
keep: true,
key: 'challenges',
});

const challengesSlice = createSlice({
name: 'challenges',
initialState,
reducers: {},
extraReducers: fetchThunkKeepDataNoHandler.getFetchThunkReducer(),
});

fetchThunkKeepDataNoHandler 라른 인스턴스에 이름이 있기 때문에 data를 유지하하고 handler 가 없는 reducer를 반환함을 알 수 있다.

또한, 생성자에서 필요한 인수를 받았기 때문에 getFetchThunkReducer 에서는 인수를 받지 않아도 된다.

이항 함수

이항 함수는 함수에 인자로 2개를 받는 함수이다. 2개의 인수가 한개의 값을 표현하기에 적합하거나, 자연적인 순서가 있는것이 아니라면 인자로 2개를 받는것은 1개를 받는것보다 이해하기 어렵다 (자연스러운 경우 ? new Point(x, y) 같은 경우, 인자 2개가 하나의 표인트를 표현하는것과 순서도 자연스럽다)

인수가 2개인 경우가 반드시 나쁜 경우는 아니지만 줄일수 있도록 해야한다. 예를들어서 writeField(outpurStream, name) 인자에 순서를 기억하기 어렵기 때문에 writeField(name) 으로 줄이면 좋다. 이럴 경우에는 OutputStream 이라는 클래스를 만들고 멤버 변수로 outputStrem 을 갖고, writeField(name) 메서드를 구현하여 outputStream.writeField(name) 같이 호출할 수 있다.

삼항 함수

인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다. 삼항 함수를 만들 때는 신중히 고려해야 한다.

인수 객체

인수가 2-3 개 필요하다면 일부로 독자적인 클래스 변수로 선언할 가능성을 짚어봐야 한다.

좋지 않은 예 : ❌

makeCircle(x: number, y: number, radius: number)

좋은 예 : ✅

makeCircle(center: Point, radius: number)

인수로 객체를 넘기는 것이 눈속임 이라 생각할 수 있지만, 객체에는 이름을 담기 때문에 결국엔 어떤 개념을 전달하게 된다.그것이 코드를 읽는 사람들에게 이해하는데 더 큰 도움을 준다.

자바스크립트에서 함수에 인수로 객체를 받으면 인수에 개념을 전달할수 있게됨으로 인수를 객체로 전달하도록 하는것이 좋다.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다. 가변적인 인수를 갖는 함수는 사실상 2개의 인수를 받는 이항함수로 생각할 수 있다. function sumFunction(arg1, ...argList)

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필요하다. write(name) 함수는 곧바로 이해가 되는데 이름이 무엇이든 쓴다는 뜻으로 생각할 수 있기 때문이다. 더 나은 이름으로는 wirteField(name) 과 같이 사용할 수 있다.

또는, 함수 이름에 키워드를 추가하는 방식을 사용할 수 있다. 이렇게 하면 인수에 순서를 기억할 필요 없이 함수에 이름으로 알 수 있게 된다.

부수 효과를 일으키지 마라.

함수에서 부수효과가 일어나면 한가이 일을 하겠다고 한 함수가 사실은 그렇지 못한 것이 된다.

안 좋은 예시 : ❌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserValidator {
private cryptographer: Cryptographer;

constructor(cryptographer: Cryptographer) {
this.cryptographer = cryptographer;
}

checkPassword(userName: string, password: string): boolean {
const user: User = UserGatewayImpl.findByName(userName);
if (user !== null) {
const codePhrase = user.getPhraseEncodedByPassword();
const phrase = this.cryptographer.decrypt(codePhrase, password);
if (phrase === 'Valid Password') {
Session.initialize();
return true;
}
}
return false;
}
}

checkPassword 가 패스워드를 확인해서 boolean 값을 반환한다고 생각하지만 실제로 안에서는 세션을 초기화 하는 일까지 하고 있는 것이다. 이럴경우 이름을 checkPasswordAndSessionIntialize 라고 바꾸는 것이 좋다. 그래도 합수가 한가지 일을 하게 한다는 원칙은 어긋나지만

부수효과 피하기

사이드 이펙트를 일으키지 않기 위해서는 객체를 그대로 사용하는 것이 아니라 객체를 복사해서 새로운 객체를 만들고 변경하는 방법을 사용해야 한다. 또한 전역 변수를 변경시켜 전역 변수를 사용하는 다른 함수에 영향을 주면 안된다.

명령과 조회를 분리하라

함수는 뭔가를 수행하거 답하거나 둘 중 하나만 해야한다. 둘다 하면 혼란을 초래한다.

좋지 않은 예시 : ❌

1
2
3
4
5
6
7
8
function set(attribute: string, value: string): boolean {
//...
return true
}

if (set('name', 'koo')) {

}

작성한 함수는 객체에 속성에 지정한 value 로 설정하면 true 를 실패하면 false를 반환하는 함수이다. 그러나 if 문 안에 들어가면 해석이 다양해진다. 개발자는 동사로서 설정하라고 판단하고 함수를 작성했지만 사용하는 쪽에서는 설정되어 있다면으로 해석할 수 있다. 함수 선언부를 확인하고 나서야 객체에 설정하는 함수구나 생각하기 쉽상이다. 따라서 설정 되어 있다면을 함수로 따로 분리하는 것이 현명하다.

좋은 예시 : ✅

1
2
3
if (attrubuteExist('name')) {
setAttribute('name', 'koo')
}

값을 설정하는 함수, 반환하는 함수를 분리하면 읽는 쪽에서 코드를 더 쉽게 이해할 수 있다.

2. 의미 있는 이름

클린 코드를 읽고 공부한 내용을 타입스크립트로 정리해본 내용입니다. 개인적으로 공부한 내용이라 틀린점도 있을 수 있습니다.

의도를 분명히 밝혀라

변수나 함수 그리고 클래스의 이름은 다음과 같은 굵직한 질문에 모두 답해야 한다. 존재 이유는? 수행 기능은? 사용 방법은? 따로 주석이 필요하다면 의도를 분명히 드러내지 못햇다는 말이다.

const d: number

const daysSinceCreation: number

코드의 함축되어 있는 정보를 독자로 하여금 이해할 수 있도록 명시적으로 이름을 지어야 한다.

좋지 않은 예시 : ❌

1
2
3
4
5
6
7
8
9
function getThem() {
const list1: number[] = []
for (let x of theList) {
if (x[0] === 4) {
list1.push(x)
}
}
return list1
}

드러나지 않은 함축적인 정보는 다음과 같다.

  1. theList에 무엇이 들어있는가?
  2. theList 에서 0 번째 값이 어째서 중요한가?
  3. 값 4는 무슨 의미인가?
  4. 함수가 반환하는 리스트 list1을 어떻게 사용하는가?

만약 해당 함수가 지뢰찾기에서 깃발이 꽂힌 상태에 cell를 반환하는 함수라고 생각한다면 함축적인 의미를 이름을 통해서 명시적으로 드러낼 수 있다.

좋은 예시 : ✅

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
export interface Cell {
isFlagged: () => boolean;
data: number[];
}
const FLAGGED_STATE = 4;
class CellImpl implements Cell {
data: number[] = [0];
constructor(flaggedState: number) {
this.data[0] = flaggedState;
}
isFlagged = () => {
return this.data[0] === FLAGGED_STATE;
};
}

const gameBoard: Cell[] = [new CellImpl(FLAGGED_STATE)];
function getFlaggedCells() {
const flaggedCells: Cell[] = [];
for (let cell of gameBoard) {
if (cell.isFlagged()) {
flaggedCells.push(cell);
}
}
return flaggedCells;
}
  1. theLIst 가 gameBoard 로 무엇이 들어가 있느지 알수 있게 되었다.
  2. theList 에 0번째 값은 칸 상태, 값 4는 깃발이 꽂힌 상태라는 의미인데 cell.isFlagged() 로 명확하게 표현할 수 있게 되었다.
  3. 함수가 반환하는 리스트 list1 은 깃발이 꽂힌 상태에 cell 이라는 flaggedCells 이름으로 명확해진다.

그릇된 정보를 피하라

이름으로 잘못된 정보를 주어서는 안된다.

  1. 널리쓰이는 의미가 있는 단어를 다른 의미로 사용하지 말것
  2. 특수한 의미에 단어를 의미에 맞게 사용할것 (List 를 사용할 경우 List인 경우에만 사용할것 아니라면 accountGroup 이나 accounts 가 낫다)
  3. 서로 흡사한 이름을 사용하지 말것
  4. 유사한 개념은 유사한 표기법을 사용할것 (일관성 유지)

의미 있게 구분하라

이름이 달라야 한다면 의미도 달라져야 한다. (a1, a2) 와 같은 구분에 변수는 아무런 의미도 전달하지 못한다.

불용어를 남용 하지 말아야 한다. ProudctInfo 나 ProductData 에서 Info 나 Data 는 명확한 구분이 되지 못하고 영어에서 a, an, the와 같이 사용될 뿐이다. 접두어를 사용하지 말란 의미가 아니라 접두어를 사용하여 구분하는 의미가 있어야 한다는 것이다.

NameString 과 Name 은 차이가 없다. Customer 와 CustomerObject 도 마찬가지이다.

읽는 사람이 차이를 알도록 이름을 지어야 한다.

발음하기 쉬운 이름을 사용하라

발음하기 쉬운 단어를 사용해야 지적인 대화가 가능해집니다.

좋지 않은 예시 : ❌

1
2
3
4
5
6
class DtaRcrd102 {
private genymdhms: Date;
private modymdhms: Date;
private readonly pszqint = '102';
// ...
}

발음하기 쉬운 단어를 사용한 좋은 예시 : ✅

1
2
3
4
5
6
class Customer {
private generationTimestamp: Date;
private modificationTimestamp: Date;
private readonly recordId = '102';
// ...
}

검색하기 쉬운 이름을 사용하라

긴 이름이 짧은 이름보다 좋다. 검색하기 쉬운 이름이 상수보다 좋다. 이름 길이는 범위 크기에 비례해야 한다.

좋지 않은 예시 : ❌

1
2
3
4
5
let s = 0;
const t: number[] = [];
for (let j = 0; j < 34; j++) {
s += (t[j] * 4) / 5;
}

좋은 예시 : ✅

1
2
3
4
5
6
7
8
9
10
const NUMBER_OF_TASKS = 34;
const realDaysPerIdealDay = 4;
const WORK_DAYS_PER_WEEK = 5;
let sum = 0;
const taskEstimate: number[] = [];
for (let j = 0; j < NUMBER_OF_TASKS; j++) {
const realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
const realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
sum += realTaskWeeks;
}

WORK_DAYS_PER_WEEK 을 단순히 5라고 하였을때 문제가 생겨 변경해야 한다면 5로 검색하기는 쉽지 않을 것이다. 그러나 WORK_DAYS_PER_WEEK 으로 명시했을 경우 쉽게 검색할 수 있다.

인코딩을 피하라

인코딩한 이름은 발음하기 어려우며 오타가 생기기도 쉽다.

헝가리식 표기법

과거에는 타입을 컴파일러가 검사하지 못했기 때문에 접두어에 짧은 문자를 더해서 타입을 표현하곤 했다. 요즘에 언어들은 컴파일러가 타입을 강제하고 기억하고 있기 때문에 이렇게 하는것이 오히려 방해가 되기도 한다. 또한 타입에 변환을 어렵게 한다. phoneNumber 를 string으로 타입을 바꾸어도 여전히 이름은 phoneNumber 이기 때문에 타입을 오해하기도 쉽다.

멤버 변수 접두어

멤버 변수에 m_ 와 같은 접두어를 붙힐 필요도 없다.

인터페이스 클래스와 구현 클래스

인터페이스와 구현 클래스를 구분하기 위해 인코딩이 필요하기도 하다. 과거에는 인터페이스에 IShapeFactory , 구현 클래스에 ShapeFactory 와 같은 방식으로 I 를 붙히기도 했는데 과도한 정보를 주는 느낌도 있어서 인터페이스를 ShapeFactory 와 같이 쓰고 구현체를 ShapeFactoryImpl 과 같이 사용하는것을 추천한다.

자신의 기억력을 자랑하지 마라.

변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다.

문자 하나만 사용하는것도 루프와 같이 짧은 범위 내에서 사용하는것을 제외하면 바람직하지 못하다. 나중에 무슨 의미였는지 기억하기 어렵다.

기억하기 쉬운 명로함이 최고임을 기억해라

클래스 이름

클래스 이름이나 객체 이름은 명사나 명사구가 적절하다. Customer, AddressParser 은 좋은 예이다. Manager, Precessor, Data, Info 등과 같은 단어는 피하고 동사는 사용하지 않는다.

메서드 이름

메서드 이름은 동사나 동사구가 적합하다. postPayment, deletePage, save 등은 적합하다.

기발한 이름은 피하라

재미난 이름보다는 명로한 이름을 선택하라

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다. 예를 들어 똑같은 메서드를 클래스마다 fetch, retrieve, get 으로 제각각 부르면 혼란스럽다. 메서드 이름은 독자적이고 일관적이어야 한다.

이름이 다르면 독자는 당연히 클래스도 다르고 타입도 다르다고 생각한다. 예를들어 DeviceManager와 ProtocolController이 근본적으로 하는일이 똑같다면 하나로 통일시켜야 한다.

말장난을 하지 마라

한 단어를 두가지 목적으로 사용하지 말아야 한다.

맥락이 같다면 중복하여 사용해도 되지만 맥락이 다르다면 다른 단어를 사용해야 한다. 예를들어 클래스에 add 라는 메서드를 추가한다고 생각해 보자 어떤 매개변수 두개를 받아서 두개를 조합해 새로운 값을 반환하는 맥락이라면 여러 클래스에서 add 라는 이름에 메서드를 동일하게 사용해도 된다. 그런데 어떤 클래스에서는 어떤 값을 받아서 리스트에 더하는 메서드를 추가한다고 생각해보자. 이럴 경우 add 라는 단어를 사용하는것이 적합할까? 이럴 경우는 앞에서와 맥락이 다르므로 다른 메서드 이름을 사용해야 한다. 이 경우 insert 나 append 가 적합해 보인다.

해법 영역에서 가져온 이름을 사용하라

코드를 읽는 사람도 프로그래머 임을 기억해라. 프로그래머에게 익숙학 기술 개념은 아주 많으며 해당 개념 용어를 사용하는 것은 이해 관점에서도 아주 바람직하다. VISITOR 패턴에 친숙한 프로그래머는 AccountVisitor 라는 이름을 금방 이해할 것이다.

문제 영역에서 가져온 이름을 사용하라

적절한 프로그래머 용어가 없다면 문제 영역에서 이름을 가져온다.

의미 있는 맥락을 추가하라

클래스, 함수, 이름 공간에 넣어 맥락을 부여한다. 모든 방법이 실패하면 마지막 수단으로 접두어를 붙인다. 예를 들어서 firstName, lastName, street, state, zipcode .. 와 같은 변수를 사용한다면 주소라는 사실을 금방 알아챈다. 하지만 state 라는 변수 하나만 매개변수로 사용한다면 주소에 일부분이라고 생각할 수 있을까? 좀 더 큰 구조(주소) 에 속한다는 것을 명시하기 위해서 addr 이라는 접두어를 사용할 수 있다. addrFirstName, addrState…

또는, 맥락에 맞는 클래스를 생성하는것이 더 도움이 된다. 예를 들어 밑에 나오는 코드에서 num, verb, pluraModifier 는 통계 추측에 사용되는 변수이다. 단순히 사용되기만 한다면 어디에 사용되는 변수인지 코드를 다 읽지 않고서는 예측하기 힘들다. 이때 GuessStaticMessage 라는 클래스를 만들어 해당 변수를 멤버변수로 포함시킨다면 맥락을 더해서 어디에 사용되는 변수인지 명확히 표현할 수 있다.

좋지 않은 예시 : ❌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function printGuessStatistics(candidate: string, count: number) {
let num: string;
let verb: string;
let pluralModifier: string;

if (count === 0) {
num = 'no';
verb = 'are';
pluralModifier = 's';
} else if (count === 1) {
num = '1';
verb = 'is';
pluralModifier = '';
} else {
num = String(count);
verb = 'are';
pluralModifier = 's';
}

const guessMessage = `There ${verb} ${num} ${candidate}${pluralModifier}`;
console.log(guessMessage);
}

좋은 예시 : ✅

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
34
35
36
37
38
class GuessStatisticsMessage {
private num: string = '';
private verb: string = '';
private pluralModifier: string = '';

make(candidate: string, count: number) {
this.createPluralDependentMessageParts(count);
return `There ${this.verb} ${this.num} ${candidate}${this.pluralModifier}`;
}

private createPluralDependentMessageParts(count: number) {
if (count === 0) {
this.thereAreNoLetters();
} else if (count === 1) {
this.thereIsOneLetter();
} else {
this.thereAreManyLetters(count);
}
}

private thereAreManyLetters(count: number) {
this.num = String(count);
this.verb = 'are';
this.pluralModifier = 's';
}

private thereIsOneLetter() {
this.num = '1';
this.verb = 'is';
this.pluralModifier = '';
}

private thereAreNoLetters() {
this.num = 'no';
this.verb = 'are';
this.pluralModifier = 's';
}
}

num, verb, pluralModifier 가 GuessStatisticMessage 클래스 안에 있으므로 맥락상 통계 메세지를 만드는데 사용되는 멤버 변수란 것을 알게 된다.

불필요한 맥락을 없애라

불필요한 맥락은 없애야 한다. 예를들어 고급 휘발유 충전소 라는 애플리케이션을 짠다고 했을때, 모든 클래스에 GSD 접두어를 붙히는 것은 바람직하지 못하다.

일반적으로 의미가 분명하다면 짧은 이름이 긴 이름보다 좋다. accountAddress 와 customerAddress 는 Address 클래스 인스턴스로는 적합하나 클래스 이름으로는 부적합하다.

Docker

  • 도커 이미지 안에는 도커를 실행시키는 파일 스냅샷과 명령어가 닮겨 있다. docker run 명령어를 실행하면 파일 스넵샷을 컨테이너로 옮기고 실행 명령어를 통해 실행한다. docker run <이미지> <명령어> 를 통해 뒤에 명령어를 컨테이너가 생성된후 실행 시킬 수 있다.
  • Docker에 Container는 C-grop과 네임스페이스를 통해 격리된 환경을 제공하는데, 리눅스에서 가능한 작업이 다른 호스트 os에서도 가능한 이유는 Docker 가 내부적으로 리눅스 vm을 사용하고 리눅스 커널을 공유하는 격리된 컨텐이너를 만들기 때문이다.
  • docker Stop 은 SIGTERUM 을 주어서 SIGKILL을 진행하기 전에 완료되지 않은 작업이 있다면 완료한다음 컨테이너를 종료시키고, docker kill 은 SIGTERUM 을 주지 않고 바로 컨테이너를 중지시킨다.
  • 실행중인 컨테이너에서 명령을 실행시키려면 exec 으로 실행시켜야 한다. docker exec -it <containerID> 명령어 이런식으로 사용하는 -it 는 -i (interactive), -t (terminal) 을 합성한 것으로 명령을 실행하고 바로 나와버리는 것이 아니라 실행한 후에 터미널에서 추가적인 명령을 입력할 수 있도록 해준다.
  • 쉘의 접속하고 싶다면 docker exect -it <containerId> sh 로 쉘의 접속할 수 있다. sh 는 bash, zsh 등 사용하는 컨테이너에 따라 다양하게 사용 가능하다.

DockerFile

  1. baseimage 설정 FROM
  2. 워킹 디렉터리를 설정해줌 → 파일 시스템덮어씌워지는것 방지, 관리하기 용이하게 WORKDIR
  3. 추가적인 스넵샷 및 shellscript 추가 RUN
  4. 리소스 복사 COPY
  5. 이미지 시작후 명령어 CMD

이미지가 생성되는 순서

dockerfile → 이미지 생성 → 임시 컨테이너 생성 → 임시컨테이너로 이미지 생성 → 이미지 생성 완료

dockerfile → dockerClient → dockerServer → 이미지 생성

네트워크 연결하기

docker run -p <로컬 포트>:<컨테이너 포트> <이미지이름>

백그라운드 실행

docker run -d -p 5000:8000 rnwkdbs12/node

-d : detach 의 약자로 바로 빠져나온다는 의미 → 즉 백그라운드에서 실행시키고 터미널로 바로 나옴.

종속성 다시 다운로드 안받게 하려면..

npm install 전의 package.json을 따로 copy 하고 npm install 후에 다른 소스 파일을 copy 하도록 하면 된다.

다시 빌드하지 않으려면

volumn 을 설정하여 컨테이너가 로컬을 단순히 복사하는 것이 아닌 참조할 수 있도록 해야 한다.

docker run -dp 5000:8000 -v /usr/src/app/node_modules -v /$(pwd):/usr/src/app rnwkdbs12/node 처음 설정한 -v 옵션은 참조하지 않을 경로를 의미하고 두번째로 표시한 -v 테그는 참조할 디렉터리를 현제 로컬의 디렉터리로 매핑한 것을 의마한다.

Docker Compose

여러개의 컨테이너를 묶어서 실행할 수 있다.

docker-compose.yml 사용

1
2
3
4
5
6
7
8
version: "3" 
services:
redis-server: // 네트워크에 이름이 된다.
image: "redis"
node-app: // 다른 컨테이너가 된다.
build: . // 현제 폴더에서 Dcokerfile을 찾아서 build
ports:
- "8080:8080"

docker-compose up

docker-compose up --build build 하면서 up

docker-compose up -d background 실행

docker-compose down 실행 종료

간단한 앱 배포

개발 환경

개발 환경을 위한 Dockerfile 은 명시적으로 Dockerfile.dev 를 사용한다.

docker build -f Dockerfile.dev ./ 를 통해서 명시적으로 이름을 지정해 주어야 한다.

Dockercompose 로 간단하가게 구성하기

1
2
3
4
5
6
7
8
9
10
11
12
version: '3'
services:
react:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- /usr/src/app/node_modules
- ./:/usr/src/app
stdin_open: true

test

test 를 위해서는 추가적인 명령어를 붙히면 된다.

docker run -it rnwkdbs12/reactdev yarn run test

테스트 파일도 실시간으로 변경 사항을 적용하기 위해서는 volumn을 연결해야 한다.

dockercompose 파일을 추가하여 tests 컨테이너를 추가하면 실행과 동시에 테스트까지 진행할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version: '3'
services:
react:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- /usr/src/app/node_modules
- ./:/usr/src/app
stdin_open: true
tests:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- /usr/src/app/node_modules
- ./:/usr/src/app
command: ["yarn", "run", "test"]

운영을 위한 배포

1
2
3
4
5
6
7
8
9
FROM node:alpine as builder
WORKDIR '/usr/src/app'
COPY package.json .
RUN yarn install
COPY ./ ./
RUN yarn run build

FROM nginx
COPY --from=builder /usr/src/app/build /usr/share/nginx/html

build 후에 nginx 이미지를 설정하여 정적 파일을 옮겨 준다. nginx 에 기본 설정이 /usr/share/nginx/html 로 되어 있기 때문에 build 파일을 옮겨 준 것이다.

이미지를 실행할때 컨테이너 포트로 80번 포트를 사용하는데 nginx 기본 설정이 80번 포트를 사용하기 때문이다. 이 또한 설정에서 변경 가능하다.

TravisCI 사용

travisCI 는 .tavis.yml 파일을 이용해서 설정 파일을 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo: required

language: generic

services:
- docker

before_install:
- echo "start creating an image with dockerfile"
- docker build -t rnwkdbs12/reactdev -f Dockerfile.dev .

script:
- docker run -e CI=true rnwkdbs12/reactdev yarn run test -- --coverage

after_success:
- echo "Test success"
  • sudo : 관리자 권한이 필요함
  • language : 언어 설정
  • services : docker 와 함께 사용함 명시
  • before_install : script 실행전 필요한 명령 실행 → docker를 사용함으로 이미지 build v필요
  • script : 이미지 실행, test 실행
  • after_sucess: 스크립트 실행 후에 실행될 명령.

AWS 를 이용한 배포

EC2와 Elastic Beanstalk를 이용하여 서비스를 배포한다. travis 를 통해서 테스트에 성공한 소스를 AWS Elastic Beanstalk에 자동으로 배포하는 부분을 travis 파일에 넣어준다.

travis CI 에서 가지고 있는 파일을 압축해서 S3에 보낸다. S3 는 Elastic Beanstalk를 생성할때 자동으로 생성된다.

IAM 사용자 생성

처음 가입하고 사용하면 Root 계정으로 가입 되어서 모든 리소스에 대한 액세스 권한이 있다. Root 사용자를 사용하는 것은 좋지 않아서 보안을 위해서 IAM 유저를 생성 권한을 부여하는 방식을 사용한다.

reset

HEAD 는 현제 커밋 위치이다.

git reset HEAD~1 로 하나 전 커밋으로 갈 수 있다. ( 커밋한 기록은 사라진다) 커밋한 기록을 보관하고 싶은것이라면 reset을 사용하면 안되고 그래도 사용해야 한다면 원격 저장소에 올려 놓고 reset을 실행해야 한다.

reset의 3가지 모드 git reset HEAD~1 --<mode (soft, mixed, hard)>

  1. 기본 모드인 mixed는 새커밋에서 기존 커밋으로 이동할때 staged 는 하지 않은 상태로 돌아간다.
  2. soft 는 staged 된 상태로 돌아간다.
  3. hard 는 수정 내용을 아예 다 버린채 기존 커밋으로 돌아간다.

만약 새로운 수정 내용이 있는상태에서 마음에 들지 않아서 수정 내용을 모두 삭제하고 싶다면 HEAD 를 없애고 git reset --hard 하면 된다.

또는 commit ID 를 통해서 돌아 갈수도 있다. git reset <commitId>

revert

revert 는 커밋을 되돌리기는 하지만 이 또한 새로운 커밋으로 만든다. git revert HEAD → 새로운 커밋 생성

왜 사용할까?

  • 실수한 커밋이 원격 저장소에 올라감
  • 리셋 하면 원격에 올라간 내용은 되돌릴수 없음.
  • revert를 통해서 실수 위에 돌아가는 커밋을 다시 푸시함으로서 해결 가능. (실수 만회 까지 공유할수 있게됨)

merge

브랜치에서 다른 브랜치에 내용을 합친다. 메인 브렌치에서 기능 브랜치를 합치려면 git merge <기능 브랜치> 충돌 가능성도 있는데 파일 수정 후 커밋해서 충돌을 해결한다. merge 자체를 중단하고 싶다면 git merge --abort 하면 된다.

rebase

git rebase <기능 브랜치> 충돌 발생한 파일을 수정후 stage에 올린다. 그후 git rebase --continue 로 다음 작업을 이어 한다. 마찬가지로 중단하고 싶다면 git rebase --abort 로 수정을 중단할 수 있다.

merge 와 다른점은 메인 브랜치에 에서 분기한 조상 커밋을 기능 브랜치에 마지막 커밋으로 바꿔서 합치기 때문에 분기가 사라지고 fast forward 로 합쳐지는 효과가 있다.

Cherry Pick

merge나 reabase 를 하면 머지 하려는 브랜치에 최신 커밋 내용을 포함한 내용이 합쳐진다. 만약 머지 하려는 브랜치에 특정 커밋만 합치고 싶다면 cherrypick 을 사용하면 된다.

git cherry-pick <커밋 아이디> 특정 커밋만 합쳐서 새로운 커밋을 생성한다. 합쳐지는 것이 아님.

Cherry Pick 관련된 내용은 아니지만

git chekout <commit | branch> <file path> 로 특정 파일만 가져올수 있다.

tag

특히 중요한 커밋에 tag를 추가하면 id 가 아니로 tag로 커밋을 식별할 수 있다.

git tag v1.0.0 이제 커밋 아이디가 아니라 태그명을 사용할 수 있다. git resset v1.0.0

stash

어떤 작업을 하고 있다가 다른 작업을 해야해서 브랜치를 옮겨야 한다면 기존 방법 대로라면 완성 되지 않은 코드를 커밋 해야 한다. stash를 사용하면 커밋하지 않고 작업 내용을 저장할 수 있다.

git stash 현제 작업 중인 내용을 저장한다.

git stash list stash에 목록을 확인한다.

git stash apply stash를 적용해서 stash 한 시점으로 돌아간다.

git stash drop <stash 아이디 혹은 생략 가능(첫번째 삭제)> 로 저장했던 stash 를 삭제할 수 있다.

물마시기 알람 앱 만들기

Local Notification : 푸시 알림?

앱 내부에서 자체적으로 만든 특정 메세지를 전달하는 것

LocalNotification

UNNotificationRequest

3가지 필수 내용

  1. identifier
  2. UNMutableNotificationContent (Content.title, Contetn.body)
  3. Trigger
    1. UNCalendarNotificationTrigger
    2. UNTimerIntervalNotificationTrigger 시간 간격당 알림 ( 타이머에 가까움)
    3. UNLocationNotificationTrigger

UNNotificationCenter 에 보내면 Trigger 타이밍에 알려줌.

Content 설정

  1. AppDelegate 에 UNUserNotificationCenterDelegate 를 확장하도록 한다.
  2. UNUserNotificationCenter 를 생성하고 알람을 허용할지 요청한다.
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
import NotificationCenter
import UserNotifications

class AppDelegate: UIResponder, UIApplicationDelegate {
var userNotificationCenter: UNUserNotificationCenter?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UNUserNotificationCenter.current().delegate = self

let authrizationOptions = UNAuthorizationOptions(arrayLiteral: [.alert, .badge, .sound])

userNotificationCenter?.requestAuthorization(options: authrizationOptions, completionHandler: { _, error in
if let error = error {
print("Error: \(error.localizedDescription)")
}
})

return true
}
// 생략
}

extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .list, .badge, .sound])
}

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
completionHandler()
}
}
  1. UserNotficationCenter 에 request 를 보내는 코드를 추가한다.
1
2
3
4
5
6
7
8
9
10
11
import UserNotifications

extension UNUserNotificationCenter {
func addNotificationRequest(by alert: Alert) {
let content = UNMutableNotificationContent()
content.title = "물 마실 시간이에요.💧"
content.body = "세계보건기구가 권장하는 하루 물 섭취량은 1.5~2리터 입니다."
content.sound = .default
content.badge = 1
}
}

Trigger

trigger 를 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
extension UNUserNotificationCenter {
func addNotificationRequest(by alert: Alert) {
let content = UNMutableNotificationContent()
content.title = "물 마실 시간이에요.💧"
content.body = "세계보건기구가 권장하는 하루 물 섭취량은 1.5~2리터 입니다."
content.sound = .default
content.badge = 1

// 추가 부분
let component = Calendar.current.dateComponents([.hour, .minute], from: alert.date)
let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: alert.isOn)
}
}

request

request 를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension UNUserNotificationCenter {
func addNotificationRequest(by alert: Alert) {
let content = UNMutableNotificationContent()
content.title = "물 마실 시간이에요.💧"
content.body = "세계보건기구가 권장하는 하루 물 섭취량은 1.5~2리터 입니다."
content.sound = .default
content.badge = 1

let component = Calendar.current.dateComponents([.hour, .minute], from: alert.date)
let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: alert.isOn)

// 추가 부분
let request = UNNotificationRequest(identifier: alert.id, content: content, trigger: trigger)

self.add(request, withCompletionHandler: nil)
}
}

이제 알람이 추가되어야 할 부분에 request를 추가하고, 삭제 되어야 할 부분에 remove를 호출한다.

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
import UserNotifications

class AlertListViewController: UITableViewController {
var alerts: [Alert] = []
// userNotificationCenter 추가
let userNotificationCenter = UNUserNotificationCenter.current()
// 생략
@IBAction func addAlertButtonAction(_ sender: UIBarButtonItem) {
guard let addAlertVc = storyboard?.instantiateViewController(withIdentifier: "AddAlerViewController")
as? AddAlerViewController else {return}
addAlertVc.pickedDate = {[weak self] date in
guard let self = self else {return}

var alertList = self.alertList()
let newAlert = Alert(date: date, isOn: true)

alertList.append(newAlert)
alertList.sort { $0.date < $1.date}

self.alerts = alertList
UserDefaults.standard.set(try? PropertyListEncoder().encode(self.alerts), forKey: "alerts")
// 알람 추가
self.userNotificationCenter.addNotificationRequest(by: newAlert)

self.tableView.reloadData()
}

self.present(addAlertVc, animated: true, completion: nil)
}
//... 생략
}

알람이 필요 없는 곳에서는 삭제 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
self.alerts.remove(at: indexPath.row)
UserDefaults.standard.set(try? PropertyListEncoder().encode(self.alerts), forKey: "alerts")
// 아직 시작되지 않은 알람을 삭제하는 것이므로 removePendingNotificationRequest 호출
userNotificationCenter.removePendingNotificationRequests(withIdentifiers: [alerts[indexPath.row].id])
self.tableView.reloadData()
return
default:
break
}
}

기타

cmd + cntl + e : 모든 변수 이름 바꾸기

Design Pattern

어떤 목적을 달성하기 위한 효율적인 패턴. 유지보수하기 쉽게 해준다. 코드 구조에 대한 전략

디자인 패턴의 목적 :

  • 기술 부채 최소화
  • 재사용 및 지속 가능한 코드 만들기

클린 아키텍처, 디자인 패턴 이라는 용어로 통용 된다.

MVVM

Model -View - ViewModel

과거에는 MVC(Model - View - Controller) 패턴이 많이 사용되었다.

MVC

  • Model : 데이터 (Struct)
  • View : UI 요소 (UIView)
  • Controller : 중계자 (UIViewController) Model 과 View 사이에 중계자 역할

중계자인 UVIewConroller 가 비대해지기 시작했다. 결합도가 커지기 시작함.

MVVM

  • Model : 데이터 (Struct)
  • View : UI 요소 UIView, UIViewConroller
  • ViewModel : 중계자

ViewConroller 가 Model 에 직접 접근하지 못한다. ViewModel 이라는 클래스로 Model에 접근할 수 있게 함.

결국 MVVM 에서 하려는 것은 UIViewConroller 에 비중을 축소 시켜서 책임을 ViewModel 에 위임하고, 결과적으로 결합도를 낮추려는 것이다. → 명확한 책임을 갖게 된다.

1. 기본연산자

할당 연산자 (Assignment Opertor)

값을 초기화 시키거나 변경합니다. 튜플을 이용해 한번에 할당할 수 있습니다.

1
let (x,y) = (1,2)

C나 Objective-C와 다르게 Swift에서는 할당 연산자가 값을 반환하니 않습니다. 이는 동등 연산 비교를 사용해야 하는 곳에 실수로 할당 연산자를 사용하는 것을 막기 위함입니다.

사칙 연산자(Arthmetic Operators)

C나 Objective-C와 달리 Swift는 사칙 연산의 값이 오버플로우 되는 것을 허용하지 않습니다. 만약 허용하고 싶다면 어버플로우 연산자를 이용해야 합니다.

단항 음수, 양수 연산자

숫자 값은 -+ 로 표현되는 단항 연산자를 통해 부호를 결정할 수 있습니다.

1
2
3
let three = 3 
let minusThree = -three
let alsoPlusThree = +three

합성 할당 연산자(Compound Assignment Operaotors)

+= 같이 할당 연산자와 사칙연산 연산자의 조합을 사용 가능합니다.

비교 연산자(Comparison Operators)

기본적으로 다른 언어에서 제공되는 비교 연산자를 모두 지원합니다. 추가적으로 Swift는 객체 비교를 위해 식별자 ===!== 를 제공합니다.

같은 타입의 값을 갖는 두 개의 튜플을 비교할 수 있습니다. 튜플의 비교는 왼쪽에서 오른쪽으로 이루어 지고 한번에 한개의 값만 비교한후 결과를 반환합니다.

1
2
(1, "zebra") < (2, "apple") // true
(3, "apple") < (3, "brid)" // true

만약 타입이 달라서 비교할수 없는 경우는 에러가 발생합니다.

1
("blue", false) < ("purple", true) // 에러 , Boolen 값은 < 로 비교할 수 없음.

상함 조건 연산자(Ternary Conditional Operator)

question ? answer1 : answer2 와 같은 형태로 조건이 참일 경우 answer1 이 조건이 거짓일 경우 answer2 가 실행됩니다.

Nill 병합 연산자(Nil-Coalescing Operator)

a ?? b 형태를 갖는 연산자 입니다. 옵셔널 a 를 unwrap 해서 만약 nil 이면 b 를 반환합니다.

1
2
3
4
let defaultColorName = "red"
var userDefinedColorName: String?

var colorNameToUse = userDefinedColorName ?? defaultColorName

userDefinedColorNamenil 이 되기 때문에 defaultColorName 인 red 가 사용되게 됩니다.

범위 연산자 (Range Operators)

닫힌 범위 연산자(Closed Range Operator)

(a..b) 의 형태로 범위의 시작과 끝이 있는 연산자 입니다. 마지막 숫자인 b를 포함합니다.

1
2
3
4
5
6
7
8
for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25

반 닫힘 범위 연산자(Half-Open Range Operator)

(a..<b) 의 형태로 a 부터 b 보다 작을 때까지의 범위를 갖습니다.

단방향 범위(One-Side Ranges)

[a..] [..a] 의 형태로 범위의 시작 혹은 끝만 지정해 사용하는 범위 연산자 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names[2...] {
print(name)
}
// Brian
// Jack

for name in names[...2] {
print(name)
}
// Anna
// Alex
// Brian

마찬가지로 받 단힘 연산자도 사용 가능합니다. 또한, 특정 값을 포함하는지 여부를 확인할 때도 사용 가능합니다.

1
2
3
4
let range = ...5
range.contains(7) // false
range.contains(4) // true
range.contains(-1) // true

논리 연산자(Logical Operators)

Swift 에서는 세가지 표준 논리 연산자를 지원합니다.

  • 논리 부정 NOT (!a)
  • 논리 곱 AND (a && b)
  • 논리 합 OR (a || b)

Swift의 논리 연산자 && 와 || 는 왼쪽의 표현을 우선해서 논리 계산을 합니다.

명시적 괄호 (Explicit Parentheses)

논리 연산자의 적용 우선 순위를 연산자에게 맡기지 않고 명시적으로 괄화를 사용해 계산 순서를 정할 수 있습니다.

1
2
3
4
5
6
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// Prints "Welcome!"

CSRF 어택

로그인 해서 인증된 사용자가 원하지 않은 특정한 액션을 하도록 유도하는 공격. 이메일이나 이미지를 통해 특정 액션을 취하도록 한다. session riding 이라고도 하며 의미 적으로 로그인 된 사용자에 올라타서 권한을 흭득한 후 특정 잡업을 하도록 하는 것이다.

공격 순서

  1. 헤커가 웹사이트에 특정 행동을 하는 코드를 숨겨 놓는다.
  2. 이메일이나 채팅을 통해서 희생자에게 해당 웹사이트에 링크를 보낸다. (사이즈가 없는 이미지가 있거나, 눈에는 안보이는 버튼이 있는 형태)
  3. 희생자도 모르는 사이에 리퀘스트가 날라가고 HTTP Only 로 쿠키 정보가 저장되어 인증을 한다면 자동으로 승인된다.
  4. 헤커가 직접 쿠키를 가져갈 수 없지만 희생자에게 특정 액션을 취하게 할 수 있게 된다.

방어법

서버로부터 CSRF 토큰을 요청하고, 클라이언트는 CSRF 토큰을 메모리에서 관리될 수 있도록 하여 모든 요청때마나 CSRF 토큰도 같이 보내주도록 한다.