저번 글에서 에러 처리의 정의를 짚어보고, Fetch API를 사용하는 예를 들어 자바스크립트 환경에서 발생하는 일반적인 비동기 에러 처리 흐름을 살펴보았습니다. 단순히 try/catch + throw
를 사용하는 방식 말고, 에러 여부를 구분할 수 있는 속성을 가진 객체를 리턴하는 방식과 Golang 스타일을 약간 차용하여 [Error | null, Success | null]
의 형태가 항상 유지되는 튜플을 리턴하는 방식도 알아보았습니다.
이번 글에서는 추가로 두 가지 개념을 더 설명하겠습니다. 기회가 된다면 그 다음 시간에는 실제 리액트 애플리케이션에서 에러 처리를 하는 시나리오를 예제와 함께 소개해 보고 싶습니다.
타입스크립트로 커스텀 에러 클래스 정의하기
저번 글에서 catch
블록에 잡히는 에러 객체의 타입을 보장할 수 없어서 (개인적으로) 불안하다는 이야기를 했습니다. 특히 타입스크립트 환경에서는 더욱 답답하겠죠. 간단한 해결책은 있습니다. 직접 에러 객체를 만들고, 타입 가드를 활용하여 에러를 구분해낸다면, 그 다음부터는 타입스크립트 컴파일러의 도움을 받을 수 있습니다.
가장 쉽게 커스텀 에러를 만드는 방법은 Error
객체를 상속한 클래스를 만들고 그 에러의 인스턴스를 만들어 쓰는겁니다. 이전 예제에서 class HTTPError extends Error {}
구문을 보셨을겁니다. 이 에러에 조금 더 살을 붙여보겠습니다.
이렇게 instanceof
를 통과하면 해당 에러 객체의 속성을 좀 더 안전하게 활용할 수 있습니다. 물론 어떻게든 예상하지 못한 에러를 처리하긴 해야겠죠.
그런데 단순히 에러 객체를 만들어서 메세지만 전달하는 수준이 아니라, 에러를 처리하는 로직을 커스텀 에러가 가지도록 만들면 어떨까요? 어차피 HTTPError
도 하나의 클래스니까 여기다 메서드를 정의하면 써먹을 수 있지 않을까요?
실제로 저 코드를 타입스크립트 환경에서 실행해 보시면 showAlert
이라는 속성이 undefined
라고 나오면서 오류가 발생합니다. 타입스크립트의 구현상 한계 때문에 발생하는 것으로 2.1 버전부터 적용된 변경점이라고 합니다.
문서에서 소개하는 대로 클래스의 생성자에서 Object.setPropertyOf
를 호출해주면 잘 동작합니다만, IE10 이하에서는 동작하지 않는다고 합니다. (이것도 방법이 있긴 합니다만 굳이 고려하지 않겠습니다. 참고 자료에 링크해놓은 ts-custom-error
패키지의 소스코드를 참고하시기 바랍니다.)
위의 CustomError
클래스를 상속해서 새로운 커스텀 에러를 만들어 활용할 수 있습니다. 만약에 특정 에러에 따라 기본 동작이 있고, 경우에 따라 추가적인 동작을 실행하고 싶다면 적당한 에러 핸들러 함수를 만들어주고 인자로 에러 객체를 넘겨주면 되겠네요.
커스텀 에러를 여러개 정의했고, 하나의 try/catch
블록에서 여러 타입의 에러 핸들러를 한번의 인자 전달로 처리하고 싶어졌으니 pipeErrorHandlers
같은 함수를 만들어보죠.
어떤 핸들러에서 내가 원하는 에러 처리가 되었으면 뒤의 핸들러에게 처리를 맡길 필요가 없으니까 각각의 핸들러는 타입 가드에 따라 { moveNextHandler: boolean }
을 반환하도록 만들었습니다.
이런 방식이라면 그나마 타입스크립트의 자동완성의 도움을 받아 커스텀 에러 클래스를 활용할 수 있습니다. catch
블록이 거대해지는건 개인적으로 마음에 들진 않는데, 더 좋은 방안은 언제나 댓글로 알려주시면 좋겠습니다. 🙇
Result(혹은 Either) 모나드
’모나드란 무엇인가?’ 같은 거창한 이야기는 이 글에서 다루지 않겠습니다. 간단하게(?) 값을 담아둘 수 있고, 특정 규칙으로 통제할 수 있으며, 맵 함수(매퍼-Mapper-라고 부르곘습니다)를 통해 일련의 계산 과정을 서술하는 타입 이라 정의하겠습니다. 이번에 소개할 Either
모나드는 “에러일 수도 있고 아닐 수도 있는 값을 담아두고 서술된 연산 과정에 따라 계산을 하되, 계산 결과에 따라 에러인 경우 값을 꺼낼 때 별도의 처리를 하는” 역할을 합니다.
Either
라고 불리는 이유는 올바른 값 인 경우에는 right
와 (영어로 ‘맞다’ 라고 할 때 그 right입니다), (에러인 경우 무조건 right의 반대니까) left
두개의 값으로 구성되어 있기 때문이라고 합니다.
예제를 작성하기 위해 자료 조사를 더 하다 보니 몇몇 글에서는 Result
라고 표현하더군요. 좀 더 직관적으로 들리기 때문에 앞으로는 Result
라고 하겠습니다. 스위프트에서도 Result
라고 합니다.
Result
에는 Success
와 Failure
두 종류의 타입이 들어올 수 있습니다. 간단한 코드로 사용 예를 적어보겠습니다.
여기까진 별로 특별할 게 없어 보입니다. 성공 실패에 따라 비슷한데 다른 객체를 내보내는 것은 지난 시간에 Golang스타일 같은거 아닌가? 싶을수도 있죠. Result
모나드는 이 데이터를 쓰는 방법이 다릅니다.
- 덕분에 에러가 발생하는 경우를 고려하는 것 보다 핵심 로직을 작성하고 원하는대로 합성하는데 집중할 수 있습니다.
- 여기서는 다루지 않겠지만, 혹시 있을지 모르는
null
,undefined
값을 안전하게 처리하는데도 사용할 수 있습니다. 빈 값을 체크하는 로직을 코드 중간중간에 넣느라 코드가 지저분해지는 것 보다 내가 처리하고싶은 로직에 집중할 수 있죠. - 덕분에 에러를 다른 코드로 퍼트리지 않고 더 안전한 후처리를 보장할 수 있게 되었습니다.
try/catch
블록을 따로 보느라 뛰어다녀야 하지 않아도 되는 것은 덤이고요.
Result
에는 map
, get
말고 다른 함수를 구현할 수도 있습니다. 보통 추가하자면 flatMap
을 씁니다. 보통 매퍼가 일반 값이 아니라 또 Result
모나드를 리턴하는 연산(실패할 수도 있는 연산)을 하는 경우 모나드가 중첩되지 않도록 할 때 사용합니다.
Result
구현체는 아래의 codesandbox 예제로 살펴보겠습니다. Either
클래스가 어떻게 만들어져있는지 소스코드를 읽어보시고, helloUser.ts
파일의 동작을 살펴보시면 됩니다.
이전에 작성해둔거라 Either
라고 되어있지만 위에서 설명했다시피 Either.left === Result.failure
/ Either.right === Result.success
로 바꿔서 보시면 됩니다.
codesandbox의 시나리오대로라면 반반 확률로 실패를 하고 에러를 뿜게 되는데, API서버에 요청하는 역할을 하는 함수가 먼저 Result
객체를 리턴합니다. 그리고 View 에 맞게 데이터를 가공해주는 역할을 하는 함수는 정해진 흐름에 따라 데이터를 가공하고, View에서는 에러가 발생했다고 빈 화면이 뜨는 대신 안전하게 처리된 메세지를 볼 수 있게 되었습니다.
여태까지 소개한 방법 중에 저는 어떤 방법을 쓰냐구요? 처음엔 Golang 방식을 도입해보려 했다가 지금은 커스텀 에러를 정의하고 올바른 핸들러를 꼭 정의하도록 강제하는 방식을 취하고 있습니다. Either
모나드는 작년에 다른 프로젝트에서 활용했었습니다. 진리의 케바케인 셈이죠.
지금까지 보여드린 정도로는 “어떻게 에러 처리를 할 것인지” 설명이 많이 부족하리라 생각되지만, 저 역시도 공부하는 입장인데다 언제나 더 나은 개념 혹은 활용 방법이 있으리라 믿으면서 끊임없이 수정 가능하기 쉬운 코드를 작성하기 위해 노력하고 있습니다.
이 글을 읽으신 분들이 조금이나마 아이디어를 얻으셨으면 좋곘습니다. 더 좋은 방법 있으면 댓글에 남겨주시거나 공유 하시면서 소개해주시면 더 좋겠네요 😃
참고 자료
- GitHub - adriengibrat/ts-custom-error: Extend native Error to create custom errors
- 함수형 자바스크립트(루이스 아텐시오 저)
- Advanced functional programming in TypeScript: functional exceptions - codewithstyle.info
- Error handling in Typescript | reddit
- Optional, throws, Result, 그리고 async/await - NSHipster - 스위프트 관련 저널이지만 여기 소개된 개념을 이해하시면 제가 적은 내용보다 더 많은 것을 얻으실 수 있습니다