반응형
- 타입스크립트는 런타임에 발생할 수 있는 예외를 컴파일 타임에 잡기 위해 최선을 다한다.
- 그럼에도 런타임에 발생하는 네트워크 장애, 사용자 입력 파싱 에러, 스택 오버플로, 메모리 부족 에러와 같은 에러를 모두 막을 수는 없다.
- 타입스크립트는 타입 시스템을 이용해 이러한 런타임 에러를 표현하고 처리하는 패턴을 제공한다.
- null 반환
- 예외 던지기
- 예외 반환
- Option 타입
- 이 중 어떤 에러 처리 기법을 사용할지는 프로그래머의 이도와 응용 프로그램의 종류에 따라 달라진다. 따라서 각각의 장단점을 잘 비교해 선택해야 한다.
1. null 반환
- (장점) 에러를 처리하는 가장 간단한 방법이다.
- 예를 들어, 사용자가 생일을 입력하는 프로그램이 있을 때 유효한 내용을 입력하면 Date가 반환되고, 아니면 null이 반환되도록 구현할 수 있다.
- (단점) 문제가 생긴 원인을 알 수 없고, 모든 연산에서 null을 확인해야 하므로 연산을 연결할 때 복잡해진다.
// 1. null 반환하기
function ask() {
return prompt('When is your birthday?')
}
// 사용자의 입력을 받아 Date로 변환해서 반환
function parse(birthday: string | null):Date | null {
if (!birthday) return null
let date = new Date(birthday)
if (!isValid(date)) {
return null
}
return date
}
function isValid(date: Date) {
return Object.prototype.toString.call(date) === '[object Date]'
&& !Number.isNaN(date.getTime())
}
let date = parse(ask())
// date가 null인 경우
if (date) {
console.info(date.toISOString())
} else {
console.error('Error parsing date for some reason')
}
2. 예외 던지기
- 문제가 발생하면 null 반환 대신 예외를 던지는 것이 좋다.
- 각 상황에 해당하는 에러를 발생시키고, try-catch문을 통해 이를 처리한다.
- (장점) 커스텀 에러를 이용하면 어떤 문제가 생겼는지와 문제가 생긴 원인을 알 수 있다.
// 2. 예외 던지기
function parse2(birthday: string): Date {
let date = new Date(birthday)
if (!isValid(date)) {
throw new RangeError('Enter a date in the form YYYY/MM/DD')
}
return date
}
try {
let date = parse2(ask())
console.info(date.toISOString())
} catch(e) {
if (e instanceof RangeError) {
console.error(e.message)
} else {
throw e
}
}
// 커스텀 에러 타입
class InvalidDateFormatError extends RangeError {}
class DateIsInTheFutureError extends RangeError {}
function parse(birthday: string): Date {
let date = new Date(birthday)
if (!isValid(date)) {
throw new InvalidDateFormatError('Enter a date in the form YYYY/MM/DD')
}
if (date.getTime() > Date.now()) {
throw new DateIsInTheFutureError('Are you a timelord?')
}
return date
}
// 사용
try {
let date = parse(ask())
console.info(date.toISOString())
} catch (e) {
if (e instanceof InvalidDateFormatError) {
console.error(e.message)
} else if (e instanceof DateIsInTheFutureError) {
console.info(e.message)
} else {
throw e
}
}
3. 예외 반환
- 유니온 타입을 이용해 throws 문을 비슷하게 흉내낼 수 있다. throws문은 메서드가 어떤 종류의 런타임 예외를 발생시킬 수 있는지 알려주는 문이다.
- 각 상황에 해당하는 에러를 반환하고, 반환값을 기준으로 에러를 처리한다.
- (장점) 코드를 사용하는 개발자에게 성공과 에러 상황을 모두 처리하도록 알려줄 수 있다.
- 예를 들어 parse 함수는 birthday라는 string 타입의 매개변수를 받아
- 파싱에 성공할 경우 Date를
- 유효하지 않은 Date인 경우 InvalidDateFormateError를
- 현재 날짜보다 이후인 날짜를 입력한 경우 DateIsInTheFutureError를 반환한다.
그리고, 함수 사용자는 이 반환값을 기준으로 각각의 에러를 모두 처리하거나 다시 던져야 한다.
// parse 함수에서 발생할 수 있는 예외를 나열
function parse(
birthday: string
): Date | InvalidDateFormatError | DateIsInTheFutureError {
let date = new Date(birthday);
if (!isValid(date)) {
return new InvalidDateFormatError("Enter a date in the form YYYY/MM/DD");
}
if (date.getTime() > Date.now()) {
return new DateIsInTheFutureError("Are you a timelord?");
}
return date;
}
// 해당 함수 사용자는 세 가지 상황(파싱 성공, InvalidDateFormatError, DateIsInTheFutureError)을 모두 처리해야 하고.
// 아니면 컴파일 타임에 TypeError가 발생한다.
let result = parse(ask())
if (result instanceof InvalidDateFormatError) {
console.error(result.message)
} else if (result instanceof DateIsInTheFutureError) {
console.info(result.message)
} else {
console.info(result.toISOString())
// 또는 아래와 같이 한 번에 처리할 수도 있다.
let result = parse4(ask()); // 날짜 또는 에러 반환
if (result instanceof Error) {
console.error(result.message);
} else {
console.info(result.toISOString());
}
4. Option 타입
- 에러가 발생할 수 있는 계산에 여러 연산을 연쇄적으로 수행할 수 있도록 하는 특수 목적 데이터 타입을 활용한다.
- 가장 많이 사용하는 타입은 Try, Option, Either 타입이 있다. 이는 자바스크립트가 기본으로 제공하지 않으므로 npm에서 찾아 설치하거나 직접 구현해야 한다.
- 어떤 특정 값을 반환하는 대신 값을 포함하거나 포함하지 않을 수 있는 컨테이너를 반환하는 것이 Option의 특징이다.
- 값이 없더라도 연쇄적으로 연산을 수행할 수 있도록 한다. 값을 포함할 수 있는 자료구조로 이를 구현할 수 있다.
- (장점)
- 타입 안전성을 제공한다.
- 타입 시스템을 통해 해당 연산이 실패할 수 있음을 사용자에게 알려줄 수 있다.
- (단점)
- None으로 실패를 표현하기 때문에 무엇이 왜 실패했는지 자세히 알 수 없다.
- Option을 사용하지 않는 다른 코드와는 호환되지 않는다.
- 예를 들어, string 타입의 birthday를 인수로 받는 parseFn을 살펴보자. date가 유효하지 않은 경우에도 parseFn이 []를 반환하기 때문에 이후에 map, forEach 메서드를 호출해도 에러가 나지 않는다.
// 4. Option 타입 사용
function parseFn(birthday: string): Date[] {
let date = new Date(birthday);
// date가 유효하지 않은 경우에도 []를 리턴해 연산 시 에러가 나지 않도록 함.
if (!isValid(date)) {
return [];
}
return [date];
}
let dateResult = parseFn(ask());
dateResult
.map((date) => date.toISOString())
.forEach((formattedDate) => console.info(formattedDate));
- prompt에 사용자가 입력 했을 때 실패할 수 있다고 가정하자. 사용자가 생일 입력을 취소하면 null을 반환할 것인데, 이때 또 다른 Opiton을 이용해 이 상황을 처리할 수 있다.
- 아래의 경우 Date[][]로 map을 연산해 에러가 나는데, 이는 평탄화(flatten)을 통해 해결할 수 있다.
// prompt가 실패하는 경우 처리
function ask() {
let result = prompt('When is your birthday?')
if (result === null) {
return []
}
return [result]
}
ask()
.map(parse)
.map((date) => date.toISOString()) // 속성 'toISOString'은 'Date[]' 타입에 존재하지 않음
.forEach((date) => console.info(date));
- 이제 배열 대신 Option 컨테이너를 사용해 이를 구현해보자. 조금 복잡할 수 있지만 천천히 따라가볼 예정이다.
- 컨테이너는 1. 대상 값을 이용해 연산을 수행하는 방법과 2. 그 결과를 얻어내는 방법을 보여준다. 컨테이너를 구현하고 나면 아래와 같이 사용할 수 있게 된다. 지금은 null이나 undefined일 수 있는 값에 처리를 해 연쇄적으로 연산을 수행할 수 있다는 정도로 이해하면 된다.
ask()
.flatMap(parse)
.flatMap((date) => new Some(date.toISOString()))
.flatMap(date => new Some('Date is', date))
.getOrElse('Error parsing date for some reason')
- 먼저 Option 타입을 정의한다.
- Option은 Some<T>와 None이 구현하게 될 인터페이스이다. 이 두 클래스는 모두 Option의 한 형태가 된다. Some<T>는 T라는 값을 포함하는 Option(성공한 경우)이고, None은 값이 없는 Option(실패한 경우)을 가리킨다.
- Option은 타입이면서 함수이다. 타입 관점에서는 Some과 None의 슈퍼타입을 뜻한다. 함수 관점에서는 Opiton 타입의 새 값을 만드는 기능을 뜻한다.
// Option과 하위 타입 간략하게 정의해보기
// Option<T>는 Some<T>와 None이 공유하는 인터페이스
interface Option<T> {}
// 연산에 성공해 값이 만들어진 상황.
class Some<T> implements Option<T> {
constructor(private value: T) {}
}
// 연산이 실패한 상황. 값을 담고 있지 않다.
class None implements Option<never> {}
<Option으로 할 수 있는 연산 정의하기>
* flatMap : Option에 연산을 연쇄적으로 수행
* getOrElse : Option에서 값을 가져옴
- Option 인터페이스에 이 연산을 정의해보자. 이를 위해 Some<T>와 None에서 구체적인 코드를 구현해야 한다.
// Option 데이터 컨테이너 타입 사용하기
interface Option<T> {
// Option에 연산을 연쇄적으로 수행
flatMap<U>(f: (value: T) => Option<U>): Option<U>
// Option에서 값 읽기
getOrElse(value: T): T
}
class Some<T> implements Option<T> {
constructor(private value:T) {}
flatMap<U>(f: (value: T) => Option<U>): Option<U> {
return f(this.value) // T를 인수로 f 호출한 반환값
}
getOrElse(): T {
return this.value // Option 안의 값을 반환
}
}
class None implements Option<never> {
flatMap<U>(): Option<U> {
return this // None 반환
}
getOrElse<U>(value: U): U {
return value // 매개변수 값 그대로 반환
}
}
- 위 코드는 기본적인 구현이므로 좀 더 flatMap의 반환값을 명시적으로 나타낼 수 있다.
- None의 매핑 결과는 항상 None이고, Some<T>의 결과는 f 호출 결과에 따라 값이 있다면 Some<T>, 없다면 None이 된다. 이를 바탕으로 flatMap을 더 구체적인 타입을 제공하도록 시그니처를 오버로드 할 수 있다.
// flatMap이 더 구체적인 타입을 가지도록 시그니처 오버로드
interface Option<T> {
flatMap<U>(f: (value: T) => None): None;
flatMap<U>(f: (value: T) => Option<U>): Option<U>;
getOrElse(value: T): T;
}
class Some<T> implements Option<T> {
constructor(private value: T) {}
flatMap<U>(f: (value: T) => None): None;
flatMap<U>(f: (value: T) => Some<U>): Some<U>;
flatMap<U>(f: (value: T) => Option<U>): Option<U> {
return f(this.value);
}
getOrElse(): T {
return this.value;
}
}
class None implements Option<never> {
flatMap(): None {
return this;
}
getOrElse<U>(value: U): U {
return value;
}
}
반응형
'FrontEnd > TypeScript' 카테고리의 다른 글
[TypeScript] 상수 선언에 왜 enum 대신 as const를 사용했나요? (0) | 2023.11.14 |
---|---|
[TypeScript] 비동기 프로그래밍, 동시성과 병렬성 (0) | 2023.06.07 |
[TypeScript] 가변성(슈퍼타입/서브타입 파악하기) & 할당성 & 타입 넓히기 (0) | 2023.06.03 |
[TypeScript] 함수의 타입 (0) | 2023.06.03 |
[TypeScript] 클래스와 인터페이스 (0) | 2023.05.31 |