Skip to content

Latest commit

Β 

History

History
2032 lines (1479 loc) Β· 67.4 KB

slides.md

File metadata and controls

2032 lines (1479 loc) Β· 67.4 KB
theme highlighter favicon title info colorSchema fonts
geist
shiki
/images/favicon.ico
ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ ν•¨μˆ˜ν˜•μ„ μΆ”κ΅¬ν•˜λ©΄ μ•ˆλ˜λŠ” 걸까?
ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ— λŒ€ν•œ 이둠과 ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμ„ ν•˜λ©΄μ„œ ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ„ μ μš©ν•œ 사둀λ₯Ό μ†Œκ°œν•©λ‹ˆλ‹€.
light
mono
Noto Sans Mono

ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ ν•¨μˆ˜ν˜•μ„
μΆ”κ΅¬ν•˜λ©΄ μ•ˆλ˜λŠ” 걸까?

κΉ€λ―Όμˆ˜, 이창희

λ‹€μŒ νŽ˜μ΄μ§€λ‘œ

μžκΈ°μ†Œκ°œ

κΉ€λ―Όμˆ˜

λ°λΈŒμ‹œμŠ€ν„°μ¦ˆ
Software Engineer (Frontend Engineer)

JavaScript, TypeScript, ReactJS, GatsbyJS,
GraphQL, Functional Programming, …


μžκΈ°μ†Œκ°œ

이창희

前 λ°λΈŒμ‹œμŠ€ν„°μ¦ˆ
Software Engineer (Frontend/Backend Engineer)

JavaScript, TypeScript, Golang, Python, …


ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄λž€?


ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄λž€?

데이터λ₯Ό μˆ˜ν•™μ  계산 ν•¨μˆ˜(순수 ν•¨μˆ˜)둜 μ²˜λ¦¬ν•˜κ³ 
μ‚¬μ΄λ“œ μ΄νŽ™νŠΈλ₯Ό λ©€λ¦¬ν•˜λŠ” ν”„λ‘œκ·Έλž˜λ° νŒ¨λŸ¬λ‹€μž„

ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμ„ ν•˜λ©΄μ„œ μ‚¬μ΄λ“œ μ΄νŽ™νŠΈλ₯Ό 멀리할 수 μžˆλ‚˜μš”?
DOM μ‘°μž‘, 데이터 패칭 λͺ¨λ‘ μ‚¬μ΄λ“œ μ΄νŽ™νŠΈλ₯Ό λ°œμƒμ‹œν‚¬ 것 κ°™μ€λ°μš”?

μ—¬λŸ¬ 방법을 톡해 뢈순 ν•¨μˆ˜μ™€ 순수 ν•¨μˆ˜λ₯Ό ν•¨κ»˜ κ΄€λ¦¬ν•˜κ³ 
μ΄λŸ¬ν•œ ν•¨μˆ˜λ“€μ„ μ‘°ν•©ν•΄ ν”„λ‘œκ·Έλž¨μ„ λ§Œλ“œλŠ” νŒ¨λŸ¬λ‹€μž„


νŒ€μ— ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ° λ„μž…ν•˜κΈ°

νŒ€ λ‚΄λΆ€μ—μ„œ ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ— λŒ€ν•œ 관심이 쑴재


νŒ€μ— ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ° λ„μž…ν•˜κΈ°

μ§„ν–‰ν•˜λ˜ ν”„λ‘œμ νŠΈμ— λ³΅μž‘ν•œ μƒνƒœλ₯Ό 관리해야 ν•˜λŠ” 폼이 쑴재

ν•˜λ‚˜μ˜ 폼에 8개의 μž…λ ₯이 쑴재

각각 λ‹€λ₯Έ 검증 둜직이 쑴재

κ³΅ν†΅μœΌλ‘œ μ‚¬μš©ν•  수 μžˆλŠ” 검증 κ³Όμ • 쑴재


νŒ€μ— ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ° λ„μž…ν•˜κΈ°

fp-ts

  1. 1. Haskell, PureScript, Scala 기반의 μΈκΈ°μžˆλŠ” νƒ€μž… 좔상화λ₯Ό 제곡
  2. 2. κ΅¬ν˜„λœ νƒ€μž… ν΄λž˜μŠ€λŠ” μ•ˆμ „ν•˜κ²Œ 쑰합될 수 μžˆλ„λ‘ 좔상 λŒ€μˆ˜ν•™κ³Ό 범주둠에 기반
  3. 3. Typescriptμ—μ„œ μ§€μ›ν•˜μ§€ μ•ŠλŠ” HKT(Higher Kinded Types) κ΅¬ν˜„
  4. 4. io-ts 같이 μ‚¬μš©ν•  수 μžˆλŠ” 많음 라이브러리 λ˜ν•œ 쑴재 (fp-ts/ecosystem)

fp-ts의 νƒ€μž… 좔상화

Option<A>

type None = { _tag: 'None' };
type Some<A> = { _tag: 'Some', value: A };
type Option<A> = None | Some<A>;

Option<A>λŠ” 선택적인 κ°’ Aλ₯Ό μœ„ν•œ μ»¨ν…Œμ΄λ„ˆ μž…λ‹ˆλ‹€.

A νƒ€μž…μ˜ 값이 μ‘΄μž¬ν•œλ‹€λ©΄ Option<A>λŠ” Some<A> μΈμŠ€ν„΄μŠ€μž…λ‹ˆλ‹€.

값이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ Option<A>λŠ” None μΈμŠ€ν„΄μŠ€μž…λ‹ˆλ‹€.

Option<A>λŠ” μ‹€νŒ¨ν•  수 μžˆλŠ” κ³„μ‚°μ˜ 효과λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.


fp-ts의 νƒ€μž… 좔상화

Option<A>

type None = { _tag: 'None' };
type Some<A> = { _tag: 'Some', value: A };
type Option<A> = None | Some<A>;
import { Option, some, none } from 'fp-ts/lib/Option';

function findIndex<A>(
  as: Array<A>,
  predicate: (a: A) => boolean
): Option<number> {
  const index = as.findIndex(predicate);
  return index === -1 ? none : some(index);
}

const arr = [1, 2, 3];
findIndex(arr, (n) => n === 1); // { _tag: 'Some', value: 0 }
findIndex(arr, (n) => n === 4); // { _tag: 'None' }

fp-ts의 νƒ€μž… 좔상화

Option<A>

type None = { _tag: 'None' };
type Some<A> = { _tag: 'Some', value: A };
type Option<A> = None | Some<A>;
import { fromNullable } from 'fp-ts/lib/Option';

fromNullable(undefined); // { _tag: 'None' }
fromNullable(null);      // { _tag: 'None' }
fromNullable(0);         // { _tag: 'Some', value: 0 }

fp-ts의 νƒ€μž… 좔상화

Option<A>

type None = { _tag: 'None' };
type Some<A> = { _tag: 'Some', value: A };
type Option<A> = None | Some<A>;
import { fromPredicate } from 'fp-ts/lib/Option';

const isNumber = <T>(a: T) => !isNaN(Number(a));
const getOptionNumber = fromPredicate(isNumber);

getOptionNumber('a')   // { _tag: 'None' }
getOptionNumber('10'); // { _tag: 'Some', value: '10' }
getOptionNumber(1);    // { _tag: 'Some', value: 1 }

fp-ts의 νƒ€μž… 좔상화

Either<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;

Either<E,A>λŠ” 두 개의 νƒ€μž… 쀑 ν•˜λ‚˜μ˜ 값을 ν‘œν˜„ν•©λ‹ˆλ‹€. (뢄리 합집합, Disjoint Union)

Either의 μΈμŠ€ν„΄μŠ€λŠ” Left λ˜λŠ” Right μΈμŠ€ν„΄μŠ€ μž…λ‹ˆλ‹€.

EitherλŠ” 결츑값을 μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ Option λŒ€μ‹ μ— μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Option의 None은 정보λ₯Ό 포함할 수 μžˆλŠ” Left둜 λŒ€μ²΄ λ©λ‹ˆλ‹€.

일반적으둜 LeftλŠ” μ‹€νŒ¨λ₯Ό ν‘œν˜„ν•˜κ³  RightλŠ” 성곡을 ν‘œν˜„ν•©λ‹ˆλ‹€.


fp-ts의 νƒ€μž… 좔상화

Either<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;
import { Either, tryCatch } from 'fp-ts/lib/Either';

function parse(s: string): Either<Error, unknown> {
  return tryCatch(
    () => JSON.parse(s),
    (reason) => new Error(String(reason)),
  );
}

const success = '{"a": 1, "b": 2}';
const fail = '{"a": 1, "b"}';

parse(success); // { _tag: 'Right', right: { a: 1, b: 2 } }
parse(fail);    // { _tag: 'Left', left: 'Error: SyntaxError: Unexpected token...' }

fp-ts의 νƒ€μž… 좔상화

Either<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;
import { fromNullable } from 'fp-ts/lib/Either';

const getEitherString = fromNullable('defaultValue');

getEitherString(null);      // { _tag: 'Left', left: 'defaultValue' }
getEitherString(undefined); // { _tag: 'Left', left: 'defaultValue' }
getEitherString('value');   // { _tag: 'Right', right: 'value' }

fp-ts의 νƒ€μž… 좔상화

Either<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;
import { fromPredicate } from 'fp-ts/lib/Either';

const isEmptyString = (s: string) => s === '';
const getEitherString = fromPredicate(
  (s: string) => !isEmptyString(s),
  () => 'defaultValue',
);

getEitherString('');    // { _tag: 'Left', left: 'defaultValue' }
getEitherString('abc'); // { _tag: 'Right', right: 'abc' }

fp-ts의 νƒ€μž… 좔상화

Task<A> TaskEither<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;
type Task<A> = { (): Promise<A> };
type TaskEither<E, A> = Task<Either<E, A>>;

Task<A>λŠ” A νƒ€μž…μ˜ 값을 λ°˜ν™˜ν•˜λŠ” 비동기 계산을 ν‘œν˜„ν•©λ‹ˆλ‹€.

Task<A>λŠ” μ ˆλŒ€ μ‹€νŒ¨ν•˜μ§€ μ•ŠλŠ” 비동기 계산에 μ‚¬μš©λ©λ‹ˆλ‹€.

μ‹€νŒ¨ν•  수 μžˆλŠ” 비동기 계산은 TaskEither<E,A>λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.


fp-ts의 νƒ€μž… 좔상화

Task<A> TaskEither<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;
type Task<A> = { (): Promise<A> };
type TaskEither<E, A> = Task<Either<E, A>>;
import { Task } from 'fp-ts/lib/Task';

const read: Task<string> = () => {
  return new Promise<string>((resolve) => {
    const rl = createInterface({
      input: process.input,
      output: process.stdout,
    });

    rl.question('Input: ', (answer) => {
      rl.close();
      console.log(answer);
      resolve(answer);
    });
  });
}

read();

fp-ts의 νƒ€μž… 좔상화

Task<A> TaskEither<E,A>

type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A };
type Either<E, A> = Left<E> | Right<A>;
type Task<A> = { (): Promise<A> };
type TaskEither<E, A> = Task<Either<E, A>>;
import { TaskEither, tryCatch } from 'fp-ts/lib/TaskEither';

function taskEitherTest(isResolve: boolean): TaskEither<string, string> {
  return tryCatch(
    () => isResolve
      ? Promise.resolve('resolved')
      : Promise.reject('rejected'),
    () => 'fall back string',
  );
}

async function run() {
  const resolve = taskEitherTest(true);
  const reject = taskEitherTest(false);

  console.log(await resolve());
  console.log(await reject());
}

run();

fp-ts의 μœ ν‹Έ ν•¨μˆ˜

pipe

flowchart LR
  1 -- add1 --> 2 -- add2 --> 4 -- add3 --> 7
Loading

pipeλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³  ν•¨μˆ˜λ₯Ό ν•©μ„±ν•˜λŠ” 경우

const add = (a: number) => (b: number) => a + b;

const add1 = add(1);
const add2 = add(2);
const add3 = add3(3);

add3(add2(add1(1))); // 7

fp-ts의 μœ ν‹Έ ν•¨μˆ˜

pipe

flowchart LR
  1 -- add1 --> 2 -- add2 --> 4 -- add3 --> 7
Loading

pipeλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ•˜μ„ λ•Œ ν•©μ„±λ˜λŠ” ν•¨μˆ˜μ˜ μˆ˜κ°€ 점점 λ§Žμ•„μ§„λ‹€λ©΄ μ–΄λ–»κ²Œ λ κΉŒμš”??

const add = (a: number) => (b: number) => a + b;

const add1 = add(1);
const add2 = add(2);
const add3 = add3(3);

add3(add3(add3(add3(add3(add2(add1(1)))))));
Callback Hell이 λ– μ˜€λ₯΄μ‹œμ§€ μ•Šλ‚˜μš”?

fp-ts의 μœ ν‹Έ ν•¨μˆ˜

pipe

flowchart LR
  1 -- add1 --> 2 -- add2 --> 4 -- add3 --> 7
Loading

pipeλ₯Ό μ‚¬μš©ν•΄ ν•¨μˆ˜λ₯Ό ν•©μ„±ν•˜λŠ” 경우

import { pipe } from 'fp-ts/lib/function';

const add = (a: number) => (b: number) => a + b;

const add1 = add(1);
const add2 = add(2);
const add3 = add3(3);

pipe(1, add1, add2, add3);
pipe(1, add1, add2, add3, add3, add3, add3, add3, add3);
JavaScript의 pipe μ—°μ‚°μž |>λŠ” μ œμ•ˆ Stage 2단계에 μžˆμŠ΅λ‹ˆλ‹€. https://github.com/tc39/proposal-pipeline-operator

νƒ€μž… 좔상화λ₯Ό μ‚¬μš©ν•˜λŠ” 법

map

declare const optionMap: <A, B>(f: (a: A) => B) => (fa: Option<A>) => Option<B>;
declare const taskMap: <A, B>(f: (a: A) => B) => (fa: Task<A>) => Task<B>;
declare const eitherMap: <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B>;
declare const taskEitherMap: <A, B>(f: (a: A) => B) => <E>(fa: TaskEither<E, A>) => TaskEither<E, B>;

mapν•¨μˆ˜λŠ” 사상 ν•¨μˆ˜λΌκ³  ν•˜λ©° A νƒ€μž…μ˜ 값을 B νƒ€μž…μ˜ κ°’μœΌλ‘œ λ°”κΏ€ λ•Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

mapν•¨μˆ˜λŠ” κ³΅ν†΅μ μœΌλ‘œ (f: (a: A) => B) μ‹œκ·Έλ‹ˆμ²˜λ₯Ό κ°–λŠ” ν•¨μˆ˜λ₯Ό μ „λ‹¬λ°›μŠ΅λ‹ˆλ‹€.

import { fromNullable, map } from 'fp-ts/lib/Option';

pipe(
  'something value',                // string
  fromNullable,                     // Option<string>
  map((value) => value.length),     // Option<number>
  map((value) => value + 1),        // Option<number>
  map((value) => value.toString()), // Option<string>
);
Some νƒ€μž…μΌ 경우 map ν•¨μˆ˜μ— μ „λ‹¬λœ ν•¨μˆ˜κ°€ μ‹€ν–‰λ©λ‹ˆλ‹€.

νƒ€μž… 좔상화λ₯Ό μ‚¬μš©ν•˜λŠ” 법

map

declare const optionMap: <A, B>(f: (a: A) => B) => (fa: Option<A>) => Option<B>;
declare const taskMap: <A, B>(f: (a: A) => B) => (fa: Task<A>) => Task<B>;
declare const eitherMap: <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B>;
declare const taskEitherMap: <A, B>(f: (a: A) => B) => <E>(fa: TaskEither<E, A>) => TaskEither<E, B>;

mapν•¨μˆ˜λŠ” 사상 ν•¨μˆ˜λΌκ³  ν•˜λ©° A νƒ€μž…μ˜ 값을 B νƒ€μž…μ˜ κ°’μœΌλ‘œ λ°”κΏ€ λ•Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

mapν•¨μˆ˜λŠ” κ³΅ν†΅μ μœΌλ‘œ (f: (a: A) => B) μ‹œκ·Έλ‹ˆμ²˜λ₯Ό κ°–λŠ” ν•¨μˆ˜λ₯Ό μ „λ‹¬λ°›μŠ΅λ‹ˆλ‹€.

import { fromPredicate, map } from 'fp-ts/lib/Option';

pipe(
  1,                                   // number
  fromPredicate((value) => value < 0), // Option<number>
  map((value) => value * value),       // Option<number>
  map((value) => [value]),             // Option<Array<number>>
);
None νƒ€μž…μΌ 경우 map ν•¨μˆ˜μ— μ „λ‹¬λœ ν•¨μˆ˜κ°€ μ‹€ν–‰λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

νƒ€μž… 좔상화λ₯Ό μ‚¬μš©ν•˜λŠ” 법

chain

const eiterhChain = <E, A, B>(f: (a: A) => Either<E, B>) => (
  ma: Either<E, A>,
): Either<E, B> => (isLeft(ma) ? ma : f(ma.right));

chainν•¨μˆ˜λŠ” λ‹€μŒ 계산을 할지말지 κ²°μ •ν•˜κΈ° μœ„ν•΄ μ‚¬μš©λ˜λ©°

μ•ž κ³„μ‚°μ˜ λ°˜ν™˜ 값을 μ΄μš©ν•΄ μˆœμ„œλŒ€λ‘œ 계산을 μ§„ν–‰ν•©λ‹ˆλ‹€.

import { Either, chain, left, right } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';

const multiplyByTen = <T>(value: T): Either<string, number> =>
  typeof value === 'number' ? right(value * 10) : left('Not a number');
const increment = (value: number): Either<string, number> => right(value + 1);

const func = <T>(value: T) => pipe(
  value,
  multiplyByTen,
  chain(increment),
);

func('Hello World!');
Left νƒ€μž…μΌ 경우 chain ν•¨μˆ˜μ— μ „λ‹¬λœ ν•¨μˆ˜κ°€ μ‹€ν–‰λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

νƒ€μž… 좔상화λ₯Ό μ‚¬μš©ν•˜λŠ” 법

chain

const eiterhChain = <E, A, B>(f: (a: A) => Either<E, B>) => (
  ma: Either<E, A>,
): Either<E, B> => (isLeft(ma) ? ma : f(ma.right));

chainν•¨μˆ˜λŠ” λ‹€μŒ 계산을 할지말지 κ²°μ •ν•˜κΈ° μœ„ν•΄ μ‚¬μš©λ˜λ©°

μ•ž κ³„μ‚°μ˜ λ°˜ν™˜ 값을 μ΄μš©ν•΄ μˆœμ„œλŒ€λ‘œ 계산을 μ§„ν–‰ν•©λ‹ˆλ‹€.

import { Either, chain, left, right } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';

const multiplyByTen = <T>(value: T): Either<string, number> =>
  typeof value === 'number' ? right(value * 10) : left('Not a number');
const increment = (value: number): Either<string, number> => right(value + 1);

const func = <T>(value: T) => pipe(
  value,
  multiplyByTen,
  chain(increment),
);

func(10);
Right νƒ€μž…μΌ 경우 chain ν•¨μˆ˜μ— μ „λ‹¬λœ ν•¨μˆ˜κ°€ μ‹€ν–‰ λ©λ‹ˆλ‹€.

νƒ€μž… 좔상화λ₯Ό μ‚¬μš©ν•˜λŠ” 법

match fold

declare const optionMatch: <A, B>(onNone: () => B, onSome: (a: A) => B) =>
  (ma: Option<A>) => B;
declare const eitherMatch: <E, A, B>(onNone: (e: E) => B, onSome: (a: A) => B) =>
  (ma: Either<E, A>) => B;

match와 fold ν•¨μˆ˜λŠ” λ™μΌν•œ κΈ°λŠ₯을 μˆ˜ν–‰ν•˜λ©° κ°€λŠ₯ν•œ μΌ€μ΄μŠ€μ— 따라 μ‹€ν–‰ν•  ν•¨μˆ˜λ₯Ό λ°›μ•„ μ‹€ν–‰ν•©λ‹ˆλ‹€.

import { fromPredicate, match } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/function';

pipe(
  2,
  fromPredicate((value) => value !== 0),
  match(() => 0, (value) => 10 / value),
);
Option 의 Some νƒ€μž…μΌ 경우 두 번째 인자둜 μ „λ‹¬λœ ν•¨μˆ˜κ°€ μ‹€ν–‰λ©λ‹ˆλ‹€.

νƒ€μž… 좔상화λ₯Ό μ‚¬μš©ν•˜λŠ” 법

match fold

declare const optionMatch: <A, B>(onNone: () => B, onSome: (a: A) => B) =>
  (ma: Option<A>) => B;
declare const eitherMatch: <E, A, B>(onNone: (e: E) => B, onSome: (a: A) => B) =>
  (ma: Either<E, A>) => B;

match와 fold ν•¨μˆ˜λŠ” λ™μΌν•œ κΈ°λŠ₯을 μˆ˜ν–‰ν•˜λ©° κ°€λŠ₯ν•œ μΌ€μ΄μŠ€μ— 따라 μ‹€ν–‰ν•  ν•¨μˆ˜λ₯Ό λ°›μ•„ μ‹€ν–‰ν•©λ‹ˆλ‹€.

import { fromPredicate, match } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/function';

pipe(
  0,
  fromPredicate((value) => value !== 0),
  match(() => 0, (value) => 10 / value),
);
Option 의 None νƒ€μž…μΌ 경우 첫 번째 인자둜 μ „λ‹¬λœ ν•¨μˆ˜κ°€ μ‹€ν–‰λ©λ‹ˆλ‹€.

ν˜„μ‹€μ˜ 문제 ν•΄κ²° ν•˜κΈ°

폼에 8개의 μž…λ ₯이 μ‘΄μž¬ν•˜λŠ”λ°, 이것을 μ–΄λ–»κ²Œ μš°μ•„ν•˜κ²Œ μ²˜λ¦¬ν•  수 μžˆμ„κΉŒμš”?


μš°λ¦¬κ°€ νΌμ—μ„œ ν•΄μ•Όν•˜λŠ” 일

  1. μ‚¬μš©μžμ—κ²Œ κ°’ μž…λ ₯λ°›κΈ°
  2. μž…λ ₯된 κ°’ κ²€μ¦ν•˜κΈ°
  3. 검증에 μ‹€νŒ¨ν–ˆλ‹€λ©΄ 였λ₯˜ λ©”μ‹œμ§€ 보여주기


const [mobileNumber, setMobileNumber] = React.useState<string>('');
const [mobileNumberError, setMobileNumberError] = React.useState<string>('');

// μ˜¬λ°”λ₯Έ ν˜•μ‹μ˜ νœ΄λŒ€ν° λ²ˆν˜ΈμΈμ§€ κ²€μ¦ν•˜λŠ” ν•¨μˆ˜
const validateMobileNumber = (value: string): boolean => {
  if (value == '') {
    setMobileNumberError('νœ΄λŒ€ν° 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.');
    return false;
  }

  if (!mobileNumberRegex.test(value)
      || !value.startswith('01')
      || value.length < 10
      || value.length > 11
  ) {
    setMobileNumberError('νœ΄λŒ€ν° λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.');
    return false;
  }

  return true;
}

// νœ΄λŒ€ν° 번호 input의 onChange 이벀트 ν•Έλ“€λŸ¬
const handleMobileNumberChange = (e) => {
  const { value } = e.target;
  validateMobileNumber(value);
  setMobileNumber(value);
}

// form onsubmit 이벀트 ν•Έλ“€λŸ¬
const onSubmit = () => {
  const validations = [validateMobileNumber(mobileNumber), ...];
  if (validations.some((valid) => !valid)) {
    return;
  }
  // Submit Form ...
}

return (
  ...
  <input onChange={handleMobileNumberChange} value={mobileNumber} />
  <span className="error">{mobileNumberError}</span>
  ...
)

ν˜„μž¬ μƒνƒœ

검증 κ°€λŠ₯ν•œ μž…λ ₯ ν•„λ“œ ν•˜λ‚˜λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄ μš°λ¦¬λŠ” μ•„λž˜μ˜ 것듀이 ν•„μš”ν•©λ‹ˆλ‹€.

  • 2개의 React.useState (μž…λ ₯ κ°’, 였λ₯˜ λ©”μ‹œμ§€)
  • 1개의 ν•„λ“œ 검증 ν•¨μˆ˜
  • 검증 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜κ³  μž…λ ₯ κ°’ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜λŠ” 1개의 이벀트 ν•Έλ“€λŸ¬ 콜백 ν•¨μˆ˜
8개의 ν•„λ“œκ°€ μžˆλ‹€λ©΄, 16개의 μƒνƒœμ™€ 8개의 이벀트 ν•Έλ“€λŸ¬κ°€ ν•„μš”ν•©λ‹ˆλ‹€.

layout: center

const [mobileNumber, setMobileNumber] = React.useState<string>('');
const [mobileNumberError, setMobileNumberError] = React.useState<string>('');
const [name, setName] = React.useState<string>('');
const [nameError, setNameError] = React.useState<string>('');
const [email, setEmail] = React.useState<string>('');
const [emailError, setEmailError] = React.useState<string>('');

const validateMobileNumber = (value: string): boolean => {
  /* ... */
};
const validateName = (value: string): boolean => {
  /* ... */
};
const validateEmail = (value: string): boolean => {
  /* ... */
};

const handleMobileNumberChange = (e) => {
  /* ... */
};
const handleNameChange = (e) => {
  /* ... */
};
const handleEmailChange = (e) => {
  /* ... */
};

layout: center

πŸ€”


μ†”λ£¨μ…˜?

1. React-Hook-Form

2. Formik

3. fp-ts


곡톡 λΆ€λΆ„ 문제 μ°ΎκΈ°

  1. μž…λ ₯ κ°’κ³Ό 였λ₯˜ λ©”μ‹œμ§€λ₯Ό κ΄€λ¦¬ν•˜λŠ” 두 개의 μƒνƒœκ°€ μžˆμŠ΅λ‹ˆλ‹€.
  2. μ‚¬μš©μžκ°€ 값을 μž…λ ₯ν•˜λ©΄ μƒνƒœλ₯Ό κ°±μ‹ ν•©λ‹ˆλ‹€.
  3. 검증 ν•¨μˆ˜λ₯Ό 톡해 μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 값이 μ˜¬λ°”λ₯Έμ§€ κ²€μ¦ν•©λ‹ˆλ‹€.
  4. 검증에 였λ₯˜κ°€ μžˆλ‹€λ©΄ 였λ₯˜ λ©”μ‹œμ§€ μƒνƒœλ₯Ό κ°±μ‹ ν•©λ‹ˆλ‹€.

곡톡 λΆ€λΆ„ 문제 μ°ΎκΈ°

  1. μž…λ ₯ κ°’κ³Ό 였λ₯˜ λ©”μ‹œμ§€λ₯Ό κ΄€λ¦¬ν•˜λŠ” 두 개의 μƒνƒœκ°€ μžˆμŠ΅λ‹ˆλ‹€.
  2. μ‚¬μš©μžκ°€ 값을 μž…λ ₯ν•˜λ©΄ μƒνƒœλ₯Ό κ°±μ‹ ν•©λ‹ˆλ‹€.
  3. 검증 ν•¨μˆ˜λ₯Ό 톡해 μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 값이 μ˜¬λ°”λ₯Έμ§€ κ²€μ¦ν•©λ‹ˆλ‹€.
  4. 검증에 였λ₯˜κ°€ μžˆλ‹€λ©΄ 였λ₯˜ λ©”μ‹œμ§€ μƒνƒœλ₯Ό κ°±μ‹ ν•©λ‹ˆλ‹€.

문제 ν•΄κ²° ν•˜κΈ°

  1. κ³΅ν†΅μ μœΌλ‘œ μ‚¬μš©ν•  수 μžˆλŠ” 검증 ν•¨μˆ˜ λ§Œλ“€κΈ°
  2. 검증 κ·œμΉ™ μ •μ˜ν•˜κΈ°
  3. μž…λ ₯ ν•„λ“œ 검증기 λ§Œλ“€κΈ°
  4. μ»€μŠ€ν…€ ν›… λ§Œλ“€κΈ°

곡톡 검증 ν•¨μˆ˜ λ§Œλ“€κΈ°

import { fromPredicate } from 'fp-ts/Either';
import { pipe, type Predicate } from 'fp-ts/function';
import { every, map } from 'fp-ts/Array';

const validate = <T>(validators: Array<Predicate<T>>, errorMessage: string) => (value: T) => pipe(
  value,
  fromPredicate(
    (val) => pipe(
      validators,
      map(fn => fn(val)),
      every(Boolean),
    ),
    () => errorMessage,
  ),
);
const my_validator = validate(myMobileNumberRules, '잘λͺ»λœ μ „ν™”λ²ˆν˜Έ ν˜•μ‹μž…λ‹ˆλ‹€.');

my_validator('01012345678'); // right('01012345678')
my_validator('01aabb');      // left('잘λͺ»λœ μ „ν™”λ²ˆν˜Έ ν˜•μ‹μž…λ‹ˆλ‹€.')

검증 κ·œμΉ™ μ •μ˜ν•˜κΈ°

const startsWith = (search: string): Predicate<string> => (text: string) => text.startsWith(search);

const minLength = (limit: number): Predicate<string> => (text: string) => text.length >= limit;

const maxLength = (limit: number): Predicate<string> => (text: string) => text.length <= limit;

const testPhoneNumberPattern = (text: string) => !/[^0-9]/gi.test(text);
const myMobileNumer = '010123456';

testPhoneNumberPattern(myMobileNumer); // true
startsWith('01')(myMobileNumer);       // true
maxLength(11)(myMobileNumer);          // true
minLength(10)(myMobileNumer);          // false

μž…λ ₯ ν•„λ“œ 검증기 λ§Œλ“€κΈ°

import { chain } from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

export const validatePhoneNumber = (phoneNumber: string): Either<string, string> =>
  pipe(
    phoneNumber,
    validate([minLength(1)], 'ν•„μˆ˜ν•­λͺ©μž…λ‹ˆλ‹€.'), // 아무것도 μž…λ ₯λ˜μ§€ μ•Šμ•˜λŠ”μ§€ κ²€μ‚¬ν•©λ‹ˆλ‹€.
    chain(
      validate(
        [
          testPhoneNumberPattern, // 숫자 외에 λ‹€λ₯Έ λ¬Έμžκ°€ μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
          startsWith('01'),       // νœ΄λŒ€ν° λ²ˆν˜ΈλŠ” 01둜 μ‹œμž‘ν•΄μ•Όν•©λ‹ˆλ‹€.
          minLength(10),          // νœ΄λŒ€ν° 번호의 κΈΈμ΄λŠ” μ΅œμ†Œ 10μžμ—¬μ•Όν•©λ‹ˆλ‹€.
          maxLength(11),          // νœ΄λŒ€ν° 번호의 κΈΈμ΄λŠ” μ΅œλŒ€ 11μžμ—¬μ•Όν•©λ‹ˆλ‹€.
        ],
        'μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ λ²ˆν˜Έν˜•μ‹μž…λ‹ˆλ‹€.'
      )
    )
  );
validatePhoneNumber('');            // left('ν•„μˆ˜ν•­λͺ©μž…λ‹ˆλ‹€.');
validatePhoneNumber('012323abc');   // left('μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ λ²ˆν˜Έν˜•μ‹μž…λ‹ˆλ‹€.');
validatePhoneNumber('01012345678'); // right('01012345678');

μ»€μŠ€ν…€ ν›… λ§Œλ“€κΈ°

import * as Either from 'fp-ts/Either';
import * as string  from 'fp-ts/string';
import { identity, pipe } from 'fp-ts/function';

type StateValidator = {
  validate: () => boolean,
  error: string,
};

const useStateWithValidator = <T>(initialState: T, validator: (v: T) => Either<string, T>):
[T, (v: T, t?: boolean) => void, StateValidator] => {
  const [value, setValue] = useState<T>(initialState);
  const [error, setError] = useState('');

  const changeError = (e: string) => {
    setError(e);
    return e;
  };

  const changeValue = (v: T) => {
    pipe(
      validator(v),
      Either.match(
        identity,
        () => pipe(
          v,
          setValue,
          () => string.empty,
        ),
      ),
      changeError,
    );
  };

  const stateValidator: StateValidator = {
    validate(): boolean {
      return pipe(
        validator(value),
        Either.match(identity, () => string.empty),
        changeError,
        string.isEmpty,
      );
    },

    get error(): string {
      return error;
    },
  };

  return [value, changeValue, stateValidator];
};

κΈ°μ‘΄ μ½”λ“œ κ°œμ„ ν•˜κΈ°

const [mobileNumber, setMobileNumber] = React.useState<string>('');
const [mobileNumberError, setMobileNumberError] = React.useState<string>('');

// μ˜¬λ°”λ₯Έ ν˜•μ‹μ˜ νœ΄λŒ€ν° λ²ˆν˜ΈμΈμ§€ κ²€μ¦ν•˜λŠ” ν•¨μˆ˜
const validateMobileNumber = (value: string): boolean => {
  if (value == '') {
    setMobileNumberError('νœ΄λŒ€ν° 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.');
    return false;
  }

  if (!mobileNumberRegex.test(value)
      || !value.startswith('01')
      || value.length < 10
      || value.length > 11
  ) {
    setMobileNumberError('νœ΄λŒ€ν° λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.');
    return false;
  }

  return true;
}

// νœ΄λŒ€ν° 번호 input의 onChange 이벀트 ν•Έλ“€λŸ¬
const handleMobileNumberChange = (e) => {
  const { value } = e.target;
  validateMobileNumber(value);
  setMobileNumber(value);
}

// form onsubmit 이벀트 ν•Έλ“€λŸ¬
const onSubmit = () => {
  const validations = [validateMobileNumber(mobileNumber), ...];
  if (validations.some((valid) => !valid)) {
    return;
  }
  // Submit Form ...
}

return (
  ...
  <input onChange={handleMobileNumberChange} value={mobileNumber} />
  <span className="error">{mobileNumberError}</span>
  ...
)

κΈ°μ‘΄ μ½”λ“œ κ°œμ„ ν•˜κΈ°

const [mobileNumber, setMobileNumber, mobileNumberValidator] = useStateWithValidator<string>('', validatePhoneNumber);

// νœ΄λŒ€ν° 번호 input의 onChange 이벀트 ν•Έλ“€λŸ¬
const handleMobileNumberChange = (e) => {
  const { value } = e.target;
  validateMobileNumber(value);
  setMobileNumber(value);
}

// form onsubmit 이벀트 ν•Έλ“€λŸ¬
const onSubmit = () => {
  const validations = [validateMobileNumber(mobileNumber), ...];
  if (validations.some((valid) => !valid)) {
    return;
  }
  // Submit Form ...
}

return (
  ...
  <input onChange={handleMobileNumberChange} value={mobileNumber} />
  <span className="error">{mobileNumberError}</span>
  ...
)

κΈ°μ‘΄ μ½”λ“œ κ°œμ„ ν•˜κΈ°

const [mobileNumber, setMobileNumber, mobileNumberValidator] = useStateWithValidator<string>('', validatePhoneNumber);

// νœ΄λŒ€ν° 번호 input의 onChange 이벀트 ν•Έλ“€λŸ¬
const handleMobileNumberChange = (e) => setPhoneNumber(e.target.value);

// form onsubmit 이벀트 ν•Έλ“€λŸ¬
const onSubmit = () => {
  const validations = [validateMobileNumber(mobileNumber), ...];
  if (validations.some((valid) => !valid)) {
    return;
  }
  // Submit Form ...
}

return (
  ...
  <input onChange={handleMobileNumberChange} value={mobileNumber} />
  <span className="error">{mobileNumberError}</span>
  ...
)

κΈ°μ‘΄ μ½”λ“œ κ°œμ„ ν•˜κΈ°

const [mobileNumber, setMobileNumber, mobileNumberValidator] = useStateWithValidator<string>('', validatePhoneNumber);

// νœ΄λŒ€ν° 번호 input의 onChange 이벀트 ν•Έλ“€λŸ¬
const handleMobileNumberChange = (e) => setPhoneNumber(e.target.value);

// form onsubmit 이벀트 ν•Έλ“€λŸ¬
const onSubmit = () => {
  const validators = [mobileNumberValidator, ...];
  const isInvalid = validators
      .map((validator) => validator.validate())
      .some((valid) => !valid);

  if (isInvalid) {
    // Do something when input is invalid
    return;
  }

  // Submit Form ...
}

return (
  ...
  <input onChange={handleMobileNumberChange} value={mobileNumber} />
  <span className="error">{mobileNumberError}</span>
  ...
)

const [mobileNumber, setMobileNumber, mobileNumberValidator] = useStateWithValidator<string>('', validatePhoneNumber);

// νœ΄λŒ€ν° 번호 input의 onChange 이벀트 ν•Έλ“€λŸ¬
const handleMobileNumberChange = (e) => setPhoneNumber(e.target.value);

// form onsubmit 이벀트 ν•Έλ“€λŸ¬
const onSubmit = () => {
  const validators = [phoneNumberValidator, ...];
  const isInvalid = validators
      .map((validator) => validator.validate())
      .some((valid) => !valid);

  if (isInvalid) {
    // Do something when input is invalid
    return;
  }

  // Submit Form ...
}

return (
  ...
  <input onChange={handleMobileNumberChange} value={mobileNumber} />
  <span className="error">{mobileNumberValidator.error}</span>
  ...
)

정리

  • ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄ λŒ€μΆ© 무엇인지
  • fp-ts에 μ–΄λ–€ νƒ€μž…κ³Ό μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜κ°€ μ‘΄μž¬ν•˜κ³  μ–΄λ–»κ²Œ μ“°λŠ”μ§€
  • κ°’ 검증에 ν•„μš”ν•œ λ‘œμ§λ“€μ„ fp-tsλ₯Ό μ‚¬μš©ν•΄ ν•¨μˆ˜ν˜•μœΌλ‘œ μΆ”μƒν™”ν•˜λŠ” 방법
  • 곡톡 λΆ€λΆ„ 문제λ₯Ό ν•΄κ²°ν•˜λŠ” ν•¨μˆ˜μ™€ μ»€μŠ€ν…€ 훅을 λ§Œλ“€μ–΄ μ‚¬μš©ν•˜λŠ” μž…μž₯μ—μ„œμ˜ κ΅¬ν˜„μ„ λ‹¨μˆœν™” ν•˜λŠ” 방법
--- layout: two-cols ---

μ’‹μ•˜λ˜ 점

  • λŒ€μˆ˜μ  νƒ€μž…(ADT)λ₯Ό μ‚¬μš©ν•˜μ—¬ 문제λ₯Ό 더 잘 μ •μ˜ν•˜κ³  ν•΄κ²°ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
  • μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ©° ν•¨μˆ˜ν˜• 사고λ₯Ό ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    • κ³Όμ—° 이 ν•¨μˆ˜μ—μ„œ 이 일을 ν•˜λŠ” 것이 μ˜³μ„κΉŒ?
    • μ‚¬μ΄λ“œ μ΄νŽ™νŠΈ μ΅œμ†Œν™” 및 μ μ ˆν•˜κ²Œ μ‚¬μš©
  • νŒ€μ—μ„œ μƒˆλ‘œμš΄ κΈ°μˆ μ΄λ‚˜ κ°œλ…μ„ λ„μž…ν•  λ•Œ μ–΄λ–»κ²Œ 해야할지 μ•Œκ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

::right::

μ•„μ‰¬μ› λ˜ 점

  • fp-tsλ₯Ό 써보지 μ•Šμ•˜κ±°λ‚˜ ν˜Ήμ€ ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ° κ²½ν—˜μ΄ μ—†λ‹€λ©΄, μ½”λ“œ μž‘μ„±μ— 어렀움을 κ²ͺ을 수 μžˆμŠ΅λ‹ˆλ‹€.
  • ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ„ μ μš©ν•˜μ§€ μ•Šκ³  κ·Έλƒ₯ μž‘μ„±ν•˜λŠ” μ½”λ“œκ°€λ” 가독성이 μ’‹κ³  짧은 κ²½μš°λ„ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    • 예) Array 데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ μ»΄ν¬λ„ŒνŠΈλ‘œ λ§€ν•‘ν•˜λŠ” μ½”λ“œ
  • μ΅œμ‹  μžλ£Œκ°€ λ§Žμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
    • fp-ts λ©”μΈν…Œμ΄λ„ˆκ°€ μž‘μ„±ν•œ 글이 μžˆμ§€λ§Œ, μ˜€λž˜λ˜μ–΄ outdated 된 λ‚΄μš©μ΄ λ§Žμ•˜μŠ΅λ‹ˆλ‹€.
  • fp-ts의 곡식 λ¬Έμ„œκ°€ μΉœμ ˆν•œ νŽΈμ€ μ•„λ‹™λ‹ˆλ‹€.

layout: center

κ°μ‚¬ν•©λ‹ˆλ‹€.