타입스크립트 기초 - 24

Stulta Amiko·2022년 8월 22일
0

타입스크립트 기초

목록 보기
24/24
post-thumbnail

Maybe 테스트

현실적인 상황에서 Maybe 모나드를 사용하는 예를 들면
웹서버는 HTML을 서버에서 만들어보내는 경우와
데이터를 JSON 포맷으로 보내고 브라우저쪽에서 데이터를 바탕으로 동적으로 생성하게 하는 API 서버로 구분할 수 있다.

웹브라우저에서 API 서버 데이터를 가져올때는 fetch 라는 함수를 사용한다.
fetch 함수는 웹 브라우저에서 기본으로 제공하지만 node.js 에서는 그렇지 않다.

fetch 함수는 다음처럼 문자열로 된 URL을 입력 매개변수로 호출하면 Promise 객체를 반환한다.

fetch(url: string): Promise

fetch가 반환한 Promise 객체는 then 메서드를 호출해 얻은 응답 객체의 text,blob,json과 같은 메서드를 호출해 실제 데이터를 얻을 수 있다.

다음 코드는 JSON으로 포맷된 글을 fetch 함수로 가져오는 코드이다.

코드를 쓰려고했는데...
책에있는 node-fetch의 버전이 낮은건지 현재 설치된 버전이랑 호환이 안되는지 node-fetch의 많은사람들이 빠지는 문제에 빠지게되었다.

그래서 Maybe 모나드의 테스트 코드는 넘어가기로 했다.
무엇보다 모나드의 이해를 위해 fetch를 이용하는것인데
fetch를 뜯어보고있으면 의도되지않은 방향으로 나가는것 이므로 다음으로 넘어가기로 결정했다.

그리고 무엇보다 하루동안이나 스택오버플로우랑 깃허브를 뒤지고 진짜 별에별 블로그를 다 뒤져봤는데도 결국 똑같은 문제가 발생한다.

Validation 모나드 이해와 구현

Validation 모나드란?

데이터 유무의 따라 코드가 적절하게 동작하는게 Maybe 모나드였다.
데이터가 있는데 그 데이터가 유효한지 판단하는 용도로 설계된 모나드가 Validation이다.
Validation 모나드는 판타지랜드의 어플라이 규격에 의존에 동작한다.

Validation 모나드는 Maybe와 비슷하게 Success 와 Failure 두 가지 모나드로 구성된다.
Success 와 Failure 모나드는 기본적으로 Identitiy 모나드의 ap 메서드 방식으로 동작한다.
ap 메서드를 사용할 때에는 Identity 모나드의 value가 함수여야 한다.

import { Identity } from "../classes/Identity";

const add = (a: number) => (b: number) => a+b
console.log(
    add(1)(2),
    Identity.of(add).ap(1).ap(2).value()
)

먼저 앞에서본 Identity 모나드를 이용해서 2차 고차함수를 value로 삼은뒤 ap 메서드를 두번 호출해서 3을 만든다.

import { Identity } from "../classes/Identity";
type ISuccess = {isSuccess: boolean,isFailure: boolean}

const chechSuccess = (a: ISuccess) => (b: ISuccess) :boolean =>
    [a,b].filter(({isFailure}) => isFailure == true).length == 0

const isSuccess = Identity.of(chechSuccess)
                          .ap({isSuccess: true,isFailure: false})
                          .ap({isSuccess: false,isFailure: true})
                          .value()

console.log(isSuccess)

checkSuccess 함수는 두개의 고차 매개변수들을 배열로 만든다음
isFailure 값이 true인것들만 추려내서 그 개수가 0개일 때만 성공이라고 판단한다.

하지만 코드는 isFaliure가 한번이라도 true인 적이 있으므로 false가 출력된다.
Validation 모나드가 제공하는 Success와 Failure 모나드는 이런방식으로 동작한다.

Validation 모나드 구조

import { Success } from "./Success";
import { Failure } from "./Failure";

export class Validation{
    static Success = Success
    static Failure = Failure
    static of<T>(fn:T): Success<T> {return Success.of<T>(fn)}
}

export {Success,Failure}

Validation 클래스는 Maybe와 비슷하게 Success와 Failure 두가지 모나드로 구성된다.

Success 모나드 구현

Success 모나드는 IChain 형태로는 동작하지 않으므로 IFunctor와 IApply,IApplicative만 구현한다.
다른 메서드들과 달리 ap메서드는 매개변수가 Failure인지에 따라 조금 다르게 동작한다.

import { IFunctor,IApply } from "../../../ch11/src/interface";
import { IValidation } from "./IValidation";

export class Success<T> implements IValidation<T>,IFunctor<T>,IApply<T>{
    constructor(public value: T,public isSuccess = true,public isFailure = false) {}
    //IApplicative
    static of<U>(value: U): Success<U> {return new Success<U>(value)}

    //IFunctor
    map<U>(fn: (x:T) => U){
        return new Success<U>(fn(this.value))
    }

    //IApply
    ap(b){
        return b.isFailure ? b : b.map(this.value)
    }
}

Success 클래스의 value는 현재 함수이다.
따라서 map 함수의 콜백함수로 사용될 수 있다.

다음 테스트코드는 checkSuccess 함수가 최종적으로 boolean 타입의 값을 반환하므로 value 값은 true이다.

import { Success } from "../classes/Success";

const checkSuccess = <T>(a: Success<T>) => (b: Success<T>):boolean =>
    [a,b].filter(({isFailure}) => isFailure == true).length == 0

console.log(
    Success.of(checkSuccess)
        .ap(Success.of(1))
        .ap(Success.of(2))
)

실행결과
Success { value: true, isSuccess: true, isFailure: false }

Failure 모나드 구현

Failure 모나드는 최종적으로 실패한 원인을 문자열 배열로 저장하는 것으로 구현할 것이다.

다음 Failure 구현코드는 ap메서드에서 과거 ap 호출때 발생한 에러 문자열들이 담긴 현재의 에러 문자열들을 전개연산자로 병합한다.

import { IFunctor,IApply } from "../../../ch11/src/interface";
import { IValidation } from "./IValidation";

export class Failure<T> implements IValidation<T>,IFunctor<T>,IApply<T>{
    constructor(public value: T[],public isSuccess = false,public isFailure = true) {}

    //IApplicative
    static of<U>(value: U[]):Failure<U> {return new Failure<U>(value)}

    //IFunctor
    map(fn) {return new Failure<T>(fn(this.value))}

    //IApply
    ap(b){
        return b.isFailure ? new Failure<T>([...this.value,...b.value]) : this
    }
}

비밀번호 검증기능 구현

비밀번호를 검증하려면 객체에 password 속성이 있어야하고 이속성에 string 타입이 들어있어야 한다.
다음 checkNull 함수는 이런내용을 검증한다.

import { Success,Failure } from "../classes/Validation";

export const checkNull = <S,F>(o: {password?: string}) => {
    const {password} = o
    return (password == undefined || typeof password != 'string')?
        new Failure(['PAssword cant be null']) : new Success(o)
}

그리고 길이를 만족해야지 넘어갈수있는지를 판별하는 함수인 checkLength함수는 다음과 같이 구현한다.

import { Success,Failure } from "../classes/Validation";

export const checkLength = (o: {password?: string}, minLength: number = 6) =>{
        const {password} = o
        return (!password || password.length < minLength) ?
            new Failure(['Password must have more than 6 characters']) : new Success(o)
}

위 두 함수를 이용해서 비밀번호의 조건이 만족하는지에 대해 판별하는 함수인 checkPassword 함수는 다음과 같이 구현한다.

import { Validation } from "../classes/Validation";
import { checkNull } from "./checkNUll";
import { checkLength } from "./checkLength";

export const checkPassword = (o) : [object,string[]] =>{
    const result = Validation.of(a => b => o)
        .ap(checkNull(o))
        .ap(checkLength(o))

    return result.isSuccess ? [result.value, undefined] : [undefined, result.value]
}

그리고 이를 테스트하는 코드를 짜보자

import { checkPassword } from "../utils/checkPassword";

[
    {password: '123456'},
    {password: '1234'},
    {},
    {pa: '123456'},
]

    .forEach((target,index) => {
        const [value, failureReason] = checkPassword(target)
        if(failureReason)
            console.log(index, 'validation fail.', JSON.stringify(failureReason))
        else 
            console.log(index, 'validation ok', JSON.stringify(value))
    })

실행결과
0 validation ok {"password":"123456"}
1 validation fail. ["1Password must have more than 6 characters"]
2 validation fail. ["12Password cant be null","1Password must have more than 6 characters"]
3 validation fail. ["12Password cant be null","1Password must have more than 6 characters"]

잘 작동되는 모습을 볼 수 있다.

이메일주소 검증기능 구현

이메일 주소처럼 어떤 패턴이 있는 경우 정규식을 사용해 유효성을 판별할 수 있다.
다음 코드는 정규식을 사용해 이메일 주소를 검증한다.

import { Success,Failure } from "../classes/Validation";

export const checkEmailAddress = (o: {email?: string}) =>{
    const {email} = o
    const re = new RegExp(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
    return re.test(email) ? new Success(email)
        :new Failure(['invalid email address'])
}

정규식은 인터넷에 검색하면 나온다.

import { Validation } from "./classes/Validation";
import { checkNull } from "./utils/checkNUll";
import  {checkEmailAddress} from "./utils/checkEmailAddress"

export const checkEmail = (o) : [object, string[]] => {
    const result = Validation.of(a => o)
        .ap(checkEmailAddress(o))

    return result.isSuccess ? [result.value, undefined] : [undefined, result.value]
}

위는 이메일을 검증하는 함수이다.

다음은 이메일 체크 메서드르 시험하는 코드이다.

import { checkEmail } from "../checkEmail";

[
    {email: 'abc@cgf.com'},
    {email: 'aaaa'}
].forEach((target,index) =>{
    const [value,failureReason] = checkEmail(target)
    if(failureReason)
        console.log(index,'validation Fail',JSON.stringify(failureReason))
    else
        console.log(index,'validation ok',JSON.stringify(value))
})

실행결과
0 validation ok {"email":"abc@cgf.com"}
1 validation Fail ["invalid email address"]

정상적으로 작동하는 모습을 볼 수 있다.

IO 모나드 이해와 구현

IO 모나드란?

Promise 객체는 생성할때 넘겨주는 콜백함수가 then 메서드를 호출해야 비로소 동작하는데 IO모나드도 이런방식으로 동작한다.
IO모나드는 사용하는 쪽 코드를 알아야 그 동작을 이해할 수 있으므로 그 동작을 이해할 수 있다.

다음코드에서 work 라는 함수가 있고 IO.of(work)로 IO 객체를 만든다.
여기서는 work 함수가 실행되지 않고
이후에 runIO 메서드가 호출되면 그때 동작하게된다.

runIO 메서드 이해하기

export interface IRunIO {
    runIO<R> (...args: any[]):R
}

runIO 메서드는 다음 코드처럼 여러개의 매개변수를 사용해 동작할 수 있다.

IO 모나드 구현

IO 모나드가 동작하는 모습은 IApply의 ap 메서드를 연상시키지만 runIO에 의해 동작한다.
따라서 다음 IO 모나드 구현 코드에서는 IApply 메서드를 구현하지 않는다.
IO모나드의 map 메서드는 runIO가 호출되기 전까지는 동작하지 말아야 한다.
이에 따라 다른 모나드와 다르게 입력받은 콜백함수를 pipe를 사용해 조합하는 방식으로 구현해야한다.

import { IRunIO } from "./IrunIO";
import { IFunctor } from "../interface";

const pipe = (...funcs) => (args) => funcs.reduce((value,fn) => fn(value),args)

export class IO implements IRunIO,IFunctor<Function>{
    constructor(public fn : Function) {}
    static of(fn: Function) {return new IO(fn)}

    //IRunIO
    runIO<T> (...args: any[]):T {
        return this.fn(...args) as T
    }
    //IFunctor
    map(fn: Function): IO{
        const f: Function = pipe(this.fn,fn)
        return IO.of(f)
    }    
    //IChain
    chain(fn){
        const that = this
        return IO.of((value) =>{
            const io = fn(that.fn(value))
            return io.fn()
        })
    }
}

0개의 댓글