Front-end Developer

0%

절차적 언어의 아쉬운 점

  • 하나의 속성으로 뭉쳐야 할 데이터가 분산되어 각 배열로 관리되게 된다. 예를 들어 몸무게, 키의 속성을 가지는 사람 A, B의 데이터를 관리한다고 할 때, 각각의 속성들이 A라는 사람의 몸무게, 키, B라는 사람의 몸무게, 키로 관리되지 않고, 각각 사람이름 배열, 몸무게 배열, 키 배열로 분산되어 관리된다.
  • 그렇기 때문에 데이터가 많아지면 관리가 힘들어지고, 실수할 가능성이 높아진다.

절차적 언어에 대한 보완점

구조체: 데이터를 그룹으로 묶는다.

  • 그룹을 마치 하나의 변수처럼 사용한다.
  • 생성과 동시에 그룹 안의 모든 데이터는 초기화된다.
  • 기계가 이해하는 데이터 형태는 아니다.
  • 컴파일러가 알아서 그룹 내의 변수를 선언하는 느낌이다.
1
2
3
4
5
6
7
8
9
10
11
//c 언어의 의사 코드
struct Human
{
int age;
float height;
}

//함수 어딘가
Human human;
Human.age = 10;
Human.height = 170.0f;

절차적 언어(struct)의 한계

  • 여전히 데이터와 동작이 분리되어 있다.
  • 함수와 구조체가 여러 개 있다면 어떤 구조체가 어떤 함수와 연관있는지 찾기 귀찮다.

그렇다면 함수도 하나로 그룹화 하면 되지 않을까?


클래스

커스텀하게 만드는 자료형

1. 클래스 만들기

  • 이미 있는 자료형이 아니다.
  • Car라고 하는 새로운 자료형을 직접 만든 것이다.
  • 보통 Car.cs처럼 클래스명을 파일명으로 하고, 별도의 cs파일을 만들어서 클래스를 생성한다.
1
2
3
4
5
6
7
public class Car
{
//Car라는 데이터 타입의 정의
public int Price;
public float Gas;
public string Owner;
}

Price, Gas, Owner와 같이 중괄호 안에 작성된 것을 멤버 변수라 한다. 멤버 변수는 클래스 안에서 자유롭게 사용 가능하다. 멤버 변수는 초기화가 되고, 기본값이 0이다. 참조형(string)의 기본값은 null이다.

2. 개체 만들기

클래스를 사용한다는 것은 클래스를 데이터형으로 사용해서 그 데이터형의 변수를 만들 수 있다는 의미이다. 즉 개체를 만든다는 의미이다.

<클래스 이름> <변수명> = new <클래스 이름>();

1
2
3
//메인함수
//Program.cs
Car car = new Car();
  • 새로운 <클래스 이름>형의 데이터를 만든다.
  • 클래스로 정의된 형에 맞는 구체적인 데이터를 개체라고 한다.
  • C#에서는 new를 통해서만 개체를 만들 수 있다.

3. 개체의 멤버에 접근하기

1
2
3
4
5
6
//메인함수
//Program.cs
Car car = new Car();
car.Owner = "Alex";
car.Price = 2000;
car.Gas = 60.0f;

여기에 동작(함수)를 추가해야 한다.

4. 메서드 - 동작(함수) 추가

메서드(맴버 함수)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Car
{
public int Price;
public float Gas;
public string Owner;

public void Move()
{
Gas -= 0.5f;
Console.WriteLine($"Move! \n (Gas: {Gas}L left)");
}

public void Honk()
{
Console.WriteLine("honk");
}
}
  • 메서드는 개체의 행위들을 말한다.
  • 클래스 안에서 선언한 함수
  • 클래스 안에서 자유롭게 사용할 수 있고, 같은 클래스의 멤버 변수에 접근 가능하다.
1
2
3
4
5
6
7
8
9
//메인함수
//Program.cs
Car car = new Car();
car.Owner = "Alex";
car.Price = 2000;
car.Gas = 60.0f;

myCar.Move();
myCar.Honk();

클래스 생성 후 다음과 같은 경우 문제가 생길 수 있다.

1
2
3
4
5
6
//메인 함수
//Program.cs
Car car = new Car();
car.Gas = 50.0f;

Console.WriteLine($"Price is {car.Price}");

여기서 출력결과는 Price is 0이다. Price가 0이 나온 이유는 개체 생성 후 깜빡하고 데이터를 대입하지 않았기 때문이다. 이처럼 개체 생성 후 깜빡하고 데이터를 대입 안 할 경우 에기치 못한 문제가 발생할 수 있다.

생성자를 쓰면 이러한 문제를 해결할 수 있다.

생성자

개체가 만들어질 때 자동으로 호출되는 특정한 함수

public <클래스명> (<매개변수 리스트>) {}

1
2
3
4
5
6
7
8
9
10
11
12
//Car.cs
public class Car
{
public Car(int price)
{
Price = price;
}
}

//Program.cs
Car car1 = new Car(200); //ok
Car car2 = new Car() //컴파일 오류
  • 생성자 개체를 생성할 때(new) 반드시 호출되는 함수
  • 함수명으로 클래스명을 쓴다.
  • 반환형은 아예 적지 않는다.
  • 생성에 필요한 매개변수를 강제할 수 있다.
  • 생성자는 여러 개를 써도 된다.

하지만 생성자를 써도 문제가 발생할 수 있다.
클래스에 있는 모든 개체가 특정한 동일한 값을 가져야 하는 경우가 있을 수 있다. 예시처럼 Car의 첫 가격이 항상 2000이어야 하는 경우. 아래 코드를 보면 모든 차의 가격이 2000이어야 하는데, 개발자가 실수로 20을 입력한다면 car3의 가격은 20이 된다. 즉 예기치 못한 결과를 얻게 된다.

1
2
3
4
5
6
7
8
9
10
11
public class Car
{
public Car(int price)
{
Price = price;
}
}

Car car1 = new Car(2000);
Car car2 = new Car(2000);
Car car3 = new Car(20);

생성자 안에서 상수를 바로 대입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Car
{
public int Price;
//...

public Car(int price)
{
Price = 500000;
}
}

//Program.cs
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();

만약 생성자를 만들지 않았다면 기본적인 생성자는 있다고 가정한다. 따라서 car1, car2, car3의 값은 0이다. 이처럼 생성자를 만들지 않았다면 new Car()로 사용할 수 있고, 중간에 Car(int price)와 같은 값을 넣었다면 컴파일이 되지 않는다. 즉 생성자에 어떠한 값을 넣기 시작하면 기본 설정한 생성자는 사라져버린다.

위 방법보다 조금 더 나은 방법은 애초에 Car 클래스에서 개체를 만들 때 5000을 넣어준다. 이렇게 하면 생성자를 만들지 않아도 컴파일 에러가 나지 않고, 모든 Car의 Price는 5000이다.

1
2
3
4
public class Car
{
public int Price = 5000;
}

References
실무 프로그래밍 입문(C#)

  • 딕셔너리와 매우 비슷하다. 차이점은 해시셋은 키만 있다는 점이다.
  • 리스트와 딕셔너리를 쓰는 경우가 거의 90%이므로 거의 쓸 일이 없고, 특별할 때만 쓴다.
  • 중복 데이터를 제거할 때 가장 쓰기 편하다.
1
HashSet<T> <변수명> = new HastSet <T>(); //T는 저장할 키의 자료형
1
2
HashSet<int> studentIDs = new HashSet <int> ();
HastSet<string> studentNames = new HashSet <string> ();

해시셋에 요소 추가하기

1
bool bSuccess = HastSet<T>.Add(T data);
  • 해시셋에 없는 키 새 요소로 추가한 후 참을 반환
  • 해시셋에 있는 키 거짓을 반환
1
2
3
4
HashSet<int> studentIDs = new HashSet <int>();
bool bSuccess1 = studentIDs.Add(123); //참
bool bSuccess2 = studentIDs.Add(456); //참
bool bSuccess3 = studentIDs.Add(123); //거짓

이 요소가 해시셋에 있나요?

1
bool bConatin = HastSet<T>.Contain(T data);
  • 해시셋에 있는 키 참을 반환
  • 해시셋에 없는 키 거짓을 반환
1
2
3
HashSet<int> studentIDs = new HashSet <int>(); //{123, 456}
bool bContain1 = studentIDs.Contains(123); //참
bool bContain2 = studentIDs.Contains(178); //거짓

해시셋 요소 삭제

1
bool bRemoved = HastSet<T>.Remove(T data);
  • 해시셋에 있는 키 요소를 삭제한 후 참을 반환
  • 해시셋에 없는 키 거짓을 반환
1
2
3
HashSet<int> studentIDs = new HashSet <int>(); //{123, 456}
bool bRemoved1 = studentIDs.Remove(123); //참
bool bRemoved2 = studentIDs.Remove(178); //거짓

해시셋의 모든 요소 삭제

1
HastSet<T>.clear();
1
2
HashSet<int> studentIDs = new HashSet <int>(); //{123, 456}
studentID.Clear();

해시셋의 요소 가져오기

1
bool bSuccess = HastSet<T>.TryGetValue(T Key, out T key);
  • 해시셋에 있는 키 요소를 키에 연결된 요소를 out 매개변수에 대압하고 참을 반환
  • 해시셋에 없는 키 거짓을 반환
1
2
3
HashSet<int> studentIDs = new HashSet <int>(); //{123, 456}
int id;
bool bSuccess = studentIDs.TryGetValue(456, out iff)

컬렉션과 같이 쓰면 유용한 것들

리스트를 예로 들어, 순차적으로 요소를 탐색할 때 i가 왜 필요한지에 대한 의문이 생긴다. 또 부등호를 잘못 쓰는 것처럼 실수할 가능성도 있다.

1
2
3
4
5
6
List<string> names = new List<string>(5);

for (int i = 0; i < names.Count; ++i)
{
Console.WriteLine($"Name: {names[i]}");
}

foreach

어떤 컬렉션이든 순회할 수 있는 방법이 있으면서 실수를 방지하는 좋은 방법이 바로 foreach이다.

1
foreach (T<변수명> in List<T>)
  • foreach 문 안의 T는 리스트 선안할 때 사용한 자료형이다.
  • <변수명>은 foreach 문 범위에서만 사용된다.

리스트와 foreach문

1
2
3
4
5
6
List<string> names = new List<string>(4096);

foreach (string name in names)
{
Console.WriteLine(name);
}

딕셔너리와 foreach문

1
2
3
4
foreach (KeyValuePair<TKey, TValue> <변수명> in Dictionary<TKey, TValue>)
{

}
  • KeyValuePair의 TKey, TValue는 각각 딕셔너리를 선언할 때 사용한 키와 값의 자료형이다.
  • <변수명>은 foreach 문 범위에서만 사용된다.
1
2
3
4
5
6
Dictionary<string, string> students = new Dictionary<string, string>();

foreach (KeyValuePair<string, string> score in students)
{
Console.WrtieLine($"key: {score.key}, value: {score.Value}");
}

배열([])과 foreach문

1
foreach (T <변수명> in T[])
1
2
3
4
5
6
float scores = {30.5f, 41.0f}

foreach (float score in scores)
{
Console.WriteLine(score);
}

var

딕셔너리 foreach문이 작성해야하는 코드가 길고, 매번 keyValuePair를 써야한다는 문제점을 var가 해결해준다.

1
2
3
4
5
6
Dictionary<string, string> students = new Dictionary<string, string>();

foreach (var score in students)
{
Console.WriteLine($"Key: {score.key}, value: {score.value}");
}
  • 묵시적 자료형 : 컴파일러가 알아서 자료형을 추론해줌.
  • 지역 변수에서만 사용 가능.
  • 긴 자료형을 짧게 줄여준다.
  • 반드시 선언과 동시에 대입해주어야 한다.
  • 대입하는 값을 통해 명백하게 자료형을 알 수 있을 때만 사용한다.

References
실무 프로그래밍 입문(C#)

기본 타입

javaScript에는 string, number, boolean, null, undefined, symbol의 원시타입과 object, array, function 등의 객체가 있는데, typeScript에서도 거의 동일한 데이터를 지원한다. typeScript의 타입은 다음과 같다.

  • Boolean
  • Number
  • String
  • Object
  • Array
  • Null
  • Undefined
  • Tuple
  • Enum
  • Any
  • Void
  • Never

타입 표기(Type Annotation)

:을 사용하여 표기한다.

1. Boolean

1
2
//진위값
const show: boolean = true;

2. Number

TypeScript의 모든 숫자는 부동 소수 값이다.

1
2
3
4
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

3. String

1
2
3
4
5
6
7
8
9
10
11
//JS 문자열 선언방식
const str = 'hello';

//TS 문자열
const tsStr: string = 'hello';

//x템플릿 문자열 사용가능
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}.
I'll be ${age + 1} years old next month.`;

4. Array

배열의 표현 방법은 두 가지가 있다.

  • []: 배열요소들을 나타내는 타입 뒤에 []를 사용
  • 제네릭 배열 타입: Array<elemType>
1
2
3
4
5
6
//TS 배열 선언방식 <>안에 작성된 타입만이 배열의 요소로 사용가능
//<string>으로 작성되었는데 number를 넣으면 에러 발생.
const tsArr: Array<number> = [1, 2, 3];
const heroes: Array<string> = ['Capt', 'hulk', 'thor'];
//배열 리터럴을 통해 선언할 수도 있다.
const items: number[] = [1, 2, 3];

5. Tuple

요소의 타입과 개수가 고정된 배열 표현

1
2
3
4
5
6
7
//TS 튜플 - 배열의 특정 위치와 타입까지 설정.
const locations: [string, number] = ['gangnam', 100];

//정해진 인덕스 외의 요소에 접근하면 오류 발생
x[3] = 'world'; // 오류, '[string, number]' 타입에는 프로퍼티 '3'이 없습니다.

console.log(x[5].toString()); // '[string, number]' 타입에는 프로퍼티 '5'가 없습니다.

6. enum

c# 같은 언어에서 쓰는 것처럼 값의 집합에 더 나은 이름을 붙여준다. 즉 특정 값(상들의 집합을 말한다. 즉 특정 값(상수)들의 집합을 말한다. 값을 지정하면 지정한 값부터 멤버들의 번호를 매기고, 따로 지정하지 않았다면 0부터 시작하여 멤버들의 번호를 매긴다. 모든 값은 순차적으로 매기지 않고, 수동으로 설정해 줄 수도 있다.

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
//따로 값을 매기지 않았으니 0부터 시작하여 순차적으로 번호가 매겨진다.
enum Color {
Red,
Green,
Blue,
}

let c: Color = Color.Green;

//수동으로 값 설정
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
let c: Color = Color.Green;

// 매겨진 값을 사용해서 enum 멤버 이름을 알아낼 수 있다.
//아래 코드에서 2라는 값과 매칭되는 멤버의 이름을 알아낼 수 있다.
enum Color {
Red = 1,
Green,
Blue,
}
let colorName: string = Color[2];

console.log(colorName); // 값이 2인 'Green'이 출력됩니다.

7. Object

1
2
3
4
5
6
7
8
9
10
11
12
//TS 객체
const obj: object = {};
const person: object = {
name: 'capt',
age: 100,
};

//객체의 구체적인 타입 설정
const tsPerson: { name: string; age: number } = {
name: 'capt',
age: 100,
};

8. Any

모든 타입 허용

알지 못하는 타입을 표현해야 하는 경우(사용자로부터 받은 값 혹은 데이터, 서드파티 라이브러리와 같은 동적 컨텐츠에서 온 값) any를 사용하여 표기하면 컴파일 중에 점진적으로 타입 검사를 하거나 하지 않을 수 있다.

1
2
3
4
//다음과 같이 타입의 일부만 알고 전체 타입을 모르는 경우 유용하다.
let list: any[] = [1, true, 'free'];

list[1] = 100;

9. Void

어떤 타입도 존재할 수 없다. 보통 함수에서 반환 값이 없을 때 반환값을 표현하기 위해 사용한다.

Void 타입의 변수 선언은 undefined, null만 할당할 수 있다.

1
2
3
function warnUser(): void {
console.log('This is my warning message');
}

10. Null and Undefined

유용하게 쓰는 경우가 거의 없다. 모든 타입의 하위 타입이기 때문에 number 같은 타입에 할당할 수 있다.

1
2
3
// 이 외에 이 변수들에 할당할 수 있는 값이 없다.
let u: undefined = undefined;
let n: null = null;

11. Never

절대 발생할 수 없는 타입

함수 표현식이나 화살표 함수 표현식에서 항상 오류를 발생시키거나 절대 반환하지 않는 반환 타입으로 쓰인다. 변수 또한 타입 가드에 의해 아무 타입도 얻지 못하게 좁혀지면 never 타입을 얻게 될 수 있다. 이 역시 모든 타입에 할당 가능한 하위 타입이다. 그러나 never 자신을 제외한 어떠한 타입도 never에 할당할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// never를 반환하는 함수는 함수의 마지막에 도달할 수 없다.
function error(message: string): never {
throw new Error(message);
}

// 반환 타입이 never로 추론된다.
function fail() {
return error('Something failed');
}

// never를 반환하는 함수는 함수의 마지막에 도달할 수 없다.
function infiniteLoop(): never {
while (true) {}
}

References
타입스크립트 핸드북
타입스크립트 핸드북

인터체이스

상호 간에 정의한 약속 혹은 규칙. 보통 다음과 같은 범주에 대해 약속을 정의할 수 있다.

  • 객체의 스펙(속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙(파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스

typeScript의 핵심 원칙 중 하나는 덕 타이핑 또는 구조적 서브타이핑이다. 타입 검사가 값의 형태에 초점을 맞추고 있다는 것이다. 인터페이스는 이런 타입들의 이름을 짓고, 코드 안의 계약을 정의하고, 프로젝트 외부에서 사용하는 코드의 계약을 정의한다.

딕셔너리

List<T>와 달리 색인이 0~n 사이의 수가 아니라 임의의 데이터형이다. 사전과 비슷하게 키를 이용해 그 키에 해당하는 값을 찾는다.

키(key): 임의의 데이터형. 어떠한 데이터형도 키로 쓸 수 있다.
값(value): 실제 저장되는 값

  • 딕셔너리는 배열처럼 연속된 메모리에 내부 데이터를 저장할 수 없다. 따라서 배열이 더 효율적이다.
  • 다른 언어에서는 맵(map)이라고 한다.
  • 배열처럼 0, 1, 2… 이런 식으로 순서대로 저장하기 힘든 경우에 쓰면 좋다.
  • 데이터 저장 공간이 크고, 배열 중간에 데이터를 삽입 및 삭제를 자주해야하는 경우 쓰면 좋다.

딕셔너리 생성

1
Dictionary<TKey, TValue> <변수명> = new Dictionary<TKey, TValue>();
1
2
Dictionary<int, string> students = new Dictionary<int, string>();
Dictionary<int, int> scores = new Dictionary<int, int>();
  • TKey: 어떤 자료형의 키를 담을지 표현
  • TValue: 어떤 자료형의 값을 담을지 표현

딕셔너리에 데이터 추가하기

1
Dictionary.Add(TKey, TValue);

키와 매핑되는 값을 딕셔너리에 추가

1
2
3
4
5
6
Dictionary<string, string> students = new Dictionary<string, string>();
students.Add("A1","Bob");
students.Add("A2","Bobby");

//이미 들어 있는 키로 새로운 데이터를 추가하면?
students.Add("A2","Alex"); // 에러 발생

중복된 키를 확인 후 데이터 추가하기

이미 있는 키를 가지고 새로운 데이터를 추가하려고 하면 에러가 발생하기 때문에 이를 해결하기 위해 중복된 키가 있는지 확인 후 추가한다.

1
bool bSuccess = Dictionary<TKey, TValue>.TryAdd(Tkey key, TValue value);
1
2
3
4
5
Dictionary<string, string> students = new Dictionary<string, string>();
// {("A1", "Bob"),("A2", "Bobby")}

bool bSuccess1 = students.TryAdd("A1", "Bob"); //거짓
bool bSuccess1 = students.TryAdd("A3", "Alex"); //참
  • 딕셔너리 안에 키가 이미 있으면 거짓 반환
  • 딕셔너리 안에 키가 없으면 새로운 값을 넣고 참을 반환

딕셔너리 안에 키가 있는지 확인하기

1
bool bContain = Dictionary<TKey, TValue>.ContainsKey(TKey, key);

딕셔너리 안에 키가 있으면 참, 없으면 거짓 반환

1
2
3
4
5
Dictionary<string, string> students = new Dictionary<string, string>();
// {("A1", "Bob"),("A2", "Bobby")}

bool bContain1 = students.ContainKey("A1"); //참
bool bContain2 = students.ContainKey("Bob"); //거짓

딕셔너리 안에 밸류가 있는지 확인하기

1
bool bContain = Dictionary<TKey, TValue>.ContainsValue(TKey, key);

딕셔너리 안에 키가 있으면 참, 없으면 거짓 반환

1
2
3
4
5
Dictionary<string, string> students = new Dictionary<string, string>();
// {("A1", "Bob"),("A2", "Bobby")}

bool bContain1 = students.ContainsValue("Bob"); //참
bool bContain2 = students.ContainsValue("Alex"); //거짓

딕셔너리의 모든 요소 삭제하기

딕셔너리의 모든 요소를 삭제

1
Dictionary<TKey, TValue>.Clear();

딕셔너리 안에 있는 요소 삭제

1
bool bRemoved = Dictionary<TKey, TValue>.Remove(TKey, key);

딕셔너리 안에 키가 있으면 요소를 삭제 후 참, 없으면 거짓 반환

1
2
3
4
5
Dictionary<string, string> students = new Dictionary<string, string>();
// {("A1", "Bob"),("A2", "Bobby")}

bool bRemoved1 = students.Remove("A1"); //참
bool bRemoved2 = students.Remove("A3"); //거짓

딕셔너리에서 키와 매핑된 값 가져오기

1
bool bFound = Dictionary<TKey, TValue>.TryGetValue(TKey, key, out TValue value);
  • 딕셔너리 안에 키가 있으면 값을 out 매개변수에 대입하고 참을 반환
  • 딕셔너리 안에 키가 없으면 거짓 반환
1
2
3
4
5
Dictionary<string, string> students = new Dictionary<string, string>();
// {("A1", "Bob"),("A2", "Bobby")}

string value;
bool bFound = students.TryGetValue("A1", out value)

요소 추가/접근법 - []

1
Dictionary<TKey, TValue>[key] = value;
  • 키가 이미 있다면 연결된 값 변경
  • 키가 없다면 키와 값을 새로운 원소로 추가
1
2
3
4
5
6
Dictionary<string, string> students = new Dictionary<string, string>();
// {("A1", "Bob"),("A2", "Bobby")}

students["A1"] = "Tomas"
string student = students["A3"] // throws an exception
students["A2"] = "Jason"
[] 사용
key A1 A2 A3
value Bob -> Tomas Bobby Jason

References
실무 프로그래밍 입문(C#)

부동소수점형의 정밀도 문제

float형은 정밀도 문제가 있다. 아래 코드에서 num1과 num2가 다른데 같다고 취급하고 있다.

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
float num1 = 0.099999999999f;
float num2 = 0.1f;

if(num1 == num2)
{
console.WriteLine("same");
}
}
  • 부동소수점형은 언제나 근사값이다.
  • 비트 수는 정해져 있는데 표현할 숫자가 너무 많다.
  • 예를 들어 0과 1 사이에는 무수히 많은 수가 있어서 정확하게 출력하기 어렵다.
  • 정수는 열거형이라 이러한 문제가 없다.
  • 위의 예시처럼 부동소수점에서 근접한 두 수는 같은 값이 될 수 있다.
  • 돈과 관련된 프로그램에서는 오차가 발생한다.

그럼에도 부동소수점 형을 쓰는 이유는?
CPU에서 자체적으로 지원하는 유일한 실수형으로 계산이 빠르다. 돈과 관련된 경우 아닌 경우에는 다른 곳에서 써도 크게 문제되지 않는다.


부동소수점형의 정밀도 문제를 해결하는 방법

1. 정수로 변환해서 쓰기

더하기, 빼기의 경우

  • 미국 달러의 경우 금액에 100을 곱해서 연산 (e.g. $10.10 + $0.01 = $10.11 -> 1010 + 1 = 1011)
  • 화면에 보여줄 때 100으로 나누고 반올림하여 보여준다.
  • 단, 정수가 표현할 수 있는 범위까지만 표현 가능하다.
    • 32비트 정수(0 ~ 4,294,967,295)에서 문제가 될 수 있다. 64비트 정수(0 ~ 9,223,372,036,853,775,807)는 범위가 커서 크게 문제되지 않지만, 환율계산과 같이 소수점 자리를 계산하는 경우, 소수점 9자리까지 계산해야 한다면 문제가 된다.

2. 문자열로 표현하기

  • 문자열은 무한의 길이를 가지기 때문에 숫자가 아닌 문자열로 저장한다.
  • 두 숫자를 계산할 때 문자열에서 각 자리의 문자를 숫자로 바꾼 뒤 뒤에서부터 한 자리 씩 계산한다.
  • 문제점

    • 받아올림, 받아내림이 번거롭다.
    • 만약 + 연산자를 잘못 사용하면 의도치 않은 결과를 얻게 된다. 예를 들어 두 숫자를 더한 값을 얻고 싶었는데, 컴퓨터가 문자열로 인식하여 문자열을 더해버려서 “10.01” + “0.01” -> “10.010.01”과 같은 값을 얻을 수 있다.
“10.01” + “0.01” [0] [1] [2] [3] [4]
string 1 0 . 0 1
string 0 . 0 1
int 1 0+0=0 . 0+0=0 1+1 =2
결과(string) 10.02

3. decimal 자료형

부동소수점형의 정밀도 문제를 해결하기 위해 c#에 존재하는 자료형

표현범위: ± 1.0 x 10 -28 ~ ± 7.9228 x 1028
정밀도: 28 ~ 29

  • CPU 자체에서 지원하는 형은 아니다.
  • 금융권에서 돈 계산에 쓰기 적합하다.
  • 일부 언어에도 (e.g. java - BigDecimal) 비슷한 해결책이 있다.

decimal 자료형 사용하기

1
2
3
4
5
decimal num1 = 10.1234567778797898989m; //ok
//m 키워드가 없으면 double이고, double형을 decimal에 대입하려 했기 때문에 오류 발생.
decimal num2 = 10.1234567778797898989;
decimal num3 = 10m; //ok
decimal num4 = 10; //ok

접미사 m을 사용한다.

  • 정수일 때는 묵시적 변환을 허용하기 때문에 안붙여도 된다.
  • 부동소수점일 때는 명시적 변환만 허용하기 때문에 반드시 붙여야 한다.

decimal과 다른 자료형 간의 변환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
decimal num1 = 10.1234567778797898989m;
decimal num2 = 10.123456777898989m;
decimal num3 = 10m;

//명시적 변환은 모두 ok
//묵시적 변환은 오류 발생
float num4 = num1; // 컴파일 오류
float num5 = (float)num1; //ok
num2 = num5; //컴파일 오류
num2 = (decimal)num5; //ok

int num6 = num3; //컴파일 오류
int num7 = (int)num3; //ok
num3 = num7; //ok

References
실무 프로그래밍 입문(C#)

컬렉션

  • 동일한 형의 여러 자료를 저장하는 공간
  • 자료구조의 일부
  • 다른 언어에서는 컨테이너라고도 부른다.
배열 컬렉션
지료구조 자료구조
요소의 수 바꿀 수 있음 요소의 수 바꿀 수 없음
유용한 함수 제공 안함 유용한 함수 기본적으로 제공함

컬렉션 결정 시 고려사항

용도에 따라 다양하게 결정하게 된다. 어떤 컬렉션을 이용하는지에 따라 메모리 사용량, 성능이 달라진다.

  • 색인의 종류: 배열처럼 정형화된 색인, 임의의 key값(어떤 자료형이든 ok. 그러나 정형화된 색인이 아닌 것.)
  • 데이터 접근 패턴: 처음부터 끝까지 순회 or 중간에 데이터를 자주 넣고 빼는지 여부

컬렉션의 종류

  • 단순 컬렉션: 길이가 바뀔 수 있는 배열
  • 복잡한 컬렉션: 자유로운 길이 + 다양한 요소 접근 방법(배열의 접근 방법을 쓸 수 없는 데이터도 있기 때문.)

1. 리스트

  • 배열과 거의 비슷
  • 색인(0부터 n)을 통해 데이터에 접근
  • 그러나 배열의 길이(담을 수 있는 최대 요소 수)를 언제든 바꿀 수 있다.
1
List<T> <변수명> = new List<T>();
1
2
List<int> scores = new List<int>();
List<string> names = new List<string>();
  • <T>
    • 어떤 자료형을 담을지 표현한다.
    • 제네릭 프로그래밍의 일부이다.
    • c++에서는 템플릿 프로그래밍이라고도 한다.
  • 리스트를 생성하는 코드
  • 리스트의 길이는 0
  • 배열 사용하는 곳에서는 다 사용하기 좋다.

총용량도 함께 생성하는 리스트 생성

1
List<T> <변수명> = new List<T>(int capacity);
  • 총용량이 capacity인 리스트를 생성하는 코드
  • 또 다른 오버로드 함수가 있다.
1
2
List<int> scores = new List<int>(6);
List<string> names = new List<string>(3);
총용량도 함께 생성하는 리스트 0 1 2 3 4 5 6
scores int int int int int int
names string string string

리스트의 총용량과 길이

1
2
int capacity = list.Capacity; //list는 List<T>
int count = list.Count; //list는 List<T>

List<T>의 현재 총용량과 사용량을 알려준다.

1
2
3
4
5
List<int> scores = new List<int>(3); // {30, 40}
Console.WriteLine($"{scores.Capacity}, {scores.Count}"); // "3, 2"

List<string> names = new List<string>(5); // {"Bob", "Alex", "Bobby"}
Console.WriteLine($"{scores.Capacity}, {scores.Count}"); // "5, 3"

리스트에 데이터 삽입하기

1
Add(T data);
1
2
3
4
5
6
7
8
List<int> scores = new List<int>(6);
List<string> names = new List<string>(3);

scores.Add(10);
scores.Add(30);

names.Add("Bob");
names.Add("Bobby");
리스트에 데이터 삽입 0 1 2 3 4 5 6
scores 10 30
names Bob Bobby

리스트에 데이터 여러 개 삽입하기

배열이나 List<T>가 매배견수가 된다.

1
AddRange(IEnumerable<T> collection);
1
2
3
4
5
6
7
8
9
10
11
12
13
int[] dummy = {10, 20};

List<int> scores = new List<int>(3);
scores.AddRange(dummy);

// 배열이 아닌 다른 리스트가 있을 때 그 리스트를 다른 리스트에 삽입.
List<string> names = new List<string>(5);
names.Add("Bob");
names.Add("Bobby");

List<string> names1 = new List<string>();
// 리스트 생성시 길이가 0이므로 임의의 크기 배열을 자동으로 생성
names1.AddRange(names)
리스트에 데이터 여러개 삽입 0 1 2 3
scores 10 20
names1 Bob Bobby

리스트 중간에 데이터 넣기

1
Insert(int index, T data);

리스트의 index번째에 data를 넣기
잘못된 색인을 넣으면 에러 발생

1
2
3
4
5
List<int> scroes = new List<int>(3); // {30, 40}
scores.int(2, 10);

List<string> names = new List<string>(5); // {"Bob", "Bobby"}
names.Insert(1, "Alex") //이미 요소가 있는 자리에 삽입하게 되면 그 자리를 차지하고, 원래 있던 요소는 뒤로 한 칸 밀린다.
리스트 중간에 데이터 삽입 0 1 2 3
scores 30 40 10
names Bob Bobby -> Alex Bobby

리스트에 해당하는 데이터가 있는지?

해당 데이터가 있으면 참, 아니면 거짓을 반환

1
bool bResult = list.Contains(T data); //list는 List<T>
1
2
3
4
5
6
7
List<int> scores = new List<int>(3); // {10, 30}
bool bResult1 = scores.Contains(40); //false
bool bResult2 = scores.Contains(30); //true

List<int> names = new List<string>(5); // {"Bob", "Bobby"}
bool bResult1 = names.Contains("Bob"); //true
bool bResult1 = names.Contains("bobby"); //false - 대소문자 구분

리스트에 해당하는 데이터가 어디에 있는지?

1. IndexOf

1
int index = list.IndexOf(T data); //list는 List<T>
  • 해당 데이터가 처음으로 나타난 위치의 색인을 반환
  • 없다면 -1을 반환
  • 다양한 오버로드 함수 있음.
1
2
3
4
5
6
7
List<int> scores = new List<int>(3); // {30, 30}
int index1 = scores.IndexOf(40); //-1
int index2 = scores.IndexOf(30); //0

List<string> names = new List<string>(5); // {"Bob", "Bobby"}
int index1 = names.IndexOf("Bob"); //0
int index2 = names.IndexOf("bobby"); //-1 - 대소문자 구분

2. LastIndexOf

1
int index = list.LasstIndexOf(T data); //list는 List<T>
  • 해당 데이터가 마지막으로 나타난 위치의 색인을 반환
  • 없다면 -1을 반환
  • 다양한 오버로드 함수 있음.
1
2
3
4
5
6
7
List<int> scores = new List<int>(3); // {30, 30}
int index1 = scores.LastIndexOf(40); //-1
int index2 = scores.LastIndexOf(30); //1

List<string> names = new List<string>(5); // {"Bob", "Bobby"}
int index1 = names.LastIndexOf("Bob"); //0
int index2 = names.LastIndexOf("bobby"); //-1 - 대소문자 구분

리스트에서 요소 삭제하기

1
bool bSuccess = list.Remove(T data); //list는 List<T>

리스트에 해당하는 data가 있으면 첫번째 찾은 data만 지우고 참을 반환, 없으면 거짓을 반환

1
2
3
4
5
6
7
8
List<int> scores = new List<int>(3); // {10, 30, 40}
bool bSuccess1 = scores.Remove(10); //참
//요소를 삭제하면 삭제한 요소 뒤의 요소가 앞으로 당겨진다.
bool bSuccess2 = scores.Remove(100); //거짓

List<string> names = new List<string>(5); // {"Bob", "Alex", "Bobby"}
bool bSuccess1 = names.Remove("Bob"); //참
bool bSuccess2 = names.Remove("Tom"); //거짓
리스트에서 요소 삭제 0 1 2 3
scores 10 -> 30 40
names Bob Alex Bobby

List<T>의 모든 요소를 삭제하기

1
list.Clear(); //list는 List<T>

List<T>의 요소를 모두 지운다. 단, 용량을 지우는 것은 아니다.

1
2
List<string> names = new List<string>(5);
names.Clear();

리스트의 요소에 접근하기

1
2
3
4
5
// 값 얻어오기
T data = list[index]; //list는 List<T>, index는 정수형

// 값 대입하기
list[index] = <T형 데이터>; //list는 List<T>, index는 정수형

리스트의 index번째 요소에 접근

1
2
3
4
List<int> scores = new List<int>(3); //{10, 30}
scores[2] = 100; //프로그램 실행 시 예외발생
int myScore = score[0]; //myScore: 10
scores[0] = 100; //{100, 30}

리스트에 순차적으로 접근하기

반복문을 이용해서 접근 가능

1
2
3
4
5
6
List<string> names = new List<string>(5); // {"Bob", "Alex", "Bobby"}

for (int i = 0; i < names.Count; ++i)
{
Console.WriteLine($"Name: {names[i]}");
}

리스트에서 배열로 변환하기

List<T>에서 순수한 배열 T[]로 변환하는 함수

  • 더이상 추가나 삭제가 필요없을 때 배열 사용
  • 다른 곳에 데이터를 넘겨줄 때 그 데이터가 배열이라면 리스트를 배열로 변환
  • e.g. List<int> -> int[], List<float> -> float[]
1
T[] array = list.ToArray(); list는 List<T>
1
2
3
4
5
6
List<string> names = new List<string>(5);
names.Add("Bob");
names.Add("Bobby");
names.Add("Alex");

string[] nameArray = names.ToArray();

References
실무 프로그래밍 입문(C#)

문자열 합치기는 임시로 만들고 버려지는 문자열이 많아서 합치는 과정이 느릴 수도 있다.

1
Console.WriteLine("hello" + "give me" + "2" + "dollars!");

연산자의 평가과정에 따라 왼쪽에서 오른쪽으로 문자열을 더해가면서 임시로 생성되는 1~3번과 같은 문자열은 코드에도 보이지 않고, 변수에도 대입되어 있지 않다. 다음 문자열이 더해지기 전까지 임시로 생성되어 다음 문자열이 평가되면 사라진다. 즉 1번이 연산되어 hello + give me의 형태인 hello give me라는 문자열이 임시로 만들어졌다가 2번 hello give me 2가 연산되면 1번은 사라지게 된다.

  1. hello + give me
  2. hello give me + 2
  3. hello give me 2 + dollars!

가비지 컬렉터

  • 새로 만들어진 문자열들은 언젠가 지워져야 함
  • 가비지 컬렉터가 이 역할을 자동으로 해준다. 다만 어느 시점에 지워지는지 알 수 없다.
  • 쓰레기 문자열이 넘쳐나면 성능 저하가 올 수 있다. 어떤 문자열이 쓰이는지 쓰이지 않는지를 확인해야 하기 때문.

문자열 빌더

가비지 컬렉터의 성능 저하와 같은 문제를 줄이기 위해 임시 문자열 수를 줄이고, 문자열을 효율적으로 만들어주는 라이브러리(클래스)

  • 긴 문자열을 담을 수 있는 충분한 공간을 미리 확보
  • 추가되는 문자열로 그 공간을 차례대로 채워 나간다.
  • 모든 것이 준비되면 최종적으로 문자열을 만들어서 반환한다.
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
using System;
using System.Text;

namespace StringBuilderExample
{
class Program
{
static void Main(string[] args)
{
const int CAPACITY = 1000;
StringBuilder builder = new StringBuilder(CAPACITY);
builder.Append("Hello World!");
builder.AppendLine(" Welcome to COMP1500!");
builder.AppendLine("Are you having fun yet?");

Console.WriteLine(builder.ToString());

builder.Insert(12, " Going to insert this here.");

Console.WriteLine(builder.ToString());

builder.Replace(" Going to insert this here.", " And replace this.");

Console.WriteLine(builder.ToString());

builder.Remove(12, 19);

Console.WriteLine(builder.ToString());

builder.Clear();

Console.WriteLine(builder.ToString());
}
}
}

StringBuilder 생성하기

StringBuilder <변수명> = new StringBuilder(int CAPACITY);

  • 파일 제일 위에 라이브러리 추가 - using System.text;
  • 함수 안에서 사용 - StringBuilder builder = new StringBuilder(CAPACITY);

String 추가하기

StringBuilder의 내부 문자열에 문자열을 추가

1
2
3
builder.Append("Hello World!");
builder.AppendLine(" Welcome to COMP1500!");
builder.AppendLine("Are you having fun yet?");
  • 문자열과 줄바꿈 추가 - AppendLine(string text);
  • 문자열만 추가 - Append(string text);;
  • 여러가지 오버로드 함수가 있다.
  • 문자열이 아닌 것도 합칠 수 있다.
1
2
3
StringBuilder builder = new StringBuilder(4096);
builder.AppendLine("Score: " + 10);
builder.Append(3.14f);

배열의 총용량과 현재 사용중인 길이 얻기

1
Console.WriteLine($"Capacity: {builder.Capacity}, Length: {builder.Length}");
  • 각각 내부 배열의 총용량과 길이 값을 가지고 있다.
  • 내부 배열의 총용량 - builder.Capacity;
  • 내부 배열의 현재 사용중인 길이 - builder.Length;

추가 공간 확보

EnsureCapacity(int newCapacity);

1
builder.EnsureCapacity(1024);

stringBuilder의 내부 배열의 총용량을 늘리는 함수. 총용량보다 작은 수를 입력했다고 총용량을 줄이지는 않는다.

최종 문자열 얻어오기

ToString();

1
2
3
StringBuilder builder = new StringBuilder(4096);
//문자열 추가하는 코드 생략
string greetings = builder.ToString();
  • 완성한 최종 문자열을 반환
  • 현재 내부 배열의 사용중인 길이 만큼만 반환
  • 오버로드 함수가 있다.

만약 처음 확보해 둔 공간을 다 쓴다면?

  • 아무문제없다.
  • StringBuilder가 자동적으로 내부 공간을 늘린 뒤(기존 배열 크기의 2배) 모든 데이터를 복사한다.
  • 하지만 복사를 안하는 것이 좋기 때문에 처음부터 충분한 공간을 확보할 수 있도록 한다.
  • 2의 승수로 크기를 잡는 경우가 많다.

StringBuilder 함수

Insert()
Replace()
Remove()
Clear()

Insert()

Insert(int index, string text);

  • StringBuilder의 내부 배열 중간(int index)에 새로운 문자열(string text)를 삽입.
  • 여러 오버로드 함수가 있다.

Replace()

Replace(char old, char new);

모든 old를 new로 바꾼다.

Replace(char old, char new, int start, int count);

start번째부터 start + count번째 사이에 있는 모든 old를 new로 바꾼다. (e.g. builder.Replace(‘P’, ‘B’, 3, 3);)

Remove()

Remove(int start, int length);

start번 째부터 length개 만큼의 문자를 지운다.

Clear()

builder.Clear();

  • 임시 문자열을 제거하는 함수
  • 이 함수를 호출 후 길이를 확인하면 0

StringBuilder vs 문자열 합치기

  • 합칠 문자열이 몇 개 없다면 굳이 StringBuilder를 쓰지 않는다.
  • 대여섯개 이상의 문자열을 합치면 그때 StringBuilder의 사용을 고려한다.

References
실무 프로그래밍 입문(C#)

타입스크립트란?

TypeScript is Typed JavaScript at Any Scale

  • JS에 타입을 부여한 언어
  • JS와 달리 브라우저에서 실행하려면 파일을 변환하는 컴파일 과정이 필요하다.

TypeScript는 JS의 구문이 허용되는, JavaScript의 상위 집합 언어

  • TS는 JS 위의 레이어: JS의 기능을 제공하면서 그 위에 자체 레이어를 추가한다.
  • JS가 가진 런타임 특성을 변화시키지 않는다.
  • TS의 컴파일러가 코드 검사를 마치면 타입 삭제 후 컴파일된 코드를 만들어낸다.

타입 추론

JavaScript가 동작하는 방식을 이해함으로써 TypeScript는 JavaScript 코드를 받아들이면서 타입을 가지는 타입 시스템을 구축할 수 있다.

타입 정의

JavaScript는 다양한 디자인 패턴을 가능하게 하는 동적 언어인데, 몇몇 디자인 패턴은 자동으로 타입을 제공하기 힘들다. 이럴 때는 TS에게 타입이 무엇이 되어야 하는지 명시 가능한 JS언어의 확장을 지원한다. 예를 들어 아래 예시에서 객체의 형태를 명시적으로 사용하기 위해 interface 선언했다.

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

const user = {
name: 'Hayes',
id: 0,
};

타입 구성

객체들을 조합하여 더 크고 복잡한 체계를 만드는 것처럼 TS는 유니언과 제네릭이 있다.

  • 유니언: 타입이 여러 타입 중 하나일 수 있음을 설명한다. e.g. type MyBool = true | false;
  • 제네릭: 타입에 변수를 제공하는 방법이다. 배열이 일반적인 예시이며, 제네릭이 없는 배열은 어떤 것이든 포함할 수 있다. 제네릭이 있는 배열은 배열 안의 값을 설명할 수 있다.

구조적 타입 시스템

TS의 핵심 원칙 중 하나로 타입 검사가 값이 있는 형태에 집중한다는 것이다. 만약 두 객체가 같은 형태를 가지면 같은 것으로 간주된다.


왜 타입스크립트를 써야하는가?

Javascript는 예기치 않은 동작을 유발하게 하는 몇 가지 문제점을 가지고 있다. 예를 들어 ==을 사용하면 인수를 강제변환한다거나(e.g. ‘’ == 0 => 참이다) 존재하지 않는 객체의 프로퍼티 값에 접근하려고 했을 때 접근을 허용하는 점이다. c나 c++ 같은 컴파일 언어는 실행 전 컴파일 단계에서 오류를 미리 확인하고 고칠 수 있다. 이렇게 프로그램을 실행시키지 않으면서 코드의 오류를 검출하는 것을 정적 검사라 하고, 어떤 것이 오류인지와 어떤 것이 연산 되는 값에 기인하지 않음을 정하는 것을 정적 타입 검사라 한다.

그러나 JavaScript는 인터프리터 언어이기 때문에 컴파일 단계가 없고, 곧바로 실행되기 때문에 실제로 실행하기 전까지 코드의 문제를 잡아낼 수 없다. 하지만 TypeScript는 정적 타입 검사자로써 프로그램 실행 전 값의 종류를 기반으로 프로그램의 오류를 찾아낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}

API 호출을 통해 위와 같은 데이터를 response로 받아왔고, const user = {}와 같은 변수에 저장한다. 그리고, name, email, address를 화면에 출력하고자 한다면, user.name, user.email, user.address와 같은 방식으로 사용될 것이다.

1
2
3
4
5
6
7
8
// user.name
Leanne Graham

//user.email
Sincere@april.biz

//user.address
[object Object]

그런데 다른 값들과 달리 address는 값이 객체이기 때문에 기대했던 값이 Kulas Light라면 이 값 대신 [object Object]가 반환된다. 또는 작성하는 과정에서 오탈자가 있어서 user.adress와 같이 사용했다면 undefined를 반환하게 된다. 이처럼 내가 작성한 코드의 결과가 제대로 출력되는지 여부는 화면에 표시된 데이터를 보고 나서야 알 수 있다.


타입스크립트를 쓰면 좋은 점

  • 에러의 사전 방지
    위 예시는 브라우저 실행 후 화면에 그려지고 나서야 어떠한 에러가 있는지 알 수 있었지만, TypeScript를 쓰면 브라우저 실행 전에 코드 상에서 미리 에러를 잡을 수 있고, 의도치 않은 값을 반환하지 않도록 수정할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function add(a: number, b: number): number {
    return a + b;
    }

    //type으로 지정된 number 대신 스트링 '20'을 넣었기 때문에 아래와 같은 에러 표시
    //Argument of type 'string' is not assignable to parameter of type 'number'.
    add(10, '20');

    //브라우저 실행 전에 에러를 확인하고 올바르게 고칠 수 있다.
    add(10, 20);
  • 코드 가이드 및 자동 완성
    일반 javaScript 코드였다면 toLocaleString()과 같은 api를 사용하고자 할 때 toLocaleString()을 한 자씩 입력해야 한다. typeScript는 vscode의 IntelliSense를 이용해서 toL까지만 입력해도 toLocaleString()를 리스트에서 확인할 수 있고 이를 선택시 바로 자동완성 되어 입력이 된다. IntelliSense는 코드 완성, 매개 변수 정보, 빠른 정보 및 구성원 목록을 비롯한 다양한 코드 편집 기능을 나타내는 일반적인 용어를 말한다. IntelliSense 기능은 때때로 “코드 완성”, “콘텐츠 지원” 및 “코드 힌트”와 같은 다른 이름으로 호출된다.

1
2
3
4
5
6
7
8
function add(a: number, b: number): number {
return a + b;
}

var result = add(10, 20);

//result.까지만 입력해도 자동완성으로 api 사용가능
result.toLocaleString();

JS에서 JSDoc과 @ts-check로 ts처럼 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @ts-check

/**
*
* @param {number} a 첫번째 숫자
* @param {number} b 두번째 숫자
* @returns
*/

function sum(a, b) {
return a + b;
}

sum(10, 20);

위의 코드처럼 사용하면 javaScript에서도 TypeScript처럼 브라우저 실행 전에 타입체크를 할 수 있다.


References
타입스크립트 입문 - 기초부터 실전까지
타입스크립트 핸드북
TS for the New Programmer
TS for the JS Programmers

재귀 함수

함수 A가 매개변수만 바꾸어 다시 함수 A(자기자신)을 호출하는 방법으로 구현

재귀 함수의 구성요소

종료조건

  • 더이상 재귀 함수를 호출하지 않고 값을 반환하는 조건
  • 매우 간단히 함수의 반환 값을 찾을 수 있는 경우
  • 종료조건이 없으면 함수를 무한히 재귀적으로 호출한다.

재귀적 함수 호출

  • 종료조건이 아닌 경우
  • 함수의 인자를 바꿔서 스스로를 다시 호출
  • 이때 함수의 인자는 현재 문제보다 작은 문제를 대표해야 한다. (매개변수를 바꿔서 지금 현재의 문제보다 조금 작은 범위의 문제에서 함수를 호출한다. 즉 함수가 바뀌는 것이 아니라 함수의 대상의 되는 문제의 범위가 작아지는 것이다.)
  • 즉 동일한 동작을 보다 작은 문제에 적용한다.
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
//종료조건이 있는 경우
static uint SumRecursive(uint num)
{
//종료조건
if (num == 0)
{
return 0;
}
else
{
//재귀적 함수 호출
return SumRecursive(num - 1) + num;
}
}

//종료조건이 없는 경우
static uint SumRecursive(uint num)
{
return SumRecursive(num - 1) + num;
}

static void Main(string[] args)
{
Console.WriteLine(SumRecursive(3));
}

반복문 vs 재귀함수

모든 재귀 함수는 반복문으로 해결 가능하다. 간단한 문제의 경우 반복문으로 해결하는 것이 더 편하지만 복잡한 문제일수록 재귀 함수를 사용하는 것이 더 편하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//반복문 - 피보나치 수열
public static int FibonacciIterative(uint number)
{
uint[] list = new uint[number + 1];

list[0] = 0;
list[1] = 1;

for (uint i = 2; i <=number; ++i)
{
list[i] = list[i - 2] + list[i - 1];
}
return list[number]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//재귀 함수 - 피보나치 수열
public static int FibonacciRecursive(uint number)
{
if (number == 0)
{
return 0;
}

if (number == 1)
{
return 1;
}

return FibonacciRecursive(number - 2) + FibonacciRecursive(number - 1);
}

폴더의 목록을 가져오는 처리가 필요하다면 반복문보다 재귀문으로 표현하는 것이 더욱 쉽다. 폴더의 목록을 가져오는 함수(의사코드)는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// GetDirectoryNames(path): path 안에 있는 모든 폴더의 이름을 가져오는 함수
// GetFilesNames(path): path 안에 있는 모든 파일의 이름을 가져오는 함수

static string[] GetFileNamesRecursive(string path)
{
string[] filesNames;
string[] directoryNames = GetDitectoryNames(path);
for (int i = 0; i < directoryNames.Length; ++i)
{
filesNames += GetFileNamesRecursive(directoryNames[i]);
}
filesNames += GetFilesNames(path);
return filesNames;
}

재귀함수의 사용

수학적 귀납법이기 때문에 종료 조건에 반드시 예상되는 값이 반환될 것을 가정하고 신뢰하며, 그 후의 수는 종료 조건에 기초하여 값을 계산할 수 있도록 한다.

재귀 함수의 장점 재귀 함수의 단점
개념상(이론상)으로 훌륭하다. 효율성이 떨어진다.
증명이 가능하다. 함수 호출 깊이에 제한이 있기 때문에 스택오버플로우가 발생할 수 있다.
캐싱 없이 간단한 반복문으로 작성 가능한 문제는 반복문을 사용하지만 그 외의 경우에는 설계와 이해가 용이한 재귀 함수로 작성한다. 재귀 함수로 작성했는데 함수 호출의 최대 깊이를 확정할 수 없거나 성능상의 문제를 발견했다면 반복문으로 리팩토링한다.

수학적 귀납법 예시

모든 자연수 n에 대하여 아래 조건이 항상 성립하는지 증명하라.

0 + 1 + 2 + … + n =

  1. n이 0일 때 위의 조건이 성립하는지 본다.(종료조건)= 0
  2. 자연수 0부터 k까지 합이 아래 조건에 성립한다고 가정한다.
  3. 0부터 k까지 합 다음 자연수인 (k+1)의 합도 이 공식을 만족하는지 증명한다.
    (0 + 1 + 2 + … + k) + (k + 1) =

피보나치의 수열

제 0항은 0, 제 1항은 1, 그 뒤의 모든 항은 바로 앞 두 항의 합인 수열
0 1 1 2 3 5 8 13 21 34 55…

수학적 정의

  • F0 = 0,
  • F1 = 1,
  • Fn = Fn-1 + Fn-2(n > 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static int FibonacciRecursive(uint number)
{
if (number == 0)
{
return 0;
}

if (number == 1)
{
return 1;
}

return FibonacciRecursive(number - 2) + FibonacciRecursive(number - 1);
}

//FibonacciRecursive 함수 호출
static void Main(string[] args)
{
FibonacciRecursive(10);
}

하노이의 탑

64개의 황금 원판이 있다. 브리흐마의 규칙에 따르면 한 번에 원판 하나씩만 옮길 수 있고, 작은 원판 위에 그보다 큰 원판은 옮길 수 없다.

  1. 막대 3개가 있고, 한 막대에 n개의 원판이 있다.
    • n개의 원판의 상위 n-1개를 다른 막대에 옮길 수 있다고 가정한다.
      • 상위 n-1개의 원판을 중간 막대로 옮긴다.
      • 마지막 n번째 원판은 목적지 막대에 옮긴다.
      • 중간 막대에 있던 상위 n-1개의 원판을 목적지 막대에 옮긴다.
  2. 막대 3개가 있고, 한 막대에 n-1개의 원판이 있다.
    • n-1개의 원판에서 상위 n-2개를 다른 막대에 옮길 수 있다고 가정한다.
      • 상위 n-2개의 원판을 중간 막대로 옮긴다.
      • 마지막 n-1번째 원판은 목적지 막대에 옮긴다.
      • 중간 막대에 있던 n-2개의 원판을 목적지 막대에 옮긴다.
  3. n-3, n-4, …을 거쳐서 마지막 n-(n-2)개까지 도달한다. n-(n-2)n - n + 2로 전개되므로 2가 된다.
  4. 막대 3개가 있고, 한 막대에 2개의 원판이 있다.
    • 2개의 원판에서 상위 1개를 다른 막대에 옮길 수 있다고 가정한다.
      • 1개의 원판을 중간 막대로 옮긴다.
      • 마지막 2번째 원판은 목적지 막대에 옮긴다.
      • 중간 막대에 있던 1개의 원판을 목적지 막대에 옮긴다.
  5. 막대 3개가 있고, 한 막대에 1개의 원판이 있다.
    • 1개의 원판을 다른 막대에 옮길 수 있다.
    • 1개의 원판을 목적지 막대로 옮긴다. 즉 종료 조건이 참이므로 위의 모든 과정도 참이 된다.

랜덤 클래스와 개체 생성

Random random = new Random();

  • Ramdom: 클래스 (여러 개의 함수가 뭉쳐있는 집합)
  • random: Random형 개체의 이름(변수, 다른 단어 대체 가능)
  • Random형 개체를 생성 (개체: 클래스 안에 있는 함수를 사용하기 위해 필요)

랜덤 생성기를 만들어 random이라는 변수에 대입한다.

랜덤 수 생성

<변수명>.Next(<최댓값>);
0 이상 최댓값 미만의 수 중 하나를 무작위로 뽑아준다. [0, 최댓값)

<변수명>.Next(<최솟값>,<최댓값>);
최솟값 이상 최댓값 미만의 수 중 하나를 무작위로 뽑아준다. [최솟값, 최댓값)

1
2
3
//Random random = new Random(); 반드시 필요한 코드
int number1 = random.Next(3);
int number2 = random.Next(1, 10);

의사 랜덤

  • 대부분 언어에서 지원하는 랜덤은 진정한 랜덤이 아니다.
  • 랜덤처럼 보이기 위해 시드(seed)값을 초기 입력값으로 하여 알고리즘을 통해 랜덤수를 만들어 내는 함수
  • 그 결과는 다시 랜덤의 입력값이 된다.
  • 시드 값이 같으면 언제나 생성된 랜덤수의 순서가 동일하다.
  • 랜덤 함수는 최초(시드)를 제외하고, 그 다음부터는 본인 스스로가 반환한 값을 다시 입력으로 사용한다. (e.g. 1이 들어와서 10을 반환, 들어온 10은 34로 반환 )

References
실무 프로그래밍 입문(C#)