김민수
데브시스터즈
Software Engineer (Frontend Engineer)
JavaScript, TypeScript, ReactJS, GatsbyJS,
GraphQL, Functional Programming, …
이창희
前 데브시스터즈
Software Engineer (Frontend/Backend Engineer)
JavaScript, TypeScript, Golang, Python, …
데이터를 수학적 계산 함수(순수 함수)로 처리하고
사이드 이펙트를 멀리하는 프로그래밍 패러다임
프론트엔드 개발을 하면서 사이드 이펙트를 멀리할 수 있나요?
DOM 조작, 데이터 패칭 모두 사이드 이펙트를 발생시킬 것 같은데요?
여러 방법을 통해 불순 함수와 순수 함수를 함께 관리하고
이러한 함수들을 조합해 프로그램을 만드는 패러다임
팀 내부에서 함수형 프로그래밍에 대한 관심이 존재
진행하던 프로젝트에 복잡한 상태를 관리해야 하는 폼이 존재
하나의 폼에 8개의 입력이 존재
각각 다른 검증 로직이 존재
공통으로 사용할 수 있는 검증 과정 존재
fp-ts
type None = { _tag: 'None' }; type Some<A> = { _tag: 'Some', value: A }; type Option<A> = None | Some<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>는 실패할 수 있는 계산의 효과를 나타냅니다.
type None = { _tag: 'None' }; type Some<A> = { _tag: 'Some', value: A }; type Option<A> = None | Some<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' }
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' }
type None = { _tag: 'None' }; type Some<A> = { _tag: 'Some', value: A }; type Option<A> = None | Some<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 }
import { fromNullable } from 'fp-ts/lib/Option'; fromNullable(undefined); // { _tag: 'None' } fromNullable(null); // { _tag: 'None' } fromNullable(0); // { _tag: 'Some', value: 0 }
type None = { _tag: 'None' }; type Some<A> = { _tag: 'Some', value: A }; type Option<A> = None | Some<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 }
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 }
type Left<E> = { _tag: 'Left', left: E }; type Right<A> = { _tag: 'Right', right: A }; type Either<E, A> = Left<E> | Right<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는 성공을 표현합니다.
type Left<E> = { _tag: 'Left', left: E }; type Right<A> = { _tag: 'Right', right: A }; type Either<E, A> = Left<E> | Right<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...' }
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...' }
type Left<E> = { _tag: 'Left', left: E }; type Right<A> = { _tag: 'Right', right: A }; type Either<E, A> = Left<E> | Right<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' }
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' }
type Left<E> = { _tag: 'Left', left: E }; type Right<A> = { _tag: 'Right', right: A }; type Either<E, A> = Left<E> | Right<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' }
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' }
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>>;
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>를 사용할 수 있습니다.
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>>;
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();
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();
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>>;
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();
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();
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
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
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)))))));
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)))))));
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);
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);
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>;
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> );
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> );
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>;
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>> );
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>> );
const eiterhChain = <E, A, B>(f: (a: A) => Either<E, B>) => ( ma: Either<E, A>, ): Either<E, B> => (isLeft(ma) ? ma : f(ma.right));
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!');
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!');
const eiterhChain = <E, A, B>(f: (a: A) => Either<E, B>) => ( ma: Either<E, A>, ): Either<E, B> => (isLeft(ma) ? ma : f(ma.right));
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);
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);
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;
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), );
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), );
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;
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), );
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), );
폼에 8개의 입력이 존재하는데, 이것을 어떻게 우아하게 처리할 수 있을까요?
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] = 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] = 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) => { /* ... */ };
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) => { /* ... */ };
🤔
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, ), );
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 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 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
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자여야합니다. ], '올바르지 않은 번호형식입니다.' ) ) );
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');
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]; };
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] = 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) => { 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 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 = [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> ... )
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> ... )