이번 편의 결과물: 05편의 인라인 입력 칸 두 개를 <form>으로 묶고, 제목이 없거나 너무 짧으면 저장을 막고 필드 아래에 오류 메시지를 보여줍니다. · 다루는 개념: <form>/FormData, required/minlength와 Constraint Validation API, 커스텀 에러 메시지 표시
이 편에서 만드는 파일
notes-app/
├── index.html (~ 인라인 추가 영역을 <form>으로 교체)
└── src/
└── main.js (~ FormData·Constraint Validation API 적용)개념 정리
form과 FormData
<form>은 입력 요소들을 하나로 묶고, submit 이벤트로 “제출”이라는 명확한 시점을 만듭니다. new FormData(form)은 폼 안의 name 속성이 붙은 입력값을 한 번에 모아 줍니다. 입력마다 변수를 따로 읽지 않아도 됩니다.
const formData = new FormData(form);
formData.get('title'); // input name="title"의 값Constraint Validation API
브라우저는 required, minlength, pattern 같은 속성만으로 기본 검증을 해 줍니다. 자바스크립트에서는 다음 메서드·프로퍼티로 이 검증에 개입합니다.
| 이름 | 역할 |
|---|---|
input.checkValidity() | 그 입력 하나가 규칙을 만족하는지 true/false로 반환 |
form.checkValidity() | 폼 안의 모든 입력을 검사 |
input.setCustomValidity(msg) | 메시지를 넣으면 강제로 무효 상태로 만듦. 빈 문자열을 넣으면 다시 유효 상태로 돌아감 |
input.validationMessage | 현재 무효 사유 메시지(브라우저 기본 문구 또는 setCustomValidity로 넣은 문구) |
form.reportValidity() | 무효한 입력에 브라우저 기본 말풍선을 표시 |
왜 직접 오류 메시지를 그리는가
브라우저 기본 말풍선(reportValidity)은 스타일을 맞추기 어렵습니다. 그래서 실무에서는 novalidate로 브라우저의 자동 검사를 끄지 않고 그대로 두되, 검증 결과를 직접 읽어 필드 아래에 문구를 그리는 방식을 함께 씁니다.
실습
1. index.html 열기
05편에서 만든 .note-composer 영역을 <form>으로 바꿉니다.
2. 코드 작성
<!-- index.html -->
<form id="note-form" class="note-composer" novalidate>
<div class="field">
<label for="new-title">제목</label>
<input id="new-title" name="title" type="text" required minlength="2" />
<p class="field-error" data-error-for="title"></p>
</div>
<div class="field">
<label for="new-body">내용</label>
<input id="new-body" name="body" type="text" />
</div>
<button type="submit">추가</button>
</form>novalidate는 폼 제출 시 브라우저가 말풍선을 자동으로 띄우는 것만 막습니다. required, minlength 속성 자체와 checkValidity()의 판정 결과는 그대로 유지됩니다.
src/main.js에서 addBtn 클릭 리스너를 지우고 submit 리스너로 바꿉니다.
// src/main.js
import { getNotes, addNote, removeNote, updateNote } from './notes.js';
import { renderList } from './render.js';
const listEl = document.querySelector('#note-list');
const form = document.querySelector('#note-form');
const titleInput = form.elements.title;
const titleError = form.querySelector('[data-error-for="title"]');
let editingId = null;
function render() {
renderList(getNotes(), listEl, { editingId });
}
titleInput.addEventListener('input', () => {
titleInput.setCustomValidity('');
});
form.addEventListener('submit', (event) => {
event.preventDefault();
if (titleInput.value.trim().length === 0) {
titleInput.setCustomValidity('제목을 입력하세요.');
} else if (titleInput.value.trim().length < 2) {
titleInput.setCustomValidity('제목은 2자 이상 입력하세요.');
} else {
titleInput.setCustomValidity('');
}
if (!form.checkValidity()) {
titleError.textContent = titleInput.validationMessage;
return;
}
titleError.textContent = '';
const formData = new FormData(form);
addNote({ title: formData.get('title').trim(), body: formData.get('body').trim() });
form.reset();
render();
});
listEl.addEventListener('click', (event) => {
const actionEl = event.target.closest('[data-action]');
if (!actionEl) return;
const card = actionEl.closest('.note-card');
const id = card.dataset.id;
const action = actionEl.dataset.action;
if (action === 'delete') {
removeNote(id);
render();
} else if (action === 'edit') {
editingId = id;
render();
} else if (action === 'cancel') {
editingId = null;
render();
} else if (action === 'save') {
const title = card.querySelector('.edit-title').value.trim();
const body = card.querySelector('.edit-body').value.trim();
updateNote(id, { title, body });
editingId = null;
render();
}
});
render();titleInput의 input 이벤트에서 매번 setCustomValidity('')로 초기화하는 이유는, 이전 제출에서 남긴 무효 상태를 지우지 않으면 사용자가 값을 고쳐도 checkValidity()가 계속 false를 반환하기 때문입니다.
3. 실행
npm run dev확인
- 제목 칸을 비운 채
추가를 누르면 “제목을 입력하세요.”가 칸 아래에 뜨고 목록에는 추가되지 않습니다. - 제목에 한 글자만 입력하면 “제목은 2자 이상 입력하세요.”가 뜹니다.
- 제목을 2글자 이상 입력하면 정상적으로 추가되고, 오류 문구는 사라집니다.
- 카드 안의 인라인 편집(
저장)은 이번 편에서 검증을 적용하지 않았습니다. 05편과 동일하게 빈 값도 저장됩니다.
직접 해보기
내용칸에도maxlength="200"속성을 추가하고, 200자를 넘기면 “내용은 200자 이하로 입력하세요.”를 보여줘 봅니다.- 카드 인라인 편집의
저장에도 제목이 비어 있으면window.alert로 막는 검증을 추가해 봅니다.
정답 보기
<input id="new-body" name="body" type="text" maxlength="200" />} else if (action === 'save') {
const title = card.querySelector('.edit-title').value.trim();
if (!title) {
window.alert('제목을 입력하세요.');
return;
}
const body = card.querySelector('.edit-body').value.trim();
updateNote(id, { title, body });
editingId = null;
render();
}자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 오류 문구가 한 번 뜨면 값을 고쳐도 사라지지 않음 | setCustomValidity로 넣은 메시지를 지우지 않음 | input 이벤트에서 setCustomValidity('')로 매번 초기화한다 |
novalidate를 뺐더니 오류 문구가 브라우저 말풍선과 같이 두 번 보임 | novalidate 없이 커스텀 오류까지 직접 그림 | novalidate로 브라우저 자동 말풍선을 끄고 직접 그린 문구만 사용한다 |
formData.get('title')이 null | input에 name 속성이 없음 | FormData는 name 속성 기준으로 값을 모으므로 반드시 붙인다 |