빈 결과와 사용자 안내
150분 안팎
학습 목표
결과가 없는 상태에서 다음 행동을 제안합니다.
개념
0건은 화면의 정상 결과입니다
검색어와 지역 조건에 맞는 행사가 없을 수 있습니다. 이때 텅 빈 공간만 남으면 사용자는 데이터가 아직 로딩 중인지, 고장 났는지, 결과가 없는지 판단하기 어렵습니다. 이번 앱은 정적 데이터 검색이 끝난 뒤 배열 길이가 0이면 빈 결과입니다. 네트워크 오류나 로딩 상태와 혼동하지 않습니다. 이후 API 모듈에서 그 상태들을 별도로 추가합니다.
빈 결과에는 현재 조건에 맞는 행사가 없다는 설명과 다음 행동이 함께 필요합니다. 검색어를 줄이거나 전체 지역으로 바꾸도록 안내하고 조건 초기화 버튼을 제공합니다. 사용자의 행동을 탓하는 문구나 다시 시도만 반복하는 안내를 피합니다. 정적 검색에서 같은 조건의 재시도는 결과를 바꾸지 않으므로 조건 변경이라는 실제 해결 행동을 연결합니다.
데이터가 수와 목록을 함께 결정합니다
renderSummary(summary, empty, count)는 결과 수를 textContent로 표시하고 count가 0일 때만 empty 영역을 보이게 합니다. 카드 목록의 길이를 문자열에서 다시 읽어 숫자를 계산하지 않습니다. searchEvents가 반환한 배열 길이를 renderList와 renderSummary가 공유합니다. 수 안내는 0건일 때도 같은 형식으로 표시해 사용자가 결과를 비교할 수 있게 합니다.
이 함수는 count가 0 이상의 정수라는 호출 계약을 사용합니다. 불완전한 API 응답을 여기서 임의로 0으로 바꾸지 않습니다. 데이터 계약 검사는 API 모듈의 경계에서 할 일입니다. 이번에는 검색 결과 배열에서 길이를 가져오므로 빈 상태와 실제 행사 수가 어긋나지 않도록 호출하는 것이 핵심입니다. 안내 문자열 자체를 상태 저장소로 사용하지 않습니다.
빈 영역은 HTML에 미리 두고 hidden 속성으로 보이거나 숨깁니다. 결과가 있으면 empty.hidden은 true, 0건이면 false입니다. visible 같은 임의 속성을 만들거나 문자열 false를 hidden에 대입하지 않습니다. hidden은 불리언으로 다룹니다. 목록이 빈 상태에서도 section 제목과 검색 폼은 남아 사용자가 조건을 바로 바꿀 수 있습니다.
이전 결과가 남지 않게 합니다
빈 결과에서 안내만 바꾸고 이전 카드를 그대로 두면 0건 문구 아래 이전 행사가 보이는 모순이 생깁니다. 매 검색마다 renderList를 호출하고 빈 배열도 같은 경로로 전달합니다. replaceChildren으로 이전 목록을 지운 뒤 0건 안내를 표시합니다. 별도 코드 분기로 카드 제거를 빠뜨리지 않도록 정상과 빈 결과를 하나의 렌더링 규칙에 넣습니다.
반대로 0건을 본 다음 결과가 있는 조건으로 바꾸면 빈 안내와 초기화 버튼을 숨깁니다. 초기 호출만 검사하면 이 복귀 버그를 놓칩니다. 결과 있음→없음→있음 순서로 테스트합니다. 목록 길이, summary 텍스트, empty.hidden을 각각 확인합니다. 화면 상태가 하나라도 이전 값이면 검색이 끝난 뒤 사용자에게 서로 다른 정보를 보여 주게 됩니다.
초기화는 조건과 화면의 전환입니다
조건 초기화는 query.value만 비우는 일이 아닙니다. q를 빈 문자열, region을 all, id를 null로 바꾸고 목록 URL로 이동한 뒤 다시 검색하고 렌더링합니다. 마지막에 query.focus를 호출해 곧바로 새 검색어를 입력할 수 있게 합니다. 버튼을 누른 후 URL에 옛 검색어가 남으면 새로고침에서 이전 빈 결과가 돌아오는 문제를 만듭니다.
초기화 버튼은 type=button입니다. type을 생략한 폼 내부 버튼은 제출 버튼으로 동작할 수 있어 초기화와 검색이 동시에 실행될 수 있습니다. form.reset을 쓰면 기본값으로 되돌아가지만 앱 상태와 주소는 자동으로 바뀌지 않습니다. 이 앱은 명시적인 상태 전환 함수로 같은 전체 조건을 두 초기화 버튼에서 공유합니다.
빈 검색을 허용하는 results.html과 required 검색어를 요구하는 index.html은 목적이 다릅니다. 소개 화면은 이름으로 검색을 시작하도록 안내하고 확장 탐색 화면에서는 전체 보기로 돌아갈 수 있습니다. 이번 레슨에서 index.html의 이전 계약을 몰래 바꾸지 않습니다. 정책 차이를 문구와 테스트에 적고 이후 제품 결정으로 통일한다면 영향 화면을 함께 변경해야 합니다.
결과 수 안내의 역할을 정합니다
result-summary에는 aria-live=polite와 role=status를 제공합니다. 사용자의 입력을 방해하는 강한 경고 대신 결과 변경을 알리는 상태 영역입니다. 짧은 수 문구만 갱신하고 카드 전체를 live 영역으로 만들지 않습니다. 목록이 많아질 때 모든 카드 내용을 한꺼번에 알리는 대신 사용자가 링크를 탐색할 수 있게 합니다. 실제 읽기 경험은 보조 기술 환경에서 확인합니다.
검색을 제출할 때마다 입력 포커스를 강제로 결과 영역으로 이동하지 않습니다. 사용자가 검색 조건을 다시 바꾸는 흐름을 유지합니다. 상세 진입은 새로운 화면이므로 제목에, 상세 복귀는 선택 링크에, 초기화는 새 검색 시작이므로 입력칸에 포커스를 둡니다. 모든 상태 변화에서 같은 요소에 초점을 보내는 대신 다음 행동을 기준으로 위치를 정합니다.
0건 안내에는 현재 검색어를 그대로 반복하지 않아도 됩니다. 폼에 조건이 남아 있고 사용자는 그 값으로 수정할 수 있습니다. 문구에 검색어를 추가한다면 textContent로 표시하고 HTML 문자열에 섞지 않습니다. 모든 상태에서 카드 제목뿐 아니라 사용자 입력이 표시되는 경계도 같은 텍스트 원칙으로 점검합니다.
작은 테스트와 사용자 흐름을 함께 봅니다
레슨 starter는 renderSummary만 미구현이고 앞 레슨 helper는 완성되어 있습니다. 0과 양수의 상태 전환을 테스트합니다. hidden의 방향을 반대로 써서 0건 때 안내가 사라지거나 다시 결과가 생겨도 안내가 남는 경우를 기대값과 비교합니다. 카드 제거 자체는 renderList의 빈 배열 검사로 확인합니다. 두 함수가 역할을 나눈다는 점을 설명할 수 있어야 합니다.
미션은 이 helper를 실제 검색·상세·복귀에 연결합니다. 없는 이름과 부산 조건으로 검색해 빈 결과를 만든 뒤 초기화합니다. 전체 날짜순 목록이 복구되고 주소에서 id가 없어야 합니다. 상세를 방문했다가 뒤로 왔을 때도 목록 조건이 유지됩니다. 모델 테스트는 이런 전환의 값과 메서드 호출을 확인하며 브라우저 기본 submit이나 키보드 탐색은 별도 확인입니다.
오류 메시지와 관찰 한계를 읽습니다
expected false, actual true로 hidden 검사가 실패하면 비어 있을 때 보여야 한다는 정책과 비교식을 먼저 봅니다. result-summary가 null이라면 HTML id와 스크립트 배치부터 확인합니다. 결과 수는 바뀌는데 목록이 남아 있으면 renderSummary가 아니라 renderList 호출 누락을 확인합니다. Cannot read properties of undefined가 나온다면 검색 반환값이 배열인지 이전 데이터 계약을 점검합니다.
npm run test:browser는 실제 Chrome/Chromium을 실행하는 별도 harness입니다. 이 작성 환경에서는 로컬 서버 바인딩이 제한되어 실행하지 못했습니다. 따라서 실제 브라우저 통과라고 기록하지 않습니다. 학습자는 로컬 환경에서 browser-tests.html의 결과와 Tab·320px·200% 확대 관찰을 LAYOUT.md에 남깁니다. 프로그램적 click이 통과해도 실제 Tab 동선 검사를 대신하지 않습니다.
모듈 미션을 마무리합니다
미션 starter는 m03 미션 solution을 포함한 상태에서 화면과 테스트를 확장했습니다. 데이터 함수를 새로 작성하는 대신 제공된 renderSummary와 submit·상세 이동 TODO를 구현합니다. 앞 모듈의 문서·CSS·데이터 검사는 그대로 통과하고 새 DOM 흐름에서 미구현 검사만 실패해야 합니다. 성공하면 검색 조건, 주소, 수 안내, 목록, 포커스가 같은 상태를 가리킵니다.
완료 자료에는 테스트 결과와 실제 관찰을 나누고 빈 결과를 만든 입력, 초기화 후 보이는 결과, 복귀 포커스 위치를 적습니다. 더 읽기는 DOM API를 확장하는 참고 자료입니다. 이 레슨에서는 정상 결과가 없는 경우도 사용자가 다음 행동을 할 수 있게 만드는 책임을 연습합니다. 다음 모듈에서는 같은 화면에 로딩과 API 오류를 추가하되 빈 결과의 의미를 유지합니다.
API 동작의 범위는 Node.textContent MDN 문서에서도 확인합니다.
따라하기
실습 파일과 이전 계약 확인
아래 실습 starter zip을 풀고 그 폴더의 터미널에서 npm ci 후 npm test를 실행합니다. 외부 의존 패키지는 없습니다. starter의 미구현 검사가 실패하는지 먼저 확인합니다. assets/data.js는 앞 모듈의 완성 함수이며 변경하지 않습니다.
이번에 고칠 함수는 renderSummary입니다. HTML과 테스트 모델은 제공되며 모델의 범위를 README에서 확인합니다.
경계값을 Node로 실행
아래 코드를 example.cjs에 저장해 node example.cjs로 실행합니다. DOM이나 브라우저 이력이 아닌 문자열·값 계산 확인입니다.
for (const count of [2,0,1]) {
console.log(`예시 행사 ${count}건입니다. / 빈 안내 숨김: ${count !== 0}`);
}실행 결과
예시 행사 2건입니다. / 빈 안내 숨김: true 예시 행사 0건입니다. / 빈 안내 숨김: false 예시 행사 1건입니다. / 빈 안내 숨김: true
TODO 구현과 테스트 대조
assets/dom.js의 해당 TODO를 자신의 코드로 구현합니다. 막히면 아래 기준 구현과 비교합니다. 파일 안의 api 내보내기와 다른 제공 함수는 유지합니다. 코드는 브라우저 문서 또는 제공 모델을 doc로 전달받으며 Node 전역 document를 사용하지 않습니다.
function renderSummary(summary, empty, count) {
summary.textContent = `예시 행사 ${count}건입니다.`;
empty.hidden = count !== 0;
}npm test를 다시 실행해 각 기대값과 실제값을 비교합니다. 모든 검사가 통과하면 solution과 비교해 원인을 설명합니다. 출력으로 통과를 위장하거나 테스트를 수정하지 않습니다.
실제 화면과 미션 연결
로컬 브라우저가 있는 환경에서 npm run test:browser를 실행하거나 python3 -m http.server 8000 --bind 127.0.0.1로 열어 http://127.0.0.1:8000/browser-tests.html을 확인합니다. 이 작성 샌드박스에서는 서버 바인딩이 제한되어 실제 브라우저 실행은 미확인입니다. 수동 서버를 사용했다면 관찰 후 터미널에서 Ctrl+C로 종료합니다.
results.html에서 검색→필터→상세→복귀·빈 결과→초기화를 확인하고 실제 Tab·Enter, 320px, 200% 확대를 LAYOUT.md에 기록합니다. 자동 click은 실제 키보드 동선 검사를 대신하지 않습니다. 미션 starter는 m03 solution을 이어받았으므로 검증된 데이터·문서·CSS를 유지하면서 submit·상세 이동·수 안내 TODO를 연결합니다.
확인 문제
실습
결과가 없는 상태에서 다음 행동을 제안합니다. starter zip을 풀고 npm ci 후 npm test를 실행합니다. assets/dom.js의 renderSummary를 구현하며 제공 함수·테스트·원본 데이터를 수정하지 않습니다. 정상·빈 목록·잘못된 id·반복 렌더링 경계 중 해당 레슨 검사를 통과시킵니다. README의 실제 브라우저 확인과 모델 검사의 차이를 기록합니다.
실행 명령
npm test
기대 결과
DOM 모델 검사 12개, 실패 0개. starter는 해당 TODO 관련 검사 실패, solution은 전부 통과합니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 로딩·오류·빈 결과 화면을 나눈 이유를 설명합니다.