JavaScript 폼 제출에서 공백 검사와 오류 표시, 초점 복귀를 어떻게 처리하나요?
submit 이벤트에서 입력값의 공백을 정리하고 빈 값을 검사합니다. 오류는 문구와 aria-invalid로 알리고, 정상 제출 뒤에는 입력값을 비운 다음 입력창으로 초점을 돌립니다.
적용 조건·범위: required 속성은 기본 검증을 제공하지만 JavaScript에서도 앱이 요구하는 규칙을 확인할 수 있습니다.
본문 근거: MDN aria-invalid 안내
독립 실습: DOM 폼 검증
JavaScript DOM 폼 만들기: 입력 검증과 접근성 처리 · 독립 실습
작업을 입력하고 추가해 DOM 선택과 이벤트 처리 결과를 확인하세요.
예제 크게 보기 (새 탭)독자 질문: 공백 입력을 막고 정상 입력 뒤 포커스를 어떻게 돌려주는가?
index.html과 app.js를 같은 폴더에 저장하고 브라우저에서 여세요. 추가 패키지는 없습니다.
index.html 전체
<!doctype html>
<html lang="ko">
<head><meta charset="utf-8"><title>작업 입력</title><script src="./app.js" defer></script></head>
<body>
<form id="task-form" novalidate>
<label for="task-title">작업 이름</label>
<input id="task-title" required>
<button type="submit">추가</button>
</form>
<p id="task-message" aria-live="polite"></p>
<ul id="task-list"></ul>
</body>
</html>
app.js 전체
const form = document.querySelector('#task-form');
const input = document.querySelector('#task-title');
const message = document.querySelector('#task-message');
const list = document.querySelector('#task-list');
form.addEventListener('submit', (event) => {
event.preventDefault();
const title = input.value.trim();
if (!title) {
input.setAttribute('aria-invalid', 'true');
message.textContent = '작업 이름을 입력해 주세요.';
input.focus();
return;
}
const item = document.createElement('li');
item.textContent = title;
list.append(item);
input.value = '';
input.setAttribute('aria-invalid', 'false');
message.textContent = '작업을 추가했습니다.';
input.focus();
});
예상 결과: 빈 값 또는 공백만 입력해 제출하면 오류 문구가 표시되고 목록은 비어 있습니다. ‘공부’를 입력해 제출하면 목록에 공부가 한 번 추가되고 입력창은 비워진 뒤 다시 포커스를 받습니다.
직접 풀어볼 문제
공백만 입력했을 때 목록이 늘지 않고 aria-invalid가 true인지 DevTools에서 확인하세요. 이어서 정상 입력을 제출해 aria-invalid가 false로 바뀌는지, 입력창이 비워지고 다시 포커스를 받는지 확인하세요. Enter 키 제출과 버튼 제출에서 항목이 각각 한 번만 추가되는지도 비교해 보세요.
이 독립 예제는 오류 상태와 메시지 갱신을 연습합니다. 입력창에 오류 메시지 요소를 aria-describedby로 연결한 HTML은 TaskFlow 폼 예제에서 확인하세요. aria-invalid 자체의 의미는 MDN aria-invalid 안내를 참고하세요. 문서 확인일: 2026년 10월 6일.
검증 상태: 독립 파일의 실제 브라우저 제출·키보드 조작은 미실행·미확인입니다. 다음 회차는 목록 클릭을 이벤트 위임으로 처리합니다.
DOM 요소 선택
<input id="task-title" name="title" />
const titleInput = document.querySelector('#task-title');
querySelector()는 조건에 맞는 첫 요소를 반환하고 찾지 못하면 null을 반환합니다.
필수 요소라면 초기화 단계에서 존재 여부를 확인해 잘못된 HTML 구조를 빠르게 발견할 수 있습니다.
if (!titleInput) {
throw new Error('task-title input을 찾을 수 없습니다.');
}
form은 submit 이벤트를 기준으로 처리합니다
버튼의 click만 듣는 대신 form의 submit을 처리하면 Enter 키 제출도 같은 코드 경로를 사용합니다.
form.addEventListener('submit', (event) => {
event.preventDefault();
});
preventDefault()는 기본 form 제출로 페이지가 이동하거나 새로고침되는 동작을 막습니다.
공백 입력 검증
const title = titleInput.value.trim();
if (!title) {
showMessage('작업 이름을 입력해 주세요.', true);
return;
}
required 속성은 기본 검증을 제공하지만 JavaScript에서도 앱이 요구하는 규칙을 확인할 수 있습니다.
오류 상태를 화면에 전달하기
<p id="task-message" aria-live="polite"></p>
function showMessage(message, isError = false) {
messageElement.textContent = message;
titleInput.setAttribute('aria-invalid', String(isError));
}
aria-live="polite"는 화면에 새로 나타난 상태 메시지를 보조 기술이 적절한 시점에 알릴 수 있도록 돕습니다.
입력 오류를 빨간색만으로 전달하지 않고 텍스트 메시지를 함께 제공합니다.
성공 뒤 포커스 복귀
titleInput.value = '';
titleInput.focus();
연속해서 작업을 입력하는 사용자는 마우스를 다시 클릭하지 않아도 됩니다.

자주 발생하는 오류
버튼 click과 form submit을 둘 다 등록합니다
같은 추가 함수가 두 번 호출될 수 있습니다. 제출 경로는 form submit 하나로 통일합니다.
오류 뒤 aria-invalid를 되돌리지 않습니다
정상 입력이 성공했으면 false로 바꿉니다.
selector가 HTML과 다릅니다
#task-form, #task-title, #task-message가 실제 id와 정확히 일치하는지 확인합니다.
학습 완료 체크리스트
- DOM 요소를 선택할 수 있습니다.
- submit 이벤트를 처리할 수 있습니다.
- 공백 입력을 차단할 수 있습니다.
- 오류 메시지를 텍스트와 접근성 상태로 전달할 수 있습니다.
- 성공 후 입력값과 포커스를 정리할 수 있습니다.
독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript로 화면에 동작을 붙입니다.
위에서 추가한 작업 관리 데모 HTML/CSS와 함께 아래 JavaScript 파일을 적용하면 이번 체크포인트가 완성됩니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
JavaScript DOM 폼 만들기: 입력 검증과 접근성 처리 · 단계별 TaskFlow
작업 이름을 추가하고 상태 요약을 확인하세요. 목록 렌더링은 다음 단계에 추가됩니다.
예제 크게 보기 (새 탭)변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.
시작본과 이번 변경
JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준의 완료 체크포인트를 시작본으로 사용합니다.
index.html의 features 뒤·pricing 앞에 작업 관리 section을 추가하고 style.css에 제시한 스타일을 더합니다. js/task-view.js를 만들고 app.js에서 addTask와 bindTaskForm을 연결합니다. 공백 제출·Enter 제출·정상 추가·포커스 복귀와 Console 요약을 확인합니다.
연속 프로젝트 선수 조건
javascript-step-06-reduce-summary체크포인트- 웹 퍼블리싱 최종 TaskFlow의
main구조 - form, label, button의 기본 HTML 의미
이전 체크포인트
javascript-step-06-reduce-summary
파일 구조
index.html
style.css
package.json
js/
app.js
task-store.js
task-view.js
TaskFlow 작업 관리 데모 HTML
기존 features 뒤, pricing 앞에 다음 section을 추가합니다.
<section
class="task-demo"
id="task-demo"
aria-labelledby="task-demo-title"
>
<h2 id="task-demo-title">작업 관리 데모</h2>
<p>JavaScript 과정에서 이 영역을 단계적으로 동작하게 만듭니다.</p>
<form class="task-form" id="task-form" novalidate>
<div class="form-field">
<label for="task-title">작업 이름</label>
<input
id="task-title"
name="title"
type="text"
maxlength="80"
required
aria-describedby="task-message"
/>
</div>
<button type="submit">작업 추가</button>
</form>
<p id="task-message" aria-live="polite"></p>
<div id="task-summary" aria-live="polite"></div>
<ul class="task-list" id="task-list"></ul>
</section>
novalidate는 이번 실습에서 직접 오류 메시지를 제어하기 위해 사용합니다. 실제 서비스에서는 native validation을 그대로 활용하는 선택도 가능합니다.
최소 CSS
기존 디자인 시스템을 유지하면서 필요한 레이아웃만 추가합니다.
.task-demo {
padding: 24px;
border: 1px solid #d7d7d7;
background: #fff;
}
.task-form {
display: grid;
gap: 12px;
max-width: 560px;
}
.task-list {
margin: 24px 0 0;
padding: 0;
list-style: none;
}
이번 회차 변경 코드와 전체본 연결
위에서 추가한 작업 관리 데모 HTML/CSS와 함께 아래 JavaScript 파일을 적용하면 이번 체크포인트가 완성됩니다.
전체 파일·코드 보기
js/task-view.js
export function bindTaskForm(onAdd) {
const form = document.querySelector('#task-form');
const titleInput = document.querySelector('#task-title');
const message = document.querySelector('#task-message');
if (!form || !titleInput || !message) {
throw new Error('TaskFlow 작업 폼 요소를 찾을 수 없습니다.');
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const title = titleInput.value.trim();
if (!title) {
titleInput.setAttribute('aria-invalid', 'true');
message.textContent = '작업 이름을 입력해 주세요.';
titleInput.focus();
return;
}
onAdd(title);
titleInput.value = '';
titleInput.setAttribute('aria-invalid', 'false');
message.textContent = '작업을 추가했습니다.';
titleInput.focus();
});
}
js/app.js
import { addTask, getTaskSummary } from './task-store.js';
import { bindTaskForm } from './task-view.js';
bindTaskForm((title) => {
const task = addTask(title);
console.log('추가한 작업:', task);
console.log('현재 요약:', getTaskSummary());
});
이번 회차에서는 목록 렌더링까지 하지 않습니다. 추가된 데이터와 요약은 Console로 확인합니다.
검증
- 빈 값 제출 → 오류 문구 표시
- 공백만 입력 → 오류 문구 표시
- 정상 입력 → 작업 추가
- 입력값 초기화
- input으로 포커스 복귀
- Enter 키 제출도 동일하게 동작
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 javascript-step-06-reduce-summary의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- index.html에 task-demo를 추가하고 style.css를 보강합니다. view와 app 전체 파일을 적용합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
index.html— 전체 교체style.css— 전체 교체CHANGES.md— 전체 교체README.md— 전체 교체js/app.js— 전체 교체js/task-view.js— 추가
실행과 확인
npm start
npm test
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: 공백 제출은 aria-invalid=true와 안내문, 유효 제목 제출은 요약 total 증가와 입력 초기화입니다. 이 단계는 목록 렌더링 전입니다.
실제 검증: JavaScript 모듈·DOM·경계값 실행 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
index.html — 전체 파일
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>
TaskFlow - 간단한 작업 정리 서비스
</title>
<link href="./style.css" rel="stylesheet"/>
<meta content="TaskFlow의 주요 기능과 요금제, 문의 방법을 확인할 수 있는 작업 관리 서비스 소개 페이지입니다." name="description"/>
</head>
<body>
<a class="skip-link" href="#main">
본문으로 바로가기
</a>
<header class="site-header">
<div class="page site-header__inner">
<a class="brand" href="#home">
TaskFlow
</a>
<nav aria-label="주요 메뉴" class="nav">
<a href="#features">
기능
</a>
<a href="#pricing">
요금제
</a>
<a href="#contact">
문의
</a>
<a href="#faq">
FAQ
</a>
</nav>
</div>
</header>
<main class="page" id="main" tabindex="-1">
<div class="service-layout">
<div>
<section class="hero" id="home">
<div>
<h1>
해야 할 일을 한눈에 정리하세요
</h1>
<p>
TaskFlow는 오늘의 작업을 빠르게 정리하는 서비스입니다.
</p>
<a class="button-link" href="#contact">
시작 문의하기
</a>
</div>
<img alt="TaskFlow 서비스 소개 페이지의 기능·문의 화면" class="hero__image" height="800" src="./images/hero.webp" width="1200"/>
</section>
<section id="features">
<h2>
주요 기능
</h2>
<div class="feature-grid">
<article class="card feature-card">
<span class="feature-card__badge">
NEW
</span>
<h3>
빠른 작업 추가
</h3>
<p>
해야 할 일을 바로 기록할 수 있습니다.
</p>
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
</article>
<article class="card">
<h3>
간단한 분류
</h3>
<p>
해야 할 일을 구분합니다.
</p>
</article>
<article class="card">
<h3>
진행 확인
</h3>
<p>
완료 여부를 확인합니다.
</p>
</article>
<article class="card task-preview">
<div class="task-preview__content">
<h3>
아주 긴 작업 제목과 URL
</h3>
<p>
https://example.com/very-long-task-url-without-normal-break-points-123456789
</p>
</div>
<a href="#contact">
문의
</a>
</article>
</div>
</section>
<section class="faq" id="faq">
<h2>
자주 묻는 질문
</h2>
<details>
<summary>
TaskFlow는 어떤 서비스인가요?
</summary>
<p>
해야 할 일을 빠르게 정리하는 작업 관리 서비스 예제입니다.
</p>
</details>
<details>
<summary>
모바일에서도 볼 수 있나요?
</summary>
<p>
작은 화면부터 설계한 반응형 레이아웃을 사용합니다.
</p>
</details>
<details>
<summary>
문의는 어디서 하나요?
</summary>
<p>
아래 문의 폼을 이용할 수 있습니다.
</p>
</details>
</section>
<section id="pricing">
<h2>
요금제
</h2>
<div class="pricing-grid">
<article class="card">
<h3>
Basic
</h3>
<p>
개인 학습용
</p>
</article>
<article class="card">
<h3>
Pro
</h3>
<p>
더 많은 프로젝트
</p>
</article>
</div>
<div aria-label="요금제 비교표 가로 스크롤" class="table-scroll" role="region" tabindex="0">
<table class="pricing-table">
<caption>
TaskFlow 요금제 기능 비교
</caption>
<thead>
<tr>
<th scope="col">
항목
</th>
<th scope="col">
Basic
</th>
<th scope="col">
Pro
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
프로젝트
</th>
<td>
3개
</td>
<td>
무제한
</td>
</tr>
<tr>
<th scope="row">
작업 보관
</th>
<td>
30일
</td>
<td>
무제한
</td>
</tr>
<tr>
<th scope="row">
팀 공유
</th>
<td>
없음
</td>
<td>
지원
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="contact">
<h2>
문의하기
</h2>
<form>
<div class="form-field">
<label for="name">
이름
</label>
<input autocomplete="name" id="name" name="name" type="text"/>
</div>
<div class="form-field">
<label for="email">
이메일
</label>
<input autocomplete="email" id="email" name="email" type="email"/>
</div>
<div class="form-field">
<label for="message">
문의 내용
</label>
<textarea id="message" name="message" rows="5"></textarea>
</div>
<button type="submit">
문의 보내기
</button>
</form>
</section>
<div class="feature-slot">
<article class="card feature-card">
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
<div>
<h3>
작업 카드
</h3>
<p>
넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
</p>
</div>
</article>
</div>
</div>
<aside aria-label="기능 미리보기">
<div class="feature-slot">
<article class="card feature-card">
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
<div>
<h3>
작업 카드
</h3>
<p>
넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
</p>
</div>
</article>
</div>
</aside>
</div>
<section
class="task-demo"
id="task-demo"
aria-labelledby="task-demo-title"
>
<h2 id="task-demo-title">작업 관리 데모</h2>
<p>JavaScript 과정에서 이 영역을 단계적으로 동작하게 만듭니다.</p>
<form class="task-form" id="task-form" novalidate>
<div class="form-field">
<label for="task-title">작업 이름</label>
<input
id="task-title"
name="title"
type="text"
maxlength="80"
required
aria-describedby="task-message"
/>
</div>
<button type="submit">작업 추가</button>
</form>
<p id="task-message" aria-live="polite"></p>
<div id="task-summary" aria-live="polite"></div>
<ul class="task-list" id="task-list"></ul>
</section>
</main>
<footer class="site-footer">
<div class="page">
<p>
© TaskFlow
</p>
</div>
</footer>
<a class="help-button" href="#contact">
문의
</a>
<script type="module" src="./js/app.js"></script></body>
</html>
style.css — 전체 파일
:root {
color: #171717;
background: #f5f5f5;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
Arial,
sans-serif;
line-height: 1.6;
color: #171717;
background: #f5f5f5;
}
.page {
width: min(
100% - 32px,
960px
);
margin-inline: auto;
}
.site-header,
.site-footer {
padding-block: 20px;
}
main {
padding-block: 48px;
}
section + section {
margin-top: 64px;
}
.card {
padding: 24px;
border: 1px solid #d7d7d7;
background: #fff;
}
form {
display: grid;
gap: 16px;
}
.form-field {
display: grid;
gap: 8px;
}
input,
textarea {
width: 100%;
min-height: 44px;
padding: 10px 12px;
border: 1px solid #b8b8b8;
background: #fff;
}
button {
min-height: 44px;
padding: 10px 16px;
border: 1px solid #171717;
background: #171717;
color: #fff;
}
a { color: inherit; }
input, textarea, button { font: inherit; }
h1 { font-size: 32px; font-weight: 700; }
h2 { font-size: 24px; }
h3 { font-size: 20px; }
.site-header__inner {
display: flex;
flex-direction: column;
gap: 12px;
}
.nav {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
}
.feature-list {
display: flex;
flex-direction: column;
gap: 16px;
}
@media (min-width: 48rem) {
.site-header__inner {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.feature-list {
flex-direction: row;
}
.feature-list > * {
flex: 1;
}
}
.site-header {
position: sticky;
top: 0;
z-index: 10;
background: #f5f5f5;
border: 1px solid #ddd;
}
.feature-card {
position: relative;
}
.feature-card__badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 8px;
border: 1px solid #171717;
background: #fff;
font-size: 12px;
}
.help-button {
position: fixed;
right: 16px;
bottom: 16px;
min-width: 44px;
min-height: 44px;
padding: 10px 14px;
border: 1px solid #171717;
background: #171717;
color: #fff;
text-decoration: none;
}
.faq {
display: grid;
gap: 12px;
}
.faq details {
border: 1px solid #d7d7d7;
background: #fff;
}
.faq summary {
padding: 16px;
cursor: pointer;
font-weight: 700;
}
.faq details p {
margin: 0;
padding: 0 16px 16px;
}
.faq summary:focus-visible {
outline: 2px solid #171717;
outline-offset: 2px;
}
.feature-grid,
.pricing-grid {
display: grid;
grid-template-columns:
repeat(
auto-fit,
minmax(
min(100%, 220px),
1fr
)
);
gap: 16px;
}
.task-preview {
display: flex;
align-items: flex-start;
gap: 12px;
}
.task-preview__content {
min-width: 0;
flex: 1;
}
.task-preview__content p {
overflow-wrap: anywhere;
}
.settings-layout {
display: grid;
grid-template-columns:
180px
minmax(0, 1fr);
gap: 24px;
}
.table-scroll {
overflow-x: auto;
}
.pricing-table {
width: 100%;
min-width: 640px;
border-collapse: collapse;
background: #fff;
}
.pricing-table caption {
margin-bottom: 12px;
text-align: left;
font-weight: 700;
}
.pricing-table th,
.pricing-table td {
padding: 12px;
border: 1px solid #d7d7d7;
text-align: left;
}
img {
display: block;
max-width: 100%;
}
.hero__image {
width: 100%;
height: auto;
}
.feature-card__image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.service-layout {
display: grid;
gap: 24px;
}
@media (
min-width: 64rem
) {
.service-layout {
grid-template-columns:
minmax(0, 1fr)
280px;
}
}
.feature-slot {
container-type:
inline-size;
}
.feature-card {
display: grid;
gap: 16px;
}
@container (
min-width: 30rem
) {
.feature-card {
grid-template-columns:
180px
minmax(0, 1fr);
align-items: center;
}
}
.service-layout > * { min-width: 0; }
:root {
--z-header: 10;
--z-floating: 20;
}
.site-header {
position: sticky;
top: 0;
z-index:
var(--z-header);
}
.help-button {
position: fixed;
right: 16px;
bottom: 16px;
z-index:
var(--z-floating);
}
:focus-visible {
outline: 2px solid #171717;
outline-offset: 3px;
}
.skip-link {
position: absolute;
left: 16px;
top: -100px;
z-index: 100;
padding: 10px 12px;
background: #171717;
color: #fff;
}
.skip-link:focus {
top: 16px;
}
section[id], main[id] { scroll-margin-top: 8rem; }
body { padding-bottom: 5rem; }
.hero { display: grid; gap: 24px; }
@media (min-width: 48rem) { .hero { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; } }
.button-link { display: inline-block; padding: 10px 16px; border: 1px solid #171717; background: #171717; color: #fff; }
.task-demo {
padding: 24px;
border: 1px solid #d7d7d7;
background: #fff;
}
.task-form {
display: grid;
gap: 12px;
max-width: 560px;
}
.task-list {
margin: 24px 0 0;
padding: 0;
list-style: none;
}
js/app.js — 전체 파일
import { addTask, getTaskSummary } from './task-store.js';
import { bindTaskForm } from './task-view.js';
bindTaskForm((title) => {
const task = addTask(title);
console.log('추가한 작업:', task);
console.log('현재 요약:', getTaskSummary());
});
js/task-view.js — 전체 파일
export function bindTaskForm(onAdd) {
const form = document.querySelector('#task-form');
const titleInput = document.querySelector('#task-title');
const message = document.querySelector('#task-message');
if (!form || !titleInput || !message) {
throw new Error('TaskFlow 작업 폼 요소를 찾을 수 없습니다.');
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const title = titleInput.value.trim();
if (!title) {
titleInput.setAttribute('aria-invalid', 'true');
message.textContent = '작업 이름을 입력해 주세요.';
titleInput.focus();
return;
}
onAdd(title);
titleInput.value = '';
titleInput.setAttribute('aria-invalid', 'false');
message.textContent = '작업을 추가했습니다.';
titleInput.focus();
});
}
완료 체크포인트
javascript-step-07-dom-form
다음 학습
다음 글에서는 이 폼과 store를 연결해 작업 추가·완료·삭제·필터·검색이 실제 화면에서 동작하는 TaskFlow 핵심 버전을 완성합니다.
다음 글: JavaScript 투두리스트 만들기: 상태와 이벤트 위임으로 완성하기
글 소개·학습 준비·전체 목차

JavaScript · 7 / 15 · 필수 실습 · DOM 폼과 입력 검증
폼의 submit 이벤트에서 입력값을 검사하고, 잘못된 입력은 오류 문구와 aria-invalid로 알립니다. 정상 제출 뒤에는 입력값을 비우고 입력창으로 포커스를 돌려줍니다. 이 글에서는 공백만 있는 값을 막는 작은 폼으로 입력 검증과 접근성 상태 처리를 연습합니다.
이전 TaskFlow 프로젝트 없이도 독립 실습의 index.html과 app.js 두 파일로 시작할 수 있습니다. 빈 값·공백·정상 입력을 각각 제출해 오류 상태와 포커스를 비교하세요. TaskFlow를 이어 만드는 적용 방법과 시작·완료 ZIP은 하단 프로젝트 실습에서 확인합니다.
이번 글에서 배우는 내용
querySelector()- form
submit이벤트 preventDefault()- input
value와trim() - 입력 오류 메시지
aria-invalid와aria-live- 제출 후 입력 초기화와 포커스
- 이벤트 리스너 중복 방지
먼저 알아야 할 내용
form·label·button의 HTML 의미와 JavaScript 변수·함수·이벤트를 알면 됩니다. 아래 두 파일만 같은 폴더에 저장하면 단독 폼을 실행할 수 있습니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.