React Final Form을 사용한 폼 제어
React Final Form은 Final Form을 React 컴포넌트로 래핑한 라이브러리다. 다른 라이브러리에 대한 의존성 없이 최소 2개의 컴포넌트와 render props 패턴을 가지고 간단히 폼을 제어할 수 있다.
최소한의 기능을 구현하기 위해 form
요소, 텍스트 입력을 위한 input
요소 하나, 그리고 form 요소의 onSubmit 이벤트를 호출하기 위한 button
요소만 있는 예제를 작성한다면 아래와 같다.
React Final Form - Only 1 field - CodeSandbox
import React from "react";
import { render } from "react-dom";
import { Form, Field } from "react-final-form";
const App = () => {
const onSubmit = values => {
window.alert(JSON.stringify(values, 0, 2));
};
return (
<Form
onSubmit={onSubmit}
initialValues={{ name: "" }}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<Field name="name" type="text">
{({ input }) => (
<input value={input.value} onChange={input.onChange} />
)}
</Field>
</div>
<div>
<button type="submit">Submit</button>
</div>
</form>
)}
/>
);
};
render(<App />, document.getElementById("root"));
무척 간단하다. Final Form이 내부적으로 옵저버 패턴을 사용하고 있어서 입력값이 변경되면 그 값을 사용하는 모든 내부 상태값과 동작에 즉시 반영된다. 예를 들면 어떤 필드의 값이 변경되면 초기값과 현재 값이 달라졌는지 여부가 업데이트되며, 그 필드의 유효성 검사도 다시 실행되는 식이다. 라이브러리를 사용하는 개발자 입장에서는 최소한의 정보만 제공하면 되어 무척 편리하다. 폼을 직접 구현하기 위해 필드의 값을 직접 컴포넌트에 state에 할당하고, 업데이트하는 함수를 선언하고, 유효성 검사 이벤트를 연결하고… 이런 반복 작업이 모두 사라진다.
물론 Form
, Field
, 그리고 render props에는 위의 예제에서 작성한 것보다 훨씬 많은 값들이 제공된다. 그리고 위의 예제에서는 폼 데이터 제어에서 필수 요소인 유효성 검사도 빠져 있다.
React Final Form API
Form
폼 제어를 위한 최상위 컴포넌트. Field는 Form 컴포넌트의 render props에 전달한 컴포넌트 안에 있어야 기능한다. Form 컴포넌트는 마운팅될 때 Final Form 인스턴스를 생성하며 Form 상태와 내부 Field들의 상태를 관리한다. 그리고 상태가 업데이트되면 Form 안에 있는 모든 Field에 변경 사항을 알려준다(publish). Field 컴포넌트는 Form으로부터 상태값을 전달받는다(subscribe).
onSubmit
함수
Form 컴포넌트에는 onSubmit
props를 꼭 전달해야 한다. onSubmit
은 폼이 제출(=form 요소의 onSubmit 이벤트가 발생)되고 모든 유효성 검사에 문제가 없을 때 호출되며 모든 필드의 데이터를 포함한 객체를 첫번째 파라미터로 받는다. 객체의 필드들의 이름은 Field 컴포넌트의 name
props로 전달된 문자열과 일치한다. 예를 들어 첫번째 예제 코드의 onSubmit 함수의 파라미터는 아래와 같은 형태가 될 것이다.
const onSubmit = (formValues: { name: string }) => {
// do something
}
폼 초기값 설정
Form 안에 있는 Field들의 초기값은 Field 컴포넌트에 1개씩 할당해도 되지만, 필드의 수가 많으면 Form 컴포넌트의 initialValues
props에 1개의 객체로 한꺼번에 전달하는 편이 더 간편하다. initialValues
가 받는 데이터는 객체이며, 객체의 필드는 Field 컴포넌트의 name
props와 일치해야 한다. 다만 initialValues
에는 모든 필드의 초기값을 전달하지 않아도 된다.
<Form
initialValues={{ userId: '이름' }}
render={() => {
return (
<Field name="userId">
{props => <input {...props.input} />} {/* userId 초기값이 적용된다. */}
</Field>
)
}}
>
</Form>
Field
Form 컴포넌트 내부에 위치하며 render props 패턴을 통해 Form 컴포넌트로부터 Field의 상태를 전달받는다. Field의 props로 필드의 이름, 렌더링할 컴포넌트를 전달한다. Field 안에 들어갈 컴포넌트 렌더링에는 입력을 받을 input 같은 요소 외의 다른 어떤 요소가 들어가도 상관없다. 단 Form 컴포넌트에 입력값을 연결하기 위해 props.input.onChange
콜백을 반드시 사용해야 한다.
<Field name="userId">
{props => (
<div>
<label>User Id</label>
<input
{/* props로 전달된 input.onChange를 사용해서 데이터를 Form 컴포넌트에 연결한다. */}
onChange={props.input.onChange}
value={props.input.value}
/>
</div>
)}
</Field>
필드 유효성 검사
Final Form에서 유효성 검사는 단순 함수로 가능하다. 필드의 유효성 검사 함수에는 현재 필드의 값이 전달되고, 함수가 문자열을 리턴하면 필드에 오류가 있음을 의미하며, undefined
가 리턴되면 필드에 문제가 없음을 의미한다.
// undefined, null, 빈 문자열이면 오류
const required = v => !v ? '이 값은 필수입니다' : undefined;
// 값이 반드시 숫자
const mustBeNumber = v => typeof v !== 'number' ? '이 값은 숫자여야 합니다' : undefined;
그리고 복수의 유효성 검사를 조합해서 사용하는 것이 가능하다.
export const composeValidators = (...validators) => value =>
validators.reduce((error, validator) => error || validator(value), undefined);
위의 조합 함수는 팩토리 함수 패턴과 Array.prototype.reduce
함수를 사용했다. reduce 함수에서는 초기값을 undefined
(=오류 없음)로 두고 error
(현재 값)에 유효한 값이 없으면 유효성 검사를 호출하고, 아니면 더 이상 실행하지 않는 방식이다. 아래와 같은 방식으로 사용할 수 있다.
<Field
{/* 예를 들어 나이를 입력받는 필드 */}
name="age"
{/* 이 필드는 undefined, null이 아니며 값이 숫자여야 한다. 그렇지 않으면 onSubmit이 호출되지 않는다.*/}
validate={composeValidators(required, mustBeNumber)}
>
{props => (
<Field name="age">
{props => <input {...props.input} />}
</Field>
)}
</Field>
유효성 검사 함수가 리턴한 문자열은 Field의 render props에서 meta.error
필드로 전달되어 오류 메시지로 사용한다. 그리고 다른 메타데이터와 조합하면 오류 메시지를 표시할 조건을 구체적으로 결정할 수 있다.
<Field
name="age"
validate={composeValidators(required, mustBeNumber)}
>
{({ input, meta }) =>
<div>
<input {...input} />
{/* 에러는 meta props가 가지고 있다. */}
{/* 폼 제출에 실패했고 에러 메시지가 있을 때 메시지를 표시하도록 한다 */}
{meta.submitFailed && meta.error && <span>{meta.error}</span>}
</div>
}
</Field>
보다 상세한 필드 유효성 검사 예제는 아래에서 확인할 수 있다.