JavaScript 기초와 비동기

3주 배열·DOM·이벤트 검색과 4주 fetch·검증·로딩·오류·재시도 구현하기

이번 문서에서 만드는 것

3주는 배열과 이벤트로 명단을 검색하고 4주는 JSON 파일을 비동기로 읽어 로딩·오류·빈 결과를 구분합니다. HTML·CSS 문서를 먼저 완료합니다. TypeScript는 React를 배운 뒤 8주 문서에서 다룹니다.

const는 변수 재할당을 막으며 객체 내부까지 자동으로 불변으로 만들지는 않습니다. let은 재할당이 필요할 때 사용합니다. 객체는 회원 한 명, 배열은 회원 여러 명을 표현합니다. 이름이 같은 회원도 있으므로 ID와 표시 이름을 구분합니다.

3주: 데이터에서 DOM 만들기

앞 주 코드를 보관하고 별도 Vite vanilla 프로젝트에서 시작합니다. Vite는 여기서 개발 서버와 기본 모듈 연결만 제공하며 React는 사용하지 않습니다.

Shell
npm create vite@latest lion-javascript -- --template vanillacd lion-javascriptnpm install

index.html의 샘플 앱 컨테이너를 아래 main으로 교체하고 템플릿이 만든 JavaScript 진입점 연결은 유지합니다. 문서 언어는 ko, title은 회원 명단으로 바꿉니다. 앞 주 CSS를 src/style.css에 복사합니다.

HTML
<main><h1>회원 명단</h1><form id="search-form">  <label for="query">이름 검색</label>  <input id="query" name="query" type="search">  <button type="submit">검색</button></form><p id="status" role="status"></p><button id="retry" type="button" hidden>다시 불러오기</button><ul id="members" class="member-grid"></ul></main>

템플릿의 src/main.js를 아래로 교체합니다. 샘플 counter와 로고 import는 이 전체 교체 과정에서 제거됩니다.

JavaScript
import './style.css';const form = document.querySelector('#search-form');const input = document.querySelector('#query');const list = document.querySelector('#members');const status = document.querySelector('#status');const retry = document.querySelector('#retry');let members = [  { id: 1, name: '가람', track: 'frontend' },  { id: 2, name: '나래', track: 'design' },];
function render() {  const keyword = input.value.trim().toLocaleLowerCase();  const filtered = members.filter(member =>    member.name.toLocaleLowerCase().includes(keyword)  );  list.replaceChildren();  for (const member of filtered) {    const item = document.createElement('li');    item.className = 'member-card';    item.textContent = member.name + ' · ' + member.track;    list.append(item);  }  status.textContent = filtered.length === 0    ? '검색 결과가 없습니다.'    : filtered.length + '명이 있습니다.';}
form.addEventListener('submit', event => {  event.preventDefault();  render();});render();

filter는 조건에 맞는 새 배열을 반환합니다. map은 각 항목을 다른 값으로 바꾸며 find는 처음 일치하는 항목 하나 또는 undefined를 반환합니다. 값이 없을 수 있다는 사실을 숨기지 말고 분기합니다.

preventDefault는 form의 기본 페이지 이동을 막습니다. 버튼 클릭만 처리하는 대신 form의 submit을 처리하면 Enter 입력도 작동합니다. 외부 문자열은 textContent로 넣습니다. 검증 없이 innerHTML에 넣으면 문자열이 HTML로 해석될 수 있습니다.

실행 방법

프로젝트 폴더에서 아래 명령을 실행하고 출력된 로컬 주소를 엽니다. 4주의 fetch는 file://로 연 문서에서 정상 동작하지 않을 수 있으므로 처음부터 HTTP로 실행합니다.

Shell
npm run dev -- --port 4173 --strictPort

포트가 사용 중이면 실행 중인 서버를 확인하거나 다른 포트를 선택합니다. 개발 서버는 공개 배포 서버가 아니며 작업이 끝나면 Ctrl+C로 종료합니다.

3주 권장 90분: 문법·배열 예측 20분, DOM·이벤트 30분, 검색 확장 25분, 리뷰 15분. 가상 회원을 추가하고 빈 검색·없는 이름·공백 검색을 검사합니다. 트랙 필터를 추가하고 이름 검색과 함께 적용하는 과제에 직접 도전합니다.

4주: 비동기 데이터와 실패 처리

프로젝트의 public 폴더에 members.json을 만듭니다. public 파일은 개발 서버의 루트 경로로 제공되므로 요청 주소는 ./members.json입니다.

JSON
[  { "id": 1, "name": "가람", "track": "frontend" },  { "id": 2, "name": "나래", "track": "design" }]

src/main.js의 초기 let members = [...]let members = [];로 바꾸고 맨 마지막 단독 render(); 호출을 제거합니다. 다음 코드를 파일 끝에 추가합니다. 기존 form 이벤트 안의 render는 그대로 둡니다.

필수: 요청 성공·실패·재시도 흐름을 완성합니다. 심화: parseMembers의 데이터 형식과 중복 ID 검증을 분석하고 실패 사례를 추가합니다.
JavaScript
function parseMembers(value) {  const tracks = new Set(['frontend', 'design', 'backend']);  if (!Array.isArray(value) || !value.every(member =>    member !== null && typeof member === 'object' &&    Number.isSafeInteger(member.id) && member.id > 0 &&    typeof member.name === 'string' && member.name.trim().length > 0 &&    tracks.has(member.track)  )) {    throw new Error('명단 데이터 형식이 올바르지 않습니다.');  }  if (new Set(value.map(member => member.id)).size !== value.length) {    throw new Error('명단 ID가 중복되었습니다.');  }  return value;}
async function loadMembers() {  retry.hidden = true;  list.replaceChildren();  status.textContent = '명단을 불러오는 중입니다.';  for (const element of form.elements) element.disabled = true;  try {    const response = await fetch('./members.json');    if (!response.ok) throw new Error('명단 요청 실패: ' + response.status);    members = parseMembers(await response.json());    render();  } catch (error) {    members = [];    status.textContent = error instanceof Error ? error.message : '명단을 불러오지 못했습니다.';    retry.hidden = false;  } finally {    for (const element of form.elements) element.disabled = false;  }}
retry.addEventListener('click', loadMembers);loadMembers();

fetch는 네트워크 실패에는 reject되지만 HTTP 404·500 자체를 자동으로 예외로 바꾸지 않습니다. response.ok를 먼저 확인합니다. JSON 파싱에 성공해도 필요한 필드가 있다는 보장은 없으므로 외부 데이터의 형태도 검증합니다.

Promise는 나중에 결과가 정해질 작업을 표현합니다. await는 해당 async 함수의 후속 실행을 기다리게 하며 브라우저 전체를 멈추는 동기 대기 명령이 아닙니다. 작업 완료 뒤 정상·오류 중 어떤 상태로 전이하는지 먼저 그려 보세요.

실패를 직접 만들어 보기

입력·환경기대 결과
정상 JSON명단 표시
JSON을 []로 변경빈 결과 안내
파일 이름을 잠시 변경요청 오류와 다시 불러오기 버튼
JSON 문법 쉼표 누락오류 안내, 앱이 멈추지 않음
ID를 중복하거나 name을 숫자로 변경데이터 형식 오류
파일을 복구하고 재시도정상 명단으로 회복

파일 이름을 바꾼 검사는 반드시 원래대로 복구합니다. 개발자 도구 Network의 응답 본문과 상태 코드를 함께 봅니다. 개발 서버에 따라 없는 경로에 HTML을 반환할 수 있는데, 이 경우 JSON 파싱 오류가 나므로 “무조건 404”라고 단정하지 않습니다.

이 최소 예제는 최초 로드와 실패 후 재시도만 제공합니다. 로딩 중 검색을 잠가 요청 중복을 줄였습니다. 검색어마다 새 요청을 보내는 자동완성으로 확장한다면 요청 취소·오래된 응답 무시가 추가로 필요하며 React Hooks 문서에서 그 경계를 다룹니다.

4주 권장 100분: Promise·HTTP 20분, 로딩 구현 30분, 실패 표 검사 30분, 리뷰 20분. 완료 기준은 성공과 실패를 모두 재현하고 try/catch/finally와 HTTP 오류 검사의 차이를 설명하는 것입니다.

더 읽어보기

연결된 PBL 미션과 VOD

주차·미션참고 VOD 범위
3주 · JavaScript 기초사이트 기본 구조 13·14장 / 리액트 실무 3장
4주 · 비동기와 데이터사이트 기본 구조 13·14장 / 리액트 실무 3장

강좌 안내: 참고 강좌 1 · 참고 강좌 2. 이 문서는 영상 전체를 옮긴 전사 자료가 아닙니다. 세부 내용은 위 공식 문서에서 확인합니다.