fp-ts 로 데이터 검증하기 1 - 데이터의 유무 확인
on Typescript, Fp-ts
가정
다음과 같은 데이터를 검증해야 한다고 가정해보자.
interface UserInput {
username: string;
password: string;
}
데이터의 유무 확인
fp-ts
를 사용하지 않는다면
먼저 데이터의 유무부터 확인하자.
fp-ts
라이브러리가 없다면 다음과 같은 함수로 검증을 할 수 있을 것이다.
const isUserInput1 = (body: any): UserInput => {
if (!body.username || !body.password)
throw new Error('username or password is missing');
return body as UserInput;
};
fp-ts
적용
username
과 password
중 하나라도 없다면 에러를 발생시키고, 아니면 UserInput
타입으로 변환한다.
이 함수에 fp-ts
라이브러리를 적용해 보자.
먼저 에러 대신 Option
타입을 반환하도록 만들어보자.
import * as O from "fp-ts/Option";
const isUserInput2 = (body: any): O.Option<UserInput> => {
if (!body.username || !body.password)
return O.none;
return O.some(body as UserInput);
};
좀더 눈에 잘 들어오도록 하나하나 작성해보자.
const isUserInput3 = (body: any): O.Option<UserInput> => {
if (!body.username) return O.none;
if (!body.password) return O.none;
return O.some({ username, password });
};
pipe
여기에 pipe
함수를 적용해보자.
import { pipe } from "fp-ts/function";
const isUserInput4 = (body: any): O.Option<UserInput> =>
pipe(
body,
(body) => body.username ? O.some(body) : O.none, // username 확인
(body) => body.password ? O.some(body) : O.none, // password 확인
);
그럴듯해 보이지만 주석을 써놓은 곳에 오류가 발생한다.
왜냐면 해당 함수에 들어가는 인자는 Option
타입이기 때문이다.
따라서 다음과 같이 수정해야 한다.
const isUserInput5 = (body: any): O.Option<UserInput> =>
pipe(
body,
(body) => body.username ? O.some(body) : O.none, // username 확인
(body) => O.isSome(body) && body.value.password ? O.some(body.value) : O.none, // password 확인
);
map
이를 O.map
함수를 이용하면 좀더 읽기 좋게 바꿀 수 있다.
const isUserInput6 = (body: any): O.Option<UserInput> =>
pipe(
body,
(body) => body.username ? O.some(body) : O.none, // username 확인
O.map((body) => body.password ? body : O.none), // password 확인
);
통일성을 위해서 username
을 작성하는 곳도 O.map
으로 바꿔보자.
const isUserInput7 = (body: any): O.Option<UserInput> =>
pipe(
body,
O.map((body) => body.username ? body : O.none), // username 확인
O.map((body) => body.password ? body : O.none), // password 확인
);
하지만 이렇게만 작성하면 body
가 Option
이 아니기 때문에 오류가 발생한다.
O.of
로 body
를 Option
으로 만들어주자.
const isUserInput8 = (body: any): O.Option<UserInput> =>
pipe(
body,
O.of,
O.map((body) => body.username ? body : O.none), // username 확인
O.map((body) => body.password ? body : O.none), // password 확인
);
fromPredicate
그럼 이제는 반복되는 부분을 바꿔보자.
다음과 같은 함수를 만들어보자.
const has1 = (key: PropertyKey) => (obj: any): boolean => key in obj;
사실 fp-ts/Record
라이브러리에 동일한 작업을 하는 has
함수가 이미 존재한다.
그러나 해당 함수는 key
를 string
으로만 제한하고 있어 PropertyKey
로 바꿔주었다.
이 함수를 이용하면 다음과 같이 작성할 수 있다.
const isUserInput9 = (body: any): O.Option<UserInput> =>
pipe(
body,
O.of,
O.map((body) => (has1("username")(body) ? body : O.none)), // username 확인
O.map((body) => (has1("password")(body) ? body : O.none)) // password 확인
);
아예 has
함수도 Option
을 반환하도록 만들면 어떨까?
다음과 같이 작성해보자.
const has2 = (key: PropertyKey) => (obj: any) =>
key in obj ? O.some(obj) : O.none;
잘 보면 해당 함수는 key
가 obj
에 존재하는지를 확인 하는 부분과 실제로 반환 값을 만드는 부분으로 나눌 수 있다.
이런 상황이 많이 발생하기 때문에 fp-ts
에서는 fromPredicate
함수를 제공한다.
const has3 = (key: PropertyKey) => (obj: any) =>
O.fromPredicate(has1)(obj);
더 나아가, 이렇게 되면 obj
인자를 생략할 수 있다.
const has4 = (key: PropertyKey) => O.fromPredicate((obj: any) => key in obj);
이를 통해 다음과 같이 작성할 수 있다.
const isUserInput10 = (body: any): O.Option<UserInput> =>
pipe(
body,
O.of,
has4("username"), // username 확인
has4("password") // password 확인
);
flatMap
하지만 이렇게 되면 함수에 오류가 난다.
Option
이 중복되어 Option<Option<Option<any>>>
가 되기 때문이다. O.flatten
함수를 이용해서 Option
을 하나로 만들어주자.
const isUserInput11 = (body: any): O.Option<UserInput> =>
pipe(
body,
O.of,
has4("username"), // username 확인
O.flatten,
has4("password"), // password 확인
O.flatten
);
너무 길어지는 듯 보인다.
사실 잘 보면 has4
는 Option
을 받아 Option
컨테이너 속 값을 까보고 확인할 수 있다.
이는 여기에 쓰인 O.fromPredicate
함수가 O.map
의 역할을 하기 때문이다.
그럼 O.flatten
과 O.map
을 합친 O.flatMap
을 이용하면 좀더 간결하게 만들 수 있지 않을까?
이를 이용해 has
함수를 다시 작성해보자.
const has5 = (key: PropertyKey) => O.flatMap(O.fromPredicate((obj: any) => key in obj));
그럼 다음과 같이 작성할 수 있다.
const isUserInput12 = (body: any): O.Option<UserInput> =>
pipe(
body,
O.of,
has5("username"), // username 확인
has5("password") // password 확인
);
최종 코드
최종적으로 다음과 같은 코드를 작성할 수 있다.
import * as O from "fp-ts/Option";
const has = (key: PropertyKey) => O.flatMap(O.fromPredicate((obj: any) => key in obj));
const isUserInput = (body: any): O.Option<UserInput> =>
pipe(body, O.of, has("username"), has("password"));
잘 작동하는지 다음과 같은 테스트 코드로 검증해보자.
const validInput = isUserInput({ username: "1", password: "1" });
const invalidInput1 = isUserInput({ username: "1" });
const invalidInput2 = isUserInput({ password: "1" });
const valueIfSome = <T>(o: O.Option<T>) => (O.isSome(o) ? o.value : "None");
console.log(
valueIfSome(validInput), // { username: '1', password: '1' }
valueIfSome(invalidInput1), // None
valueIfSome(invalidInput2) // None
);
잘 작동하는 것을 확인할 수 있다.
추가로
has
함수를 작성할 때 사용한O.flatMap(O.fromPredicate(pred));
부분이 데이터를 검증할 때 많이 사용될 것 같다.
그래서 다음과 같은 함수를 미리 만들어 보았다.
공식문서를 찾아보면 비슷한 함수가 이미 있을 것 같기도 하다.const validate = <T>(pred: Predicate<T>) => O.flatMap(O.fromPredicate(pred));
Predicate
는fp-ts
에서 제공하는 타입이다.
type Predicate<A> = (a: A) => a is A
, 인자가 어떤 타입인지를 보장하는 함수이다.
찾아보니 fp-ts/Option
의 filter
함수가 있었다.
해당 함수에 대한 설명은 다음 글에서 이어가도록 하겠다.