Devin.KR

로딩·오류·빈 상태

180분 안팎

학습 목표

HTTP 오류와 연결 실패를 화면 상태로 바꿉니다.

개념

상태가 사용자에게 알려 주는 사실

검색 화면이 비어 있으면 사용자는 검색 결과가 없는지, 아직 기다려야 하는지, 요청이 실패했는지 알 수 없습니다. 같은 빈 공간을 서로 다른 상황에 사용하는 대신 loading·success·empty·error·not-found를 구분합니다. 이 레슨의 목표는 API 결과를 받는 함수와 안내를 그리는 함수 사이에 명확한 상태 값을 만드는 것입니다. 서버에서 온 문자열을 검사 없이 화면에 넣는 대신 내부의 kind와 필요한 데이터만 렌더러로 전달합니다.

loading은 요청이 진행 중이라는 사실입니다. success는 유효한 행사 데이터가 있다는 뜻이며 empty는 유효한 목록이지만 항목이 없다는 뜻입니다. error는 작업을 완료하지 못해 결과를 알 수 없다는 뜻입니다. not-found는 상세 id에 해당하는 행사가 없다는 뜻으로 목록 전체가 비었다는 것과 다릅니다. 사용자가 할 다음 행동이 다르므로 상태를 나눕니다. 같은 안내 문장을 모든 실패에 쓰면 조건 변경과 재시도를 구분하기 어렵습니다.

상태 객체는 kind와 rows를 항상 가집니다. 오류에는 reason과 status도 추가됩니다. rows는 loading·empty·error에서 빈 배열입니다. 이전 카드가 화면에 남아 현재 검색의 결과처럼 보이지 않도록 초기화하는 정책입니다. 기존 목록을 유지하며 갱신 중임을 표시하는 제품도 있지만 그 경우 오래된 목록이라는 구분이 필요합니다. 이번 프로젝트는 이전 카드 제거라는 단순한 정책을 선택하고 테스트합니다.

작업 시작과 완료를 각각 발행합니다

load 함수는 await request 전에 loading을 publish합니다. 요청 완료 뒤에 loading을 발행하면 사용자가 기다리는 구간을 표시하지 못합니다. 제공 테스트는 resolve를 보류한 Promise로 응답을 지연하고, 완료 전 상태가 이미 loading인지 확인합니다. 단순히 최종 success만 검사하면 로딩 누락은 잡지 못합니다. 시간이 걸리는 서버를 실제로 기다리는 대신 완료 시점을 통제해 이 상태 계약을 빠르게 검증합니다.

요청 함수는 HTTP 상태 확인·본문 파싱·필드 검증을 담당하고, load는 성공값이나 오류를 화면 상태로 변환합니다. 카드 생성 함수는 네트워크를 호출하지 않습니다. 이런 책임 분리는 나중에 API가 바뀌어도 표시 함수의 보안 경계를 유지하게 합니다. publish를 인자로 전달하면 테스트가 상태 배열을 수집하고 실제 앱이 DOM을 갱신하는 두 사용법을 같은 함수로 검사할 수 있습니다.

목록의 길이가 0이면 empty, 1 이상이면 success를 발행합니다. JavaScript에서 빈 배열은 truthy이므로 if(rows)로 빈 결과를 판단하지 않습니다. 먼저 목록 계약이 배열임을 확인하고 rows.length를 읽습니다. 잘못된 객체를 빈 배열로 변환해 계약 오류를 숨기지 않습니다. 테스트에서 기대 empty, 실제 success가 보이면 배열 길이를 기준으로 분기하는지 확인합니다.

상태 코드와 전송 실패를 구분합니다

404와 500은 서버가 HTTP 응답을 보낸 경우입니다. request가 응답의 상태를 확인해 ApiError로 던지므로 load의 catch에서 분류할 수 있습니다. fetch 자체가 거부될 때는 network로 감싸지만 이것만으로 CORS인지 단절인지 확정하지 않습니다. 화면은 연결 확인 안내를 제공하고 진단은 개발자 도구에서 진행합니다. 실패 원인을 알 수 없는 경우에 임의의 500을 기록하지 않습니다.

성공 상태인데 HTML 본문이 오거나 JSON 문법이 깨졌다면 contract로 표시합니다. 응답을 처리할 수 없다는 안내는 제공하지만 사용자에게 스택이나 HTML 오류 페이지를 그대로 렌더링하지 않습니다. 형태를 검사한 JSON이라고 해서 문자열의 HTML 삽입이 안전해지는 것은 아닙니다. 행사 name에는 꺾쇠가 들어갈 수 있으며 기존 createCard의 textContent를 사용해 제목을 글자로 보존합니다.

상세 404에서는 제목에 행사를 찾을 수 없다고 알리고 목록 복귀 링크를 제공합니다. 목록의 404도 자동 검사에서는 not-found로 분류되지만 실제 화면에서 상세 카드처럼 처리하지 않습니다. 목록 endpoint의 404는 요청 주소나 서버 계약을 확인할 진단 대상입니다. 모의 서버의 목록은 200·500·연결 실패를 제공하고 존재하지 않는 상세에서 404를 재현합니다. 모의 데이터 조건과 제품의 사용자 안내를 구별해 읽습니다.

화면 갱신을 빠뜨리지 않습니다

loading을 그릴 때 목록 자식을 제거하고 빈 결과 안내를 숨깁니다. 오류 후에도 empty 안내가 같이 보이면 서버 실패가 조건에 맞는 행사 없음처럼 들릴 수 있습니다. 성공에서만 result-summary를 해당 결과 수로 갱신하고 empty 상태에서 조건 변경·초기화 버튼을 제공합니다. API 상태 안내는 별도 api-status에 넣어 데이터 개수 안내와 역할을 분리합니다. 라이브 영역에는 요청당 필요한 변화만 전달합니다.

상세 화면에서도 이전 행사 제목을 지웁니다. 다른 행사로 이동했는데 이전 이름이 로딩 동안 남으면 잘못된 행사에 대한 내용을 읽을 수 있습니다. 로딩 제목과 상태 안내를 사용하고 성공 후 실제 제목에 포커스를 둡니다. error에서는 성공 카드나 예전 상세 메타가 남지 않아야 합니다. 숨김 여부뿐 아니라 요소의 textContent도 확인해야 상태 전환의 잔여 내용을 발견할 수 있습니다.

폼의 검색어와 지역은 route에서 유지합니다. API 실패 때문에 검색 조건을 지우면 재시도할 때 사용자가 원하는 조건을 잃습니다. request에 보낼 URL을 route로 만들고 받은 rows를 기존 검색·정렬 함수와 렌더러에 연결합니다. 오류는 결과를 알 수 없다는 사실을 바꿀 뿐 사용자의 입력 의도를 바꾸지 않습니다. 뒤로 가기에서도 주소를 다시 읽어 요청을 구성하는 원칙을 유지합니다.

작은 검사로 전환을 입증합니다

정상→빈 배열 전환은 이전 카드 제거와 empty 안내 표시를 함께 검사합니다. 정상→오류 전환은 카드 제거와 오류 안내를 확인합니다. 500→재시도→성공은 다음 레슨에서 완료하지만 지금은 error까지의 상태 기록을 분명하게 만듭니다. 테스트에서 rows가 기대 []가 아니라 이전 배열이면 예외 처리에서 상태를 부분 수정했는지 확인합니다. 새 상태 객체를 발행하면 이전 데이터가 섞이는 실수를 줄일 수 있습니다.

await response.json을 빼먹었을 때의 오류는 화면 분기보다 앞에서 발생합니다. Promise에 length가 없으므로 success·empty를 비교하기 전에 값이 무엇인지 확인합니다. Cannot read properties of null이 브라우저에서 보이면 필수 상태 요소가 실제 HTML에 있고 스크립트 뒤에서 읽히는지 점검합니다. local 실습은 DOM 모델과 상태 발행 검사를 제공하며 레이아웃이나 보조 기술의 실제 읽기 결과는 별도로 관찰합니다.

제출 설명에는 각 상태를 재현한 입력과 기대 안내를 적습니다. loading은 느린 응답, empty는 mode=empty, error는 mode=500 또는 mode=network, not-found는 id=999로 재현합니다. 성공과 실패를 오가며 이전 내용이 제거되는지 확인하고 미확인 환경은 사실대로 남깁니다. 더 읽기의 fetch와 JSON 장은 일반적인 응답 사용법을 보완합니다. 여기서는 상태 전환 기록과 다음 행동이 맞는지를 자신의 코드로 설명합니다.

따라하기

빈 배열 판정을 검증합니다

states.cjs로 실행합니다. 존재 여부가 아니라 배열 길이로 화면 의미를 정합니다.

for(const rows of [[],[{id:3}]])console.log(Boolean(rows),rows.length?'success':'empty');

실행 결과

true empty
true success

대기 상태를 먼저 기록합니다

지연 Promise를 해제하기 전에 loading이 있는지 확인합니다. 이 코드는 함수 수준 실험이며 DOM 검사는 아닙니다.

(async()=>{const states=[];let resolve;const pending=new Promise(r=>resolve=r);async function load(){states.push('loading');const rows=await pending;states.push(rows.length?'success':'empty');}const task=load();console.log(states.join(','));resolve([]);await task;console.log(states.join(','));})();

실행 결과

loading
loading,empty

서로 다른 실패를 분류합니다

HTTP 상태와 연결 실패를 같은 0건으로 바꾸지 않는 분기 예제입니다.

for(const result of [{status:404},{status:500},{status:0}])console.log(result.status,result.status===404?'not-found':result.status===0?'network':'http');

실행 결과

404 not-found
500 http
0 network

empty TODO를 완성하고 화면 전환을 관찰합니다

local 실습의 load에서 목록 길이가 0일 때 empty를 발행합니다. npm test의 loading 즉시 발행·빈 배열·HTTP/연결 오류 검사를 확인합니다. 실제 화면에서는 mode=empty·500·network와 id=999를 차례로 열어 이전 카드·상세 메타가 제거되는지 기록합니다. 오류 화면에서 조건을 지우지 않는 이유를 설명합니다.

확인 문제

실습

assets/api.js load에서 빈 목록을 empty로 발행하는 TODO를 구현합니다. loading→success·empty·not-found·error 전환을 서로 구분하고 이전 rows를 남기지 않습니다.

시작 코드·테스트 내려받기

실행 명령

npm test

기대 결과

기존 회귀 검사와 API 10개 모두 통과합니다. starter는 TODO 관련 assertion 실패, solution은 실패 0개입니다.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 로딩·오류·빈 결과 화면을 나눈 이유를 설명합니다.