이름이 있는 검색 폼
120분 안팎
학습 목표
레이블과 제출 버튼으로 키보드 검색 흐름을 만듭니다.
개념
검색 폼은 사용자와 다음 화면의 계약입니다
사용자는 어떤 값을 넣어야 하는지 알고, 입력 뒤 제출할 수 있어야 합니다. 다음 화면은 전달된 값이 검색어인지 지역인지 구별해야 합니다. 이 두 요구를 레이블과 name이 각각 맡습니다. 예시 사이트에서는 검색어와 지역을 입력하고 results.html로 이동합니다. 아직 행사 목록을 필터링하지 않으며 전달된 주소를 확인하는 단계입니다. 동작하지 않는 결과 기능까지 완성했다고 설명하지 않습니다.
label, id, name을 분리해 이해합니다
label의 for가 input의 id와 같으면 화면에 보이는 검색어 문구가 입력의 이름을 제공합니다. 레이블을 클릭해 입력에 포커스가 오는지도 확인합니다. id는 문서 안에서 요소를 식별하고 레이블이나 설명의 참조에 사용합니다. name은 제출할 값의 키입니다. id가 query이고 name이 q여도 문제없으며 각각 역할이 다릅니다. 이름만 비슷하게 쓴다고 자동으로 연결되지는 않습니다. for가 keyword인데 id가 query이면 눈으로 가까이 있어도 명시적 연결이 깨집니다.
placeholder는 입력이 비어 있을 때 보여 주는 예시입니다. 입력 후 사라지고 사용자가 나중에 입력 목적을 되짚기 어렵기 때문에 레이블을 대신하는 안내로 사용하지 않습니다. 검색어라는 지속적인 이름을 label로 제공하고 책 같은 예시는 별도의 설명에 둡니다. 설명은 aria-describedby로 연결해 이름과 부가 안내를 구별합니다. 같은 문구를 aria-label로 다시 덮어쓰는 대신 눈에 보이는 레이블과 프로그램이 읽는 이름이 일치하도록 유지합니다.
폼의 기본 제출을 사용합니다
form의 action은 목적지이고 method는 전달 방식을 정합니다. 이 프로젝트는 읽기 목적의 검색이므로 get을 사용합니다. input의 name이 q이고 값이 책이면 URL 쿼리에 q라는 키로 값이 전달됩니다. 한글과 공백은 주소에서 인코딩될 수 있으며 화면 주소의 문자열이 입력과 똑같이 보이지 않아도 곧바로 오류는 아닙니다. 비밀번호 같은 비밀값은 검색 주소에 넣지 않습니다. 정적 results.html은 이 주소를 받아 열릴 뿐 전달 값을 처리하는 서버가 아닙니다.
button의 type을 submit으로 명시하면 폼 제출 버튼이라는 의도가 드러납니다. type=button은 일반 버튼이므로 별도 동작을 연결하지 않은 상태에서는 폼을 제출하지 않습니다. input과 submit 버튼을 같은 form 안에 두어 기본 제출을 이용합니다. 텍스트 입력에서 Enter 제출이 되는지는 실제 대상 브라우저에서 확인합니다. 클릭 이벤트만 추가하면 키보드 제출이 빠질 수 있으므로 이후 이벤트 구현도 폼의 submit을 중심으로 설계합니다.
지역 선택은 보이는 말과 제출 값을 나눕니다
select에는 region이라는 name과 region이라는 id를 주고 label을 연결합니다. option의 전체 지역은 사용자에게 보이는 문구이고 value=all은 다음 코드가 해석할 안정적인 값입니다. 지역 이름을 나중에 바꾸더라도 값 계약을 유지할 수 있습니다. 전체 지역을 첫 선택지로 제공하면 사용자가 지역을 지정하지 않은 검색도 의도가 분명해집니다. 전체를 빈 값으로 둘 경우 필수 검증과 충돌할 수 있어 이 프로젝트에서는 all이라는 값으로 표현합니다.
지역을 반드시 고르게 하는 요구는 현재 없습니다. 전체 지역으로 검색하는 사용자를 오류로 막지 않습니다. 검색어에는 required를 붙여 빈 입력 제출을 기본 검증으로 막습니다. required는 공백만 입력한 문자열을 의미상 검색어로 판정하는 규칙까지 제공하지 않습니다. 공백 정리와 검색 규칙은 이후 JavaScript 단계에서 구현합니다. 브라우저 검증을 서버 검증 대신이라고 말하지 않고 현재 문서 수준의 보조 장치로 설명합니다.
제출 실패를 추적합니다
버튼을 눌러도 이동이 없으면 빈 입력 때문에 검증에 막혔는지 먼저 봅니다. 값을 넣어도 안 되면 버튼 type과 form 포함 여부를 확인합니다. 결과 페이지는 열리지만 주소에 q가 없으면 name 누락을 확인합니다. 레이블을 눌러 포커스가 다른 곳으로 이동하면 id 중복이나 for 불일치를 살핍니다. 이 순서는 화면의 증상을 HTML 계약으로 바꾸는 과정입니다. name과 id를 모두 같은 글자로 바꾸기 전에 어느 역할의 오류인지 설명합니다.
주소에서 제출 계약을 읽습니다
책을 입력하고 서울을 선택해 제출하면 목적지 주소에는 q와 region 두 키가 나타나야 합니다. 예를 들어 쿼리 부분은 q=%EC%B1%85®ion=seoul처럼 표현될 수 있습니다. 화면에서 한글로 표시되는지 인코딩 문자열로 표시되는지는 주소 표시 방식에 따라 다르므로 문자열 모양만 비교하지 않습니다. q가 책을 나타내는지, region이 seoul인지 분리해 읽습니다. HTML 설명이나 코드에 이 주소를 적을 때 매개변수 사이의 & 문자는 &로 이스케이프해 작성합니다. 목적지 파일이 존재한다는 검사와 전달 값이 정확하다는 관찰은 다른 근거입니다.
지역을 전체 지역으로 되돌려 제출하면 region=all이 전달되어야 합니다. option의 글자가 전체 지역인데 value가 seoul이면 사용자가 본 선택과 전달된 조건이 어긋납니다. 레이블, 화면 선택 문구, 제출 키, 제출 값 네 가지를 표처럼 대응시켜 읽으면 이런 오류를 찾기 쉽습니다. 검색어의 화면 이름은 검색어, 요소 식별자는 query, 제출 키는 q이며 값은 사용자가 입력한 내용입니다. 지역도 같은 방식으로 정리하되 식별자와 제출 키가 우연히 같다는 이유로 두 역할을 합쳐 설명하지 않습니다.
전송 대상에서 빠지는 경우
입력에 id가 있어도 name이 없으면 일반적인 폼 제출 데이터에 그 입력의 값이 포함되지 않습니다. disabled로 비활성화한 컨트롤 역시 제출 대상에서 제외됩니다. readonly는 지원하는 입력 유형에서 편집을 막는 속성이며 disabled와 제출 동작이 같지 않습니다. 지금 검색 폼에는 두 속성이 필요하지 않으므로 문제를 해결하려고 임의로 추가하지 않습니다. 폼 밖으로 입력을 옮기면 명시적 form 연결이 없는 한 원래 폼의 제출 대상에서 빠질 수 있습니다. 레이아웃을 바꾸더라도 입력과 버튼이 속한 폼을 확인해야 합니다.
레이블과 설명을 따로 검토합니다
입력에 책을 타이핑한 뒤에도 검색어라는 레이블과 입력 안내가 남아 있는지 봅니다. 레이블을 클릭했을 때 검색어 입력으로 이동하면 명시적 연결을 사용자 동작으로 확인할 수 있습니다. 설명 문구는 무엇을 검색하는지와 어떤 값이 허용되는지를 짧게 전달해야 합니다. 단순히 입력하세요라고 쓰면 이름과 설명이 같은 말을 반복할 뿐입니다. 검색어와 지역을 각각 제출해 보고 목적지 주소를 기록하면 폼 구조가 실제 값 전달로 이어졌다는 근거가 됩니다. 이 단계의 결과 문서는 조건을 받는 예제이며 검색 엔진을 구현한 결과로 해석하지 않습니다.
따라하기
실습 폴더 준비
starter 압축을 풀고 그 폴더를 편집기로 엽니다. 터미널도 package.json이 있는 폴더에서 시작합니다. 다음 명령은 외부 의존 패키지가 없는 잠금 파일을 사용합니다.
npm ci
npm teststarter의 FAIL 항목을 먼저 읽습니다. npm 설치나 Python 실행 실패는 환경 문제이므로 HTML 수정 전에 해결합니다.
계약에 맞게 문서 수정
index.html에서 다음 구조를 기준으로 해당 레슨의 FAIL 항목을 고칩니다. 아래는 최종 문서의 참고 코드이며 그대로 저장하면 같은 파일 구성에서 사용할 수 있습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>우리 동네 행사 찾기</title>
<link rel="stylesheet" href="assets/site.css">
</head>
<body>
<header><p>우리 동네 행사</p><nav aria-label="주요 메뉴"><a href="#intro">소개</a> <a href="#events">행사 목록</a></nav></header>
<main>
<h1>우리 동네 행사 찾기</h1>
<section id="intro" aria-labelledby="intro-title">
<h2 id="intro-title">가까운 곳에서 함께 만나요</h2>
<p>지역과 검색어로 관심 있는 행사를 찾아보세요. 현재는 예시 행사만 제공합니다.</p>
</section>
<section aria-labelledby="search-title">
<h2 id="search-title">행사 검색</h2>
<p id="query-help">검색어를 한 글자 이상 입력하세요. 예: 책</p>
<form action="results.html" method="get">
<label for="query">검색어</label>
<input id="query" name="q" type="search" required aria-describedby="query-help">
<label for="region">지역</label>
<select id="region" name="region">
<option value="all">전체 지역</option>
<option value="seoul">서울</option>
<option value="busan">부산</option>
</select>
<button type="submit">검색</button>
</form>
</section>
<section id="events" aria-labelledby="events-title">
<h2 id="events-title">예시 행사 목록</h2>
<article><h3>동네 책 나눔</h3><p>서울 · 10월 20일 · 무료</p><a href="event.html">동네 책 나눔 상세 보기</a></article>
</section>
</main>
<footer><p>교육용 행사 정보이며 실제 모집 공고가 아닙니다.</p></footer>
</body>
</html>
파일 검사 재실행
저장한 뒤 실습 폴더에서 검사합니다. 마지막 실패 수가 0인지 확인합니다. 출력은 solution을 실행한 실제 검사 결과입니다.
npm test실행 결과
> frontend-m01-search-form@1.0.0 test > python3 check.py PASS 한국어 문서와 제목 PASS UTF-8 및 작은 화면 설정 PASS 스타일 파일 상대 경로 PASS 목록 및 상세 파일 존재 PASS 중복 id 없음 PASS 모든 내부 링크 목적지 존재 PASS main과 h1 각각 하나 PASS 주요 메뉴와 행사 article PASS 소개와 목록 제목 연결 PASS 행사 제목 h3 PASS GET 검색 목적지 PASS 검색어 레이블 연결 PASS 지역 레이블과 전체 옵션 PASS 명시적 제출 버튼 검사 14개, 실패 0개
브라우저에서 사용자 흐름 확인
index.html을 브라우저의 파일 열기로 엽니다. 소개와 목록 링크, 상세 페이지와 돌아가기를 확인합니다. 검색어에 책을 넣고 지역을 서울로 골라 Enter를 누릅니다. results.html 주소의 q와 region을 확인합니다. 다시 돌아와 빈 입력 제출과 Tab·Shift+Tab을 시험하고 KEYBOARD.md의 미확인을 실제 관찰로 바꿉니다. 브라우저 결과는 직접 관찰하므로 고정 출력이 없습니다.
확인 문제
실습
starter의 index.html을 수정해 이 레슨의 검사 계약을 통과시키세요. package.json 폴더에서 npm ci 후 npm test를 실행합니다. check.py는 수정하지 않습니다. 브라우저에서 링크·검색·키보드 흐름을 확인하고 KEYBOARD.md에 관찰 결과를 기록합니다. solution은 비교용이며 수동 검증 결과를 대신하지 않습니다.
실행 명령
npm test
기대 결과
모든 PASS 항목과 마지막 실패 0개를 확인합니다. 수동 점검 기록도 제출합니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- label의 for, input의 id와 name은 각각 어떤 역할을 하나요?
- GET 검색 폼의 제출 값이 주소에서 빠졌다면 무엇을 확인하나요?