Form
사용자에게 값을 입력받는 화면을 일관되게 구성하는 방법이에요. 라벨·도움말·오류 표기, 검증 시점, 필수 입력 표시, 화면 구성을 다뤄요.
Form의 구성
Form은 사용자에게 입력값을 받기 위한 구조로, 크게 네 가지 요소로 이뤄져요.
- Input: 실제 입력을 담당하는 요소예요. Text Field, Checkbox, Radio Group, Select Box, Switch 등이 있어요.
- Field: Header(라벨), Input, Footer(도움말·오류 메시지)를 하나로 묶은 요소예요.
- Fieldset: 여러 Field를 하나의 섹션으로 묶는 컨테이너예요.
- Form: Input, Field, Fieldset의 값을 제출하고 유효성을 검증하는 컨테이너예요.
Field 구성
Field는 Header, Input, Footer 세 영역으로 구성돼요.
오류가 있으면 Footer에 행동 지시형 메시지를 보여주고 입력 테두리를 Critical 색으로 바꿔요.
Helper Text와 Error Message가 함께 있으면 Error Message만 보여줘요.
- Header: Label, 필수 표시(Requirement Mark), 필요하면 오른쪽에 보조 액션(Ghost 버튼이나 아이콘)을 둬요. Label은 명사형으로 쓰고 마침표를 붙이지 않으며, 한 줄로 쓰는 것이 좋아요. 꼭 필요할 때만 두 줄까지 써요.
- Input: 실제 값을 입력받는 컴포넌트가 들어가요. 특수한 포맷이나 마스킹이 필요하면 포맷과 동작을 직접 정의해서 써요.
- Footer: 왼쪽에 Helper Text 또는 Error Message, 오른쪽에 글자 수(Character Count)를 둬요. 메시지는 "휴대폰 번호 10~11자리를 입력해 주세요"처럼 행동 지시형으로 짧게 써요. 글자 수는 입력 길이에 제한이 있을 때만 보여줘요.
제출(Submit)과 검증(Validation)
Form은 입력값이 유효한지 검증하고 값을 전달하는 과정을 거쳐요. 검증은 두 가지 방식이 있고 상황에 따라 섞어 써요.
| 방식 | 시점 | 알맞은 경우 |
|---|---|---|
| 제출 시 검증 | 제출 버튼을 눌렀을 때 전체를 검증해요 | 기본 방식. 비즈니스 로직이나 서버 규칙 검증 |
| 인라인 검증 | 포커스가 떠날 때(blur)나 값이 바뀔 때(change) Field 단위로 검증해요 | 필수 값·글자 수 같은 단순 검증, 필드가 많아 오류가 쌓이기 쉬운 화면, 보안·금융 정보처럼 오입력 위험이 큰 필드 |
제출 시 검증에서 제출 버튼은 조건 충족 여부에 따라 활성화하거나, 항상 활성화해 둔 채 누르면 검증할 수 있어요. 항상 활성화해 둔다면 제출할 때 첫 번째 오류 Field로 포커스를 옮겨 무엇을 고쳐야 하는지 바로 알 수 있게 해요.
제출 중에는 버튼에 Loading 상태를 보여주고 중복 제출을 막아요. 자세한 기준은 Loading 패턴을 참고해요.
필수 입력 여부 표시하기
필수 값은 Requirement Mark(*)로, 선택 값은 '선택' 텍스트로 표시해요.
- 한 화면에서 필드의 2/3 이상이 필수 항목이면 '선택'만 표시하고, 그렇지 않으면
*만 써요. - 한 Form 안에서는 두 표기법을 섞어 쓰지 않아요.
필수 항목이 적으면 * 만 표시해요.
같은 화면에서 Requirement Mark와 '선택' 텍스트를 섞지 않아요.
이탈 시 안내
작성·수정 화면에 들어온 뒤 나가려고 하면, 입력한 값이 지워질 수 있다는 안내를 Alert Dialog로 보여줘요. 값이 바뀌지 않았거나 자동 저장을 쓰는 경우에는 생략할 수 있어요.
단일 화면에서 Form 구성하기
어떤 항목을 생성하거나 수정할 때 쓰는 패턴이에요. 입력할 내용을 한 화면에 모두 보여주고, 하단에 제출 버튼을 고정해요. 생성(Create) Form과 수정(Update) Form은 가능하면 같은 구성을 유지해요.
- 관련 있는 Field는 Fieldset으로 묶고 섹션 제목을 달아요.
- Field 사이 간격은 일정하게 유지해요. 화면 좌우 여백은
$dimension.spacing-x.global-gutter를 써요. - 하단 고정 버튼은 Safe Area 안에 배치해요.
단계별 Form 구성하기
이전 단계에서 고른 옵션에 따라 추가 입력이 필요하거나, 모바일 화면이 여러 Field로 복잡해질 때 쓰는 패턴이에요. 한 단계에 하나의 질문이나 하나의 묶음만 보여주고 "다음" 버튼으로 이어가요.
상황과 플로우에 따라 단일 화면 Form과 섞어서 구성할 수 있어요.
구현 예
React에서는 네이티브 <form> 과 onSubmit 을 기본으로 쓰고, 상태 관리가 복잡하면 React Hook Form 같은 라이브러리와 함께 써요. 컴포넌트는 표준 name, required, aria-invalid 속성을 그대로 받으므로 어떤 폼 라이브러리와도 함께 쓸 수 있어요.
import { ActionButton } from '@/components/ui/action-button';
export function ProfileForm() {
const [pending, setPending] = React.useState(false);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setPending(true);
await save(new FormData(e.currentTarget));
setPending(false);
}
return (
<form onSubmit={onSubmit} className="flex flex-col gap-x6 px-spacing-x-global-gutter">
{/* Field 들 */}
<ActionButton type="submit" size="large" loading={pending}>
저장하기
</ActionButton>
</form>
);
}