소개
Still & Spoke는 토요일에 소규모 도예 워크숍을 엽니다. 스튜디오 페이지는 준비되어 있지만 신청 폼에는 보이는 레이블, 브라우저 기본 입력 검사, 조건부 좌석 메모, 좁은 화면에서도 키보드로 쓸 수 있는 배치가 필요합니다. 성공 문구만 표시하는 대신 실제 로컬 신청을 만듭니다. 작업을 수정하기 전에 독립적인 Study form에서 완성 예시를 경험합니다.
기본 HTML 요소, CSS 선택자와 배치, JavaScript 객체·함수·DOM 선택·이벤트를 알아야 합니다. 이 실습에서는 레이블 연결, 폼 제약, 접근 가능한 설명, 조건부 required, 보이는 초점, 여기 사용하는 간단한 반응형 규칙을 배웁니다. 첫 실습은 준비된 전송 처리기와 로컬 API를 사용하며 다음 실습에서 처리기를 만듭니다. 모든 인물과 주소는 합성 정보이며 이메일이나 결제는 발생하지 않습니다. 실용적인 사용성 연습이지 완전한 접근성 인증은 아닙니다. 50분은 읽기와 일반적인 실수 대응을 포함한 저자 추정치입니다.
명령은 Workspace, 사이트는 Studio를 사용합니다. 수정 가능한 파일은 /home/labex/project/stillspoke/form.html, validity.js, form.css입니다. 다른 파일, 준비된 API, 정원 규칙과 기존 방문자는 보존합니다. 각 실습은 독립적으로 초기화됩니다. 서버는 form.html의 정확한 내용을 브랜드 페이지 틀에 넣으며 빠진 레이블을 채우거나 작업을 재생성하지 않습니다. 새로 고침 후 로드가 끝나야 폼을 사용하세요. 문서 언어와 관계없이 사이트 레이블은 영어입니다.
각 컨트롤에 보이는 이름 붙이기
이 단계에서는 완성 예시를 키보드로 사용한 다음 작업 페이지의 각 컨트롤을 보이는 레이블 및 설명과 연결합니다.
Studio에서 페이지 링크 Study form을 선택합니다. 사이트 내부 이동이며 LabEx Tab 전환이 아닙니다. 예시는 자체 API와 데이터베이스를 사용하므로 작업을 완료 처리하거나 작업의 자리를 차지하지 않습니다. Your name에 보이는 윤곽선이 생길 때까지 Tab을 누릅니다. Example Guest를 입력하고 Tab, sample@example.test를 입력합니다. Tab으로 Session에 가서 C로 Clay, slowly / 10:00을 고르고 선택 제목을 읽은 뒤 진행합니다. 기본 선택 상자는 옵션의 시작 부분 입력을 지원합니다. Tab으로 지원 체크박스, Tab으로 선택 사항인 메모, Tab으로 Reserve my place에 갑니다. Enter를 한 번 누릅니다. Submission result에 Reserved for Example Guest와 서버가 발급한 Confirmation SS-…가 나올 때까지 기다립니다. 수동적인 전송 알림은 사라지고 결과 영역으로 초점이 옮겨져 완료 결과를 쉽게 찾습니다.
아래는 별도 제목과 진짜 확인 번호를 갖춘 결과입니다. 발급 번호는 달라집니다.

예시 페이지의 Studio records를 선택하고 같은 이름, 주소, 읽기 쉬운 시간과 번호를 찾습니다. 이 페이지는 브라우저 결과 문구가 아니라 접수된 레코드를 읽습니다. Back to signup, Work form 순으로 작업 페이지에 돌아옵니다. 작업은 여전히 미완성이며 새 작업 신청도 없습니다. 예시 결과는 성과로 세지 않습니다.
Workspace로 전환합니다. cd는 이 터미널의 현재 디렉터리를 바꿉니다. 절대 경로는 /로 시작하며 pwd는 그 디렉터리를 출력합니다. 다음 명령은 편집 대상을 분명히 합니다.
cd /home/labex/project/stillspoke
pwd
예상 출력:
/home/labex/project/stillspoke
폼의 name 값은 API 키인 name, email, session, support, note입니다. HTML id는 페이지의 한 요소를 식별합니다. 레이블의 for는 입력의 id를 가리키므로 보이는 레이블을 클릭하거나 보조 기술로 읽을 때 컨트롤을 식별할 수 있습니다. 여기서 Your name은 for="person"을 쓰지만 전송 키는 name입니다. 두 속성의 역할은 다릅니다. 체크박스를 레이블 안에 넣으면 암시적 연결이 생깁니다. autocomplete="name"과 autocomplete="email"은 브라우저가 채우기 제안을 할 정보 종류를 나타낼 뿐 데이터를 보내지 않습니다. 입력 중 사라지는 자리표시자는 보이는 레이블을 대신할 수 없습니다.
required는 요청을 보내기 전에 빈 값을 거부합니다. type="email"은 잘못된 주소 문법을 거부하도록 하지만 주소 소유자는 증명하지 않습니다. minlength="2"는 이름을 두 글자 이상 요구합니다. 맨 앞 빈 옵션 때문에 실제 회차를 선택해야 합니다. 옵션의 value는 안정적인 서버 식별자이고 표시 제목은 사람을 위한 것입니다. 순서로 관계를 추정하지 마세요.
aria-describedby는 공백으로 구분한 ID를 담아 각 필드를 상시 도움말과 처음에는 빈 오류 문단에 연결합니다. 도움말은 실패 전에 규칙을 설명하고 오류는 서버 거부 뒤 이유를 덧붙입니다. 색상뿐 아니라 명확한 글도 사용합니다. aria-hidden="true"는 장식 화살표만 보조 기술에서 숨깁니다. ↗는 HTML 숫자 문자 참조이며 ASCII 원문이 브라우저에서는 화살표가 됩니다. Sending 문단의 role="status"는 수동적 알림용입니다. 결과 영역의 aria-labelledby는 제목 ID를 참조하여 영역에 이름을 줍니다. 처음의 hidden은 표시할 내용이 생길 때까지 빈 결과를 숨깁니다. 별도 영역의 tabindex="-1"는 일반 Tab 순서에 정지점을 추가하지 않으면서 스크립트로 초점을 줄 수 있게 합니다. 상태 알림 자체에는 초점을 주지 않습니다.
다음 명령은 form.html만 바꿉니다. cat은 이어지는 텍스트를 쓰며 >는 지정 파일을 덮어씁니다. << 뒤 따옴표로 감싼 HTML은 here-document의 시작으로, HTML만 있는 마지막 줄까지 내용이 그대로 파일에 들어갑니다. 따옴표는 shell이 HTML 문자를 해석하지 않게 합니다. 끝 마커를 포함한 전체 블록을 붙여 넣으세요. shell 프롬프트가 돌아오면 저장 완료이며 별도 저장 키는 없습니다. Markdown 백틱은 붙이지 않습니다. 뒤의 작은 JavaScript와 CSS 파일도 같은 방식입니다.
cd /home/labex/project/stillspoke
cat > form.html <<'HTML'
<form id="signup">
<div class="pair">
<div class="field">
<label for="person">Your name (required)</label>
<input id="person" name="name" required minlength="2" autocomplete="name"
aria-describedby="name-help name-error">
<small id="name-help">At least two characters.</small>
<p class="error" id="name-error"></p>
</div>
<div class="field">
<label for="email">Email (required)</label>
<input id="email" name="email" type="email" required autocomplete="email"
aria-describedby="email-help email-error">
<small id="email-help">Practice address: you@example.test</small>
<p class="error" id="email-error"></p>
</div>
</div>
<div class="field">
<label for="session">Session (required)</label>
<select id="session" name="session" required aria-describedby="session-help session-error">
<option value="">Choose a time</option>
<option value="clay-morning">Clay, slowly / 10:00</option>
<option value="clay-afternoon">Clay, slowly / 14:00</option>
</select>
<small id="session-help">Choose a listed time. The afternoon session is full.</small>
<p class="error" id="session-error"></p>
</div>
<label class="check"><input type="checkbox" name="support"> I need seating assistance</label>
<div class="field">
<label for="note">Seating note (required when assistance is selected)</label>
<textarea id="note" name="note" rows="2" aria-describedby="note-help note-error"></textarea>
<small id="note-help">When assistance is selected, describe the help in at least 5 characters.</small>
<p class="error" id="note-error"></p>
</div>
<button type="submit">Reserve my place <span aria-hidden="true">↗</span></button>
<p id="progress" role="status"></p>
<section id="result" aria-labelledby="result-heading" tabindex="-1" hidden>
<h3 id="result-heading">Submission result</h3>
<p id="message"></p>
</section>
</form>
HTML
Studio로 돌아가 작업 페이지를 새로 고칩니다. 입력 후에도 레이블을 읽어 봅니다. Email을 클릭해 입력으로 초점이 옮겨지는 것을 보고 Tab으로 문서 순서를 따릅니다. 빈 폼에서 Reserve my place를 실행하면 브라우저는 Your name으로 이동해 필수임을 알려야 합니다. 한 글자를 넣고 다시 시도해 길이 규칙을 봅니다. Morgan Vale 같은 이름을 쓰고 not-an-email로 다른 메일 형식 거부를 봅니다. 이는 HTTP 422가 아닌 기본 검증 메시지이며 브라우저는 아직 신청 요청을 보내지 않았습니다. 수정은 필드에 초점이 있을 때 Ctrl+A 또는 Cmd+A로 선택하여 바꿉니다. 오타 때문에 VM을 초기화하지 마세요.
지금은 지원을 선택해도 메모가 선택 사항입니다. 다음에 그 동작을 추가합니다. 좁은 화면용 스타일도 아직 없습니다. 자체 확인: 다섯 컨트롤 모두 연결된 보이는 레이블이 있고 도움말이 남으며 빈 필수 값은 전송할 수 없어야 합니다.
체크박스에 따라 좌석 메모 바꾸기
좌석 지원을 요청할 때만 메모가 필수가 되도록 하되 입력한 글은 보존합니다.
체크박스는 불리언 checked를 제공합니다. 선택 시 true, 아니면 false입니다. 입력과 텍스트 영역은 HTML 제약에 해당하는 required 속성을 가집니다. STUDIO는 scenario.js가 읽는 준비된 설정 객체이며 이 워크숍의 noteMin은 5입니다. 이후의 모든 워크숍이 같다고 가정하지 말고 값을 읽습니다. 설정은 시나리오 데이터일 뿐 조건부 규칙을 구현하지 않습니다.
querySelector는 CSS 선택자와 맞는 첫 요소를 반환하며 [name="support"]와 [name="note"]는 해당 속성에 맞습니다. API name으로 컨트롤을 선택합니다. 함수는 체크가 바뀔 때 두 기본 제약을 갱신합니다. condition ? first : second는 조건이 참이면 first, 아니면 second를 택하는 조건 연산자입니다. 따라서 지원 시 최소 길이는 5, 아니면 0입니다. updateNote()를 한 번 호출해 로드 시 상태도 초기화합니다. change 리스너는 사용자가 전환한 뒤 다시 실행합니다. .value가 아니라 제약을 바꾸므로 해제해도 초안은 지워지지 않습니다.
Workspace에서 validity.js를 바꿉니다. JS는 앞의 HTML과 같은 마커입니다.
cd /home/labex/project/stillspoke
cat > validity.js <<'JS'
const support = document.querySelector('[name="support"]');
const note = document.querySelector('[name="note"]');
function updateNote() {
note.required = support.checked;
note.minLength = support.checked ? STUDIO.noteMin : 0;
}
support.addEventListener('change', updateNote);
updateNote();
JS
Studio로 돌아가 새로 고칩니다. Tab으로 두 글자 이상 이름, morgan@example.test 같은 올바른 문법의 주소, Clay, slowly / 10:00을 입력·선택합니다. I need seating assistance에서 Space로 선택합니다. 메모를 비워 두고 Reserve my place를 실행합니다. 이제 브라우저가 Seating note에 초점을 주고 값을 요청해야 합니다. near를 넣고 Tab으로 나와 다시 보내면 네 글자가 다섯보다 짧아 거부됩니다. minlength는 사용자가 입력한 글에 적용되므로 스크립트 대입 대신 실제로 입력합니다.
필요하면 Shift+Tab으로 체크박스에 돌아가 Space로 해제합니다. 메모는 선택 사항이며 near는 남습니다. 다시 선택하고 A seat near the entrance로 바꿉니다. 다음 단계를 위해 입력을 유지합니다. 새로 고침은 미전송 입력을 버리지만 LabEx Tab 전환은 이 페이지를 열어 둡니다.
체크가 동작하지 않는다면 Workspace에서 실제 저장 파일을 표시합니다.
cd /home/labex/project/stillspoke
cat validity.js
짧은 블록과 비교하며 checked 철자와 마지막 updateNote();도 봅니다. 필요하면 전체 블록으로 바꾸고 Studio를 새로 고칩니다. 스크립트 누락이나 JavaScript 오타는 브라우저가 빈 메모를 거부하는 것과 다릅니다. 신청이 없다는 사실만으로 제약이 작동한다고 주장하지 마세요. 자체 확인: 체크가 필수 여부를 바꾸고 선택 시만 짧은 메모를 거부하며 기존 글은 보존합니다.
읽기 쉬운 폼과 보이는 초점 유지하기
배치를 마무리하고 작업 페이지에서 실제 신청을 한 뒤 대응하는 접수 기록을 읽습니다.
두 열 폼은 접힐 수 있어야 합니다. CSS Grid의 1fr 1fr은 이름과 이메일에 같은 몫을 주고 min-width: 0은 축소를 허용합니다. 최대 800픽셀에서 미디어 쿼리가 이 쌍을 한 열로 바꿉니다. width: 100%와 box-sizing: border-box는 패딩·테두리를 가용 폭 안에 포함합니다. input:not([type="checkbox"])는 전체 폭 규칙에서 체크박스를 제외해 작은 사각형을 유지합니다. 호버 없이 글을 읽을 수 있습니다. 준비된 페이지 틀은 작은 화면에서 이야기를 폼 위로 쌓으며 이 규칙은 폼을 그 안에 맞춥니다.
:focus-visible은 키보드 초점에 보이는 윤곽선을 줍니다. 색상만 신호는 아닙니다. 필수 문구, 오류 글과 윤곽선 모양도 중요합니다. 동등하게 명확한 초점 테두리나 그림자도 유효하지만 표시를 모두 없애면 안 됩니다. [aria-invalid="true"]는 서버가 무효로 표시한 필드의 스타일이고 .error:empty는 빈 오류 문단만 숨기며 레이블이나 필요한 설명은 숨기지 않습니다. overflow-wrap: anywhere는 긴 확인 번호나 합성 주소를 줄바꿈합니다. 동작 감소 설정은 애니메이션을 줄이려는 사람에게 부드러운 이동을 추가하지 않게 합니다. 이 폼은 애니메이션이 필요 없습니다.
Workspace에서 form.css를 바꿉니다.
cd /home/labex/project/stillspoke
cat > form.css <<'CSS'
form { margin-top: 28px; }
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field { min-width: 0; margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; font-size: 13px; font-weight: bold; }
input:not([type="checkbox"]), select, textarea {
width: 100%; box-sizing: border-box; padding: 11px 12px;
border: 1px solid #89988a; border-radius: 3px;
font: inherit; font-size: 15px; color: #243d35; background: #fcfaf5;
}
small { display: block; margin-top: 6px; font-size: 11px; color: #56645a; }
.check { display: flex; gap: 9px; align-items: center; margin: 20px 0; }
.check input { width: 18px; height: 18px; accent-color: #244c3d; }
button {
padding: 14px 20px; border: 0; border-radius: 3px;
font: inherit; font-size: 14px; color: #fffdf6; background: #254f41; cursor: pointer;
}
button[type="submit"] { width: 100%; display: flex; justify-content: space-between; }
button:disabled { background: #5b6c5e; cursor: wait; }
:focus-visible { outline: 3px solid #ad4d28; outline-offset: 4px; }
[aria-invalid="true"] { border: 2px solid #9d3327; }
.error { color: #963024; font-size: 12px; margin: 5px 0; }
.error:empty { display: none; }
#progress, #message { font-size: 14px; overflow-wrap: anywhere; }
#result { margin-top: 20px; border-top: 1px solid #c9cebf; padding-top: 12px; }
#result h3 { font-size: 15px; margin: 0 0 8px; }
#records article { border-top: 1px solid #c9cebf; overflow-wrap: anywhere; padding: 12px 0; }
@media (max-width: 800px) { .pair { grid-template-columns: 1fr; gap: 0; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
CSS
Studio로 돌아가 새로 고치고 작업 폼을 사용합니다. Morgan Vale, morgan@example.test, C로 Clay, slowly / 10:00, Space로 지원을 선택하고 A seat near the entrance를 입력합니다. Tab으로 버튼에 가서 Enter를 한 번 누릅니다. 준비된 처리기의 진짜 확인을 기다립니다. 첫 실습이 처리기를 제공하며 여러분은 주변의 의미, 제약과 스타일을 구현했습니다. 다음 실습이 요청 및 피드백 로직을 설명하고 만듭니다.
Studio records에서 같은 이름, 이메일, 시간과 번호를 찾습니다. Existing Visitor와 Avery Reed는 보존합니다. 이들은 새 신청과 독립된 기록이며 자리를 만들려고 삭제하는 것은 범위 밖입니다. 정원을 강제하는 것은 서버이지 비활성화 버튼이 아닙니다. 이후 Back to signup을 선택합니다. 새로 고침이 미전송 필드를 지워도 저장된 신청은 실습에 남습니다.
브라우저 또는 Studio를 약 390픽셀로 줄이고 약 768픽셀 태블릿 폭도 살펴봅니다. 가로 스크롤이 없어야 하며 레이블과 도움말은 보이고 이름/이메일은 세로로 쌓여야 합니다. Tab으로 모두 돌아보며 초점 받은 각 컨트롤에 보이는 표시가 있는지 봅니다. 넓은 폭으로 돌려 두 열도 확인합니다. 맞추기 어려운 필드를 숨기면 안 됩니다.
옛 배치라면 /home/labex/project/stillspoke에서 base.css가 아닌 form.css를 썼는지 확인하고 새로 고칩니다. 잘못된 경로에 VM 초기화는 필요 없습니다. 자체 확인: 좁고 넓은 화면에서 전체를 읽고 조작하고, 실제 작업 신청이 기록에 있으며 기존 방문자는 남습니다. 검증은 실기록을 읽고 바꾸지 않은 파일 사본을 일시 격리 데이터에서 실행합니다. 실제 폼을 대신 제출하지 않습니다.
요약
보이는 레이블, 기본 제약과 도움말을 실제 컨트롤에 연결하고 입력을 잃지 않는 조건부 메모 및 키보드 초점이 보이는 반응형 배치를 만들었습니다. 독립 예시 뒤 실제 작업 신청을 완료하고 확인을 접수 기록과 대조했습니다. 다음 독립 실습은 요청과 피드백 처리기를 만듭니다.
