fp-ts 로 데이터 검증하기 1 - 데이터의 유무 확인

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 적용

usernamepassword 중 하나라도 없다면 에러를 발생시키고, 아니면 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 확인
  );

하지만 이렇게만 작성하면 bodyOption 이 아니기 때문에 오류가 발생한다.
O.ofbodyOption 으로 만들어주자.

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 함수가 이미 존재한다.
그러나 해당 함수는 keystring 으로만 제한하고 있어 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;

잘 보면 해당 함수는 keyobj 에 존재하는지를 확인 하는 부분과 실제로 반환 값을 만드는 부분으로 나눌 수 있다.
이런 상황이 많이 발생하기 때문에 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
  );

너무 길어지는 듯 보인다.
사실 잘 보면 has4Option 을 받아 Option 컨테이너 속 값을 까보고 확인할 수 있다.
이는 여기에 쓰인 O.fromPredicate 함수가 O.map 의 역할을 하기 때문이다.
그럼 O.flattenO.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));

Predicatefp-ts 에서 제공하는 타입이다.
type Predicate<A> = (a: A) => a is A, 인자가 어떤 타입인지를 보장하는 함수이다.

찾아보니 fp-ts/Optionfilter 함수가 있었다.
해당 함수에 대한 설명은 다음 글에서 이어가도록 하겠다.


© 2021. All rights reserved.

Powered by Hydejack v7.5.2