mirim-app/frontend/src/pages/levels/Lv2AsyncJavascriptPage.jsx
AWESOMEDEV c5f875d990
All checks were successful
CI / backend-test (push) Successful in 1m1s
CI / frontend-build (push) Successful in 42s
CI / backend-dep-scan (push) Successful in 30s
feat(learn): 수준별 학습 과정 4종 — 초급·중급·고급·특급 48강좌 + 퀴즈 240문항
기존 84강좌는 수습용으로 유지하고, 헤더 셀렉트 박스로 과정을 전환한다.

과정 구성 (각 12강좌, 배우는 순서대로):
- 초급 — 코딩 첫걸음: 프로그램의 개념→HTML→JS→변수·조건·반복·함수→에러읽기→첫 게임
- 중급 — 실전 웹 개발: 비동기·배열→API설계→Express CRUD→DB·트랜잭션→인증→React 실전→테스트→배포
- 고급 — 아키텍처·성능·보안: 레이어드·패턴→인덱스·격리→캐싱·최적화→OWASP·인증설계→관측·무중단배포
- 특급 — 대규모 시스템: 분산기초·CAP→메시지큐→MSA→샤딩→대용량 설계→k8s→SRE→시스템설계 3연작→ADR

구현:
- trackContext(localStorage 유지) + 헤더 TrackSelect + 테스트 5개
- levelCatalog(lv1-~lv4- 슬러그, 기존과 충돌 없음) + 라우트·퀴즈 자동 연결
- LearnHubPage 과정 인식(과정별 인사말·진도), CertificatePage 분모 버그 선제 수정
- 퀴즈 240문항: quizzes.json 415→655 + 운영 DB 삽입(655/131 확인)

생성 파이프라인: 설계 4 → 작성 48(병렬) → 검수 4 에이전트.
검수 BLOCKER 2건(백틱 충돌·OpenAPI 표기) 수정 후 빌드 통과.
운영 검증: 4개 과정 전환·강좌 렌더·퀴즈 로드·선택 유지 브라우저 확인, 스모크 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:13:25 +09:00

523 lines
30 KiB
JavaScript

// 이 파일이 하는 일: "비동기 JavaScript: fetch와 Promise 정복" 코스 — 동기 코드가 화면을
// 얼리는 순간에서 출발해, 콜백 지옥 → Promise → async/await → fetch → 병렬 처리 →
// 실전 패턴(로딩·취소·타임아웃)까지 7개 섹션으로 안내하는 정적 학습 페이지.
// 프레이밍은 "서버와 대화하는 코드" — 기초 문법을 아는 학생이 실제로 돌아가는 웹 서비스를
// 만들 때 반드시 넘어야 하는 첫 고개인 비동기를, 환율 API 실습으로 손에 잡히게 만든다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
import { Link } from 'react-router-dom';
// ── 예제 코드 상수들 ──
const CODE_FREEZE = `// 버튼을 누르면 3초 동안 '아무것도' 못 하는 코드 — 콘솔에서 직접 체험해 보세요
function freezeFor3Seconds() {
const start = Date.now();
while (Date.now() - start < 3000) {
// 3초 동안 CPU를 붙잡고 제자리 돌기
}
console.log('끝!');
}
// 이 함수가 도는 동안 클릭·스크롤·입력이 전부 멈춥니다.
// JavaScript는 '한 번에 한 가지 일'만 하는 싱글 스레드라서,
// 오래 걸리는 일을 그 자리에서 기다리면 화면 전체가 얼어붙어요.
// 네트워크 요청을 이런 식으로 기다렸다간? 서버가 느린 만큼 앱이 얼겠죠.`;
const CODE_KITCHEN = `주방(JS 엔진)에는 요리사가 '한 명'뿐입니다.
[주문서 더미(큐)] ← 끝난 일들의 쪽지(클릭 발생, 타이머 완료, 응답 도착)가 쌓이는 곳
↓ 한 장씩 집어서
[요리사(콜 스택)] ← 지금 만들고 있는 요리 딱 하나
↑ 기다리는 일은 위임
[홀 직원(브라우저)] ← 타이머, 네트워크 요청처럼 '언제 끝날지 모르는 일'을
요리사 대신 맡아 주는 손들
핵심: 요리사는 절대 오븐 앞에서 멍하니 기다리지 않아요.
"다 구워지면 주문서 더미에 쪽지 넣어 줘" 하고 다음 요리로 넘어갑니다.
요리사가 손이 빌 때마다 주문서 더미에서 쪽지를 집어 처리하는
이 순환 구조가 바로 '이벤트 루프'입니다.`;
const CODE_CALLBACK = `// 콜백: "이 일이 끝나면 이 함수를 불러 줘" — 함수를 인자로 넘기는 방식
setTimeout(function () {
console.log('B: 3초 뒤에 실행됩니다');
}, 3000);
console.log('A: 이 줄이 먼저 찍힙니다!');
// 출력 순서: A → (3초 뒤) → B
// setTimeout은 타이머를 브라우저(홀 직원)에게 맡기고 '즉시' 다음 줄로 넘어가요.
// 우리가 넘긴 함수는 쪽지에 적혀 있다가, 3초 뒤에 이벤트 루프가 집어 실행합니다.`;
const CODE_HELL = `// 환율을 받고 → 환전 계산하고 → 기록을 저장하는 3단계를 콜백으로 쓰면...
getRate('USD', function (rate) {
convert(1000, rate, function (result) {
saveHistory(result, function () {
console.log('완료!');
}, function (err) {
console.error('기록 실패', err); // 에러 처리 ①
});
}, function (err) {
console.error('계산 실패', err); // 에러 처리 ②
});
}, function (err) {
console.error('환율 실패', err); // 에러 처리 ③
});
// 오른쪽으로 계속 밀려나는 계단 모양 — 이른바 '콜백 지옥(callback hell)'.
// 단계가 하나 늘 때마다 들여쓰기가 한 층 깊어지고,
// 에러 처리는 세 군데로 흩어져서 하나라도 빠뜨리기 딱 좋습니다.`;
const CODE_PROMISE_STATE = `// Promise: "지금은 결과가 없지만, 나중에 꼭 주겠다"는 약속 객체
const promise = fetch('https://open.er-api.com/v6/latest/USD');
// 갓 만들어진 promise의 상태는 pending(대기 중).
// 시간이 지나면 둘 중 하나로 '단 한 번' 정착합니다.
//
// pending ──┬──→ fulfilled(이행) 성공! 결과값을 품는다
// └──→ rejected(거부) 실패! 에러를 품는다
//
// 한번 정착(settled)한 상태는 절대 다시 바뀌지 않아요.
// fulfilled였다가 rejected가 되는 일은 없습니다 — 그래서 '믿고 기다릴' 수 있어요.`;
const CODE_THEN = `// 콜백 지옥의 3단계를 then 체이닝으로 다시 쓰면
getRate('USD')
.then(function (rate) { return convert(1000, rate); }) // 1단계 → 2단계
.then(function (result) { return saveHistory(result); }) // 2단계 → 3단계
.then(function () { console.log('완료!'); })
.catch(function (err) {
// 위 어느 단계에서 실패해도 전부 여기 '한 곳'으로 모입니다
console.error('어딘가에서 실패:', err);
});
// 같은 일을 하는데 — 계단이 사라지고, 에러 처리가 catch 하나로 모였어요.
// 비밀은 then이 '항상 새 Promise를 돌려준다'는 것.
// 그래서 사슬(chain)처럼 계속 이어 붙일 수 있습니다.`;
const CODE_AWAIT = `// 같은 로직을 async/await로 — '동기처럼 읽히는' 비동기 코드
async function run() {
try {
const rate = await getRate('USD'); // Promise가 정착할 때까지 대기
const result = await convert(1000, rate);
await saveHistory(result);
console.log('완료!');
} catch (err) {
// then 체인의 catch와 같은 역할 — 실패가 한 곳으로 모입니다
console.error('어딘가에서 실패:', err);
}
}
// await는 마법이 아니라 '문법 설탕'이에요. 속은 여전히 Promise인데,
// then 체인을 위→아래로 읽히는 모양으로 바꿔 줄 뿐입니다.
// 규칙 두 가지: ① await는 async 함수 안에서만 ② await가 기다리는 건 Promise`;
const CODE_FETCH = `// fetch는 '2단계'로 동작합니다 — 응답 도착 확인 → 본문 해석
async function getUsdRate() {
const res = await fetch('https://open.er-api.com/v6/latest/USD'); // ① 응답 도착
if (!res.ok) { // ② 상태 코드 확인은 '습관'!
throw new Error('HTTP 오류: ' + res.status);
}
const data = await res.json(); // ③ 본문(JSON)을 마저 받아 해석 — 이것도 Promise
return data.rates.KRW; // 1달러 = ? 원
}
// 함정: fetch는 404·500 응답에도 reject되지 '않아요'!
// 서버와 연결 자체가 실패(네트워크 끊김 등)했을 때만 reject됩니다.
// 서버가 "그런 거 없는데요(404)"라고 '대답'한 건 fetch 입장에선 성공이거든요.
// 그래서 res.ok(상태 200~299) 확인은 선택이 아니라 습관이어야 합니다.`;
const CODE_POST = `// 옵션 객체로 메서드·헤더·본문을 지정하면 POST도 보낼 수 있어요
const res = await fetch('/api/favorites', {
method: 'POST', // 기본값은 GET
headers: {
'Content-Type': 'application/json', // "본문은 JSON이야"라고 알려주기
},
body: JSON.stringify({ currency: 'USD', amount: 1000 }), // 객체 → JSON 문자열
});
if (!res.ok) throw new Error('저장 실패: ' + res.status);
const saved = await res.json();
// 보낼 때는 JSON.stringify(객체 → 문자열), 받을 때는 res.json()(문자열 → 객체).
// 이 왕복 변환이 '서버와 대화'의 기본 문법입니다.`;
const CODE_PARALLEL = `// 방법 A: 순차 await — 앞 요청이 '끝나야' 다음이 출발 (직렬)
const usd = await fetch(BASE + '/USD').then(function (r) { return r.json(); });
const eur = await fetch(BASE + '/EUR').then(function (r) { return r.json(); });
const jpy = await fetch(BASE + '/JPY').then(function (r) { return r.json(); });
// 요청당 0.3초라면 → 합계 약 0.9초
// 방법 B: Promise.all — 셋을 '동시에' 출발시키고 전부 끝나길 기다림 (병렬)
const [usd2, eur2, jpy2] = await Promise.all([
fetch(BASE + '/USD').then(function (r) { return r.json(); }),
fetch(BASE + '/EUR').then(function (r) { return r.json(); }),
fetch(BASE + '/JPY').then(function (r) { return r.json(); }),
]);
// 동시에 달리므로 → 약 0.3초. '가장 느린 요청 하나'만큼만 걸려요.
// 서로 의존하지 않는 요청(앞 결과가 뒤 요청에 필요 없는 경우)이라면
// 병렬이 기본값이어야 합니다. console.time()으로 직접 재 보세요!`;
const CODE_ALLSETTLED = `// Promise.all의 성격: 하나라도 실패하면 '전체가' 실패(reject)
// USD·EUR은 성공했는데 JPY만 실패해도 → 성공한 둘의 결과까지 함께 버려집니다.
// "전부 있어야만 의미 있는 데이터"라면 이게 오히려 올바른 동작이에요.
// 부분 실패를 허용하고 싶다면 → Promise.allSettled
const results = await Promise.allSettled([pUsd, pEur, pJpy]);
results.forEach(function (r) {
if (r.status === 'fulfilled') {
console.log('성공:', r.value); // 성공한 것만 화면에 그리고
} else {
console.log('실패:', r.reason); // 실패한 통화엔 "조회 실패" 표시
}
});
// allSettled는 절대 reject되지 않아요 — 성공이든 실패든
// '전원의 결과 보고서'를 배열로 돌려줍니다.`;
const CODE_WIDGET = `// 실습 골격: 로딩·성공·실패 3상태를 갖춘 환율 검색 위젯 (바닐라 JS)
const box = document.querySelector('#rate-box');
async function loadRate(code) {
box.innerHTML = '<span class="spin"></span> 불러오는 중...'; // 상태 ① 로딩
try {
const res = await fetch('https://open.er-api.com/v6/latest/' + code);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
const krw = Math.round(data.rates.KRW);
box.textContent = '1 ' + code + ' = ' + krw + '원'; // 상태 ② 성공
} catch (err) {
box.innerHTML = '환율을 불러오지 못했어요. ' +
'<button id="retry">다시 시도</button>'; // 상태 ③ 실패
document.querySelector('#retry')
.addEventListener('click', function () { loadRate(code); }); // 재시도 = 같은 함수 재호출
}
}
loadRate('USD');
// 포인트: 화면은 언제나 로딩·성공·실패 '셋 중 하나'의 상태에 있습니다.
// 세 상태를 전부 코드로 표현했는지가 '되다 만 위젯'과 '완성된 위젯'의 차이예요.`;
const CODE_ABORT = `// AbortController — 이미 보낸 요청에 '그만!'을 외치는 리모컨
const controller = new AbortController();
const promise = fetch(url, { signal: controller.signal }); // 리모컨 신호선 연결
// 사용자가 검색 통화를 USD → EUR로 바꿨다면? 이전 요청은 취소
controller.abort(); // promise가 'AbortError'라는 이름의 에러로 reject됨
// ── 응용: 타임아웃 패턴 — 5초 안에 응답 없으면 포기 ──
const ctrl = new AbortController();
const timer = setTimeout(function () { ctrl.abort(); }, 5000);
try {
const res = await fetch(url, { signal: ctrl.signal });
clearTimeout(timer); // 제때 왔으면 타이머 해제
const data = await res.json();
} catch (err) {
if (err.name === 'AbortError') {
// 느린 서버 — 무한 로딩 대신 "다시 시도" 버튼을 보여 줍시다
} else {
// 그 밖의 진짜 실패 (네트워크 끊김 등)
}
}`;
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다(React 입문 섹션 2). props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
return (
<section className="step-card" id={`sec-${n}`}>
<div className="step-head">
<span className="step-num">{n}</span>
<div>
<h3>{title}</h3>
{sub && <span className="step-sub">{sub}</span>}
</div>
</div>
<div className="step-body">{children}</div>
</section>
);
}
function Code({ children }) {
return <pre className="code-block"><code>{children}</code></pre>;
}
const SECTIONS = [
{ n: 1, label: '왜 비동기인가' },
{ n: 2, label: '콜백 지옥' },
{ n: 3, label: 'Promise 3상태' },
{ n: 4, label: 'async/await' },
{ n: 5, label: 'fetch 실전' },
{ n: 6, label: 'Promise.all' },
{ n: 7, label: '로딩·취소·타임아웃' },
];
export default function Lv2AsyncJavascriptPage() {
return (
<div>
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
<div className="hero">
<div className="eyebrow">Course · 중급 과정</div>
<h1>비동기 JavaScript<br /> fetch와 Promise 정복</h1>
<p>
기초 문법을 배웠는데도 "진짜 서비스" 만들어지는 이유의 절반은{' '}
<strong>비동기</strong> . , JavaScript
가지 일밖에 하니까요. 코스에서 콜백 지옥에서 <strong>async/await</strong>
진화를 따라 걷고, 마지막엔 로딩 스피너·에러 메시지·재시도 버튼을 갖춘{' '}
<strong>환율 검색 위젯</strong> .
</p>
<div className="chip-row">
<span className="chip">예상 소요 60</span>
<span className="chip">실습: 환율 검색 위젯</span>
<span className="chip">선수 지식: JS 기초 문법</span>
</div>
</div>
{/* 섹션 바로가기 */}
<nav className="pill-nav">
{SECTIONS.map((s) => (
<a key={s.n} href={`#sec-${s.n}`}>
<b>{s.n}</b>
{s.label}
</a>
))}
</nav>
<Section n={1} title="왜 비동기인가" sub="동기 코드가 화면을 멈추게 하는 순간">
<p>
출발점은 체험이에요. 아래 코드를 브라우저 콘솔에 붙여 넣고 실행해 보세요. 함수가 도는
3 동안 <strong>클릭도, 스크롤도, 입력도 전부 먹통</strong> . JavaScript{' '}
<strong>싱글 스레드</strong> ,
그릴 사람이 없거든요.
</p>
<Code>{CODE_FREEZE}</Code>
<p>
그런데 서비스의 핵심 일과는 <strong>네트워크 요청</strong>. 0.1
대답할지, 3 걸릴지, 영영 대답이 없을지 <strong>보내기 전엔 없죠.</strong>
"언제 끝날지 모르는 일" 일꾼이 자리에서 기다리면 앱은 서버가 느린 만큼 얼어붙어요.
그래서 JavaScript는 다른 전략을 택합니다 <strong>기다리지 않고, 맡기는</strong> .
</p>
<Code>{CODE_KITCHEN}</Code>
<div className="tip">
<b> 비유를 기억하세요</b> ( ) , ()
대신 맡고, 끝난 일은 주문서 더미() 쌓였다가 요리사 손이 처리된다 {' '}
<strong>이벤트 루프</strong> (·Promise·await)
"주문서를 어떻게 예쁘게 쓰느냐" 문제로 보이기 시작합니다.
</div>
</Section>
<Section n={2} title="콜백과 콜백 지옥" sub="'나중에 실행해 줘'의 가장 원시적인 표현">
<p>
"일을 맡겼으면, 끝났을 때 뭘 할지도 알려 줘야" 하죠. 가장 원시적인 방법이{' '}
<strong>콜백(callback)</strong> <strong> </strong>{' '}
방식입니다. 이미 보셨을 거예요. <span className="icode">addEventListener</span>
번째 인자, <span className="icode">setTimeout</span> .
</p>
<Code>{CODE_CALLBACK}</Code>
<p>
단계짜리 일이면 콜백도 충분해요. 문제는 <strong>순차 작업</strong>. "
받아서 그걸로 환전 계산을 하고 결과를 기록한다"처럼 앞 결과가 다음 단계에 필요하면,
콜백 안에 콜백을, 그 안에 또 콜백을 넣을 수밖에 없어요.
</p>
<Code>{CODE_HELL}</Code>
<div className="warn">
<b>진짜 문제는 들여쓰기가 아니에요</b> 계단 모양은 보기 싫은 정도지만,{' '}
<strong>에러 처리가 세 군데로 흩어진 것</strong>은 사고로 이어집니다. 단계마다 실패
콜백을 따로 달아야 하고, 하나라도 깜빡하면 그 에러는 조용히 증발해요. "실패를 곳에
모을 없을까?" — 이 질문에 대한 답이 다음 섹션의 Promise입니다.
</div>
</Section>
<Section n={3} title="Promise의 3가지 상태" sub="pending fulfilled 또는 rejected, ">
<p>
<strong>Promise</strong>는 "지금은 결과가 없지만 나중에 주겠다"는{' '}
<strong>약속을 담은 객체</strong>예요. 콜백처럼 함수를 미리 찔러 넣는 게 아니라, 일단
약속 객체를 <strong>손에 받아 두고</strong> 거기에 "성공하면 이거, 실패하면 저거"를
붙이는 방식이죠. 약속의 일생은 세 상태로 요약됩니다.
</p>
<Code>{CODE_PROMISE_STATE}</Code>
<svg className="diagram" viewBox="0 0 640 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Promise 상태 전이 다이어그램. pending 상태에서 시작해 성공하면 fulfilled, 실패하면 rejected로 단 한 번 전이하며, 한번 정착한 상태는 다시 바뀌지 않는다.">
<rect x="40" y="80" width="150" height="60" rx="10" fill="var(--card)" stroke="var(--amber)" strokeWidth="2" />
<text x="115" y="106" textAnchor="middle" fontSize="14" fontWeight="600" fill="var(--ink)">pending</text>
<text x="115" y="126" textAnchor="middle" fontSize="11" fill="var(--muted)">대기 ...</text>
<rect x="430" y="24" width="170" height="60" rx="10" fill="var(--card)" stroke="var(--teal)" strokeWidth="2" />
<text x="515" y="50" textAnchor="middle" fontSize="14" fontWeight="600" fill="var(--ink)">fulfilled</text>
<text x="515" y="70" textAnchor="middle" fontSize="11" fill="var(--muted)">성공 · 결과값 보유 then</text>
<rect x="430" y="136" width="170" height="60" rx="10" fill="var(--card)" stroke="var(--primary)" strokeWidth="2" />
<text x="515" y="162" textAnchor="middle" fontSize="14" fontWeight="600" fill="var(--ink)">rejected</text>
<text x="515" y="182" textAnchor="middle" fontSize="11" fill="var(--muted)">실패 · 에러 보유 catch</text>
{/* pending → fulfilled */}
<line x1="190" y1="96" x2="422" y2="60" stroke="var(--teal)" strokeWidth="2" />
<path d="M430 58 L418 56 L420 66 Z" fill="var(--teal)" />
<text x="300" y="60" textAnchor="middle" fontSize="11" fill="var(--teal)">성공 (resolve)</text>
{/* pending → rejected */}
<line x1="190" y1="124" x2="422" y2="160" stroke="var(--primary)" strokeWidth="2" />
<path d="M430 162 L420 154 L418 164 Z" fill="var(--primary)" />
<text x="300" y="164" textAnchor="middle" fontSize="11" fill="var(--primary)">실패 (reject)</text>
<text x="320" y="212" textAnchor="middle" fontSize="12" fill="var(--muted)">전이는 정착(settled) 뒤엔 절대 되돌아가지 않는다</text>
</svg>
<p className="muted" style={{ fontSize: 12.5, textAlign: 'center', marginTop: 2 }}>그림: Promise는 pending에서 출발해 fulfilled 또는 rejected 하나로 정착한다</p>
<p>
약속에 후속 작업을 붙이는 문법이 <span className="icode">then</span>( ){' '}
<span className="icode">catch</span>( ). {' '}
<span className="icode">then</span> <strong> Promise </strong> ,
콜백 지옥의 계단을 <strong>수평 사슬</strong> .
</p>
<Code>{CODE_THEN}</Code>
<div className="tip">
<b>비유</b> Promise <strong> </strong>. ()
진동벨(Promise) 먼저 받죠. 벨을 손에 자리에서 일을 하다가 울리면(fulfilled)
음료를 받고, 점원이 와서 "재료가 떨어졌어요" 하면(rejected) 환불 처리를 합니다. 벨이
다르게 울리는 일은 없어요. <strong> , 하나.</strong>
</div>
</Section>
<Section n={4} title="async/await로 다시 쓰기" sub="then 체인을 '위에서 아래로' 읽히게">
<p>
then 체인은 훌륭하지만, 로직이 길어지면{' '}
<span className="icode">function</span> <span className="icode">return</span>
반복되며 눈이 미끄러져요. 그래서 나온 최종 진화형이{' '}
<strong>async/await</strong> Promise <strong> </strong>
으로 쓰는 문법입니다. 섹션 3 체인을 그대로 옮겨 볼게요.
</p>
<Code>{CODE_AWAIT}</Code>
<p>
짚어 것은 <strong>await가 '문법 설탕(syntactic sugar)'</strong> .
동작이 생긴 아니라, then 체인과 <strong>같은 </strong> .{' '}
<span className="icode">await</span> Promise
멈추는데 섹션 1 <span className="icode">while</span> {' '}
<strong>화면은 얼지 않아요.</strong> , ( )
처리하러 가거든요.
</p>
<p>
에러 처리는 우리가 아는 <span className="icode">try/catch</span> .{' '}
<span className="icode">try</span> <strong> await </strong> {' '}
<span className="icode">catch</span> .
처리가, 여기선 익숙한 문법 곳으로 모입니다.
</p>
<div className="warn">
<b>자주 하는 실수</b> <span className="icode">await</span>{' '}
<span className="icode">async</span> . {' '}
<span className="icode">async</span> <strong> Promise</strong> {' '}
<span className="icode">return 42</span> {' '}
<span className="icode">Promise</span> , {' '}
<span className="icode">await</span> . "async " .
</div>
</Section>
<Section n={5} title="fetch로 진짜 API 호출" sub="response.ok 확인은 선택이 아니라 습관">
<p>
이제 도구가 갖춰졌으니 <strong>진짜 서버</strong> .{' '}
<span className="icode">fetch</span> HTTP , Promise
돌려줘요. 구조에서 헷갈리기 쉬운 <strong>await가 </strong>
응답(헤더) 도착하길 기다리고, 본문이 내려와 해석되길 기다립니다.
</p>
<Code>{CODE_FETCH}</Code>
<p>
주석의 함정을 기억하세요. <span className="icode">fetch</span>{' '}
<strong>404 500 같은 에러 응답에도 reject되지 않습니다.</strong> ""
대답한 것과, 대답 자체가 (네트워크 단절) 구분하기 때문이에요. HTTP 수준의
실패를 잡으려면 <span className="icode">res.ok</span>( 200~299 true)
직접 <span className="icode">throw</span>, 4 {' '}
<span className="icode">catch</span> .
</p>
<p>
데이터를 <strong>보내는</strong> . , ,
지정하면 POST 요청이 됩니다.
</p>
<Code>{CODE_POST}</Code>
<div className="tip">
<b>직접 확인해 보기</b> {' '}
<span className="icode">await fetch('https://open.er-api.com/v6/latest/USD').then(r =&gt; r.json())</span>
실행해 보세요. 공개 환율 API라 없이도 응답이 옵니다.{' '}
<span className="icode">rates.KRW</span> / 7
API를 그대로 씁니다. <span className="kbd">F12</span> Network
상태 코드와 응답 본문도 눈으로 확인해 보세요.
</div>
</Section>
<Section n={6} title="병렬 처리: Promise.all" sub="순차 await의 숨은 낭비를 걷어내기">
<p>
await가 편하다 보니 생기는 <strong>흔한 낭비</strong> .
개를 순서대로 <span className="icode">await</span>, {' '}
<strong>출발</strong>. ·· .
</p>
<Code>{CODE_PARALLEL}</Code>
<p>
<span className="icode">Promise.all</span> Promise {' '}
<strong>전부 fulfilled되면</strong> ( ) . ,
성격이 확실해요 <strong>하나라도 rejected되면 전체가 즉시 reject</strong>.
"전원 성공 아니면 전체 실패". 부분 실패를 살리고 싶을 {' '}
<span className="icode">Promise.allSettled</span> .
</p>
<Code>{CODE_ALLSETTLED}</Code>
<div className="tip">
<b>고르는 기준</b> <strong> </strong> {' '}
<span className="icode">all</span>( ). {' '}
<strong>각자 독립</strong> {' '}
<span className="icode">allSettled</span>( / ). {' '}
<strong>화면 요구사항</strong> .
</div>
</Section>
<Section n={7} title="실전 패턴: 로딩·취소·타임아웃" sub="환율 위젯을 '완성품'으로 만드는 마지막 조각">
<p>
문법을 배워도 실전 코드엔 층이 있어요. 요청이 나가 있는 동안 사용자는{' '}
<strong>뭔가를 보고 있어야</strong> , <strong> </strong>()
제안받아야 하죠. 그래서 서버와 대화하는 화면은 언제나{' '}
<strong>로딩 · 성공 · 실패</strong> 3 .
환율 검색 위젯의 골격이 바로 3상태 관리예요.
</p>
<Code>{CODE_WIDGET}</Code>
<p>
여기에 실무 감각 가지를 더하면 완성입니다. 첫째, <strong>취소</strong>.
검색을 연타하면 이전 요청은 의미가 없어지는데, 늦게 도착한 응답이 화면을 덮어쓰는
사고가 나요. 둘째, <strong>타임아웃</strong>.
무한히 돕니다. <span className="icode">AbortController</span> .
</p>
<Code>{CODE_ABORT}</Code>
<div className="warn">
<b>AbortError는 '실패' 아닐 있어요</b> {' '}
<strong>우리가 스스로</strong> catch . " !"
표시하면 이상하겠죠. 그래서 catch 안에서{' '}
<span className="icode">err.name === 'AbortError'</span>
의도한 취소는 조용히, 진짜 실패만 에러 메시지로.
</div>
<div className="tip">
<b>실습 과제</b> . +
검색 로딩 스피너 성공하면 환율 표시 실패하면 에러 메시지와 재시도 버튼
5 타임아웃까지. 완성하면 여러분은 <strong>실제 서비스의 데이터 화면</strong>
구조를 처음부터 끝까지 만들어 겁니다.
</div>
</Section>
<div className="step-card">
<div className="step-body">
<h3 style={{ marginBottom: 6 }}> 여기까지 왔다면</h3>
<p className="muted">
이제 "서버와 대화하는 코드" 전체 지형이 손에 있어요. 이벤트 루프(주방), 콜백 지옥이
남긴 교훈, Promise의 3상태(진동벨), async/await라는 문법 설탕, fetch의 2단계와{' '}
<strong>res.ok 습관</strong>, Promise.all , ··
환율 위젯 하나에 전부 담아 봤습니다. 그런데 서버에서 받아 데이터는 대부분{' '}
<strong>배열</strong>. (filter), (map),
합치는(reduce) 손맛이 다음 관문입니다. 다음{' '}
<Link to="/learn/lv2-array-data-wrangling"><strong>배열 메서드로 데이터 요리하기</strong></Link>{' '}
코스에서, fetch로 받아 데이터를 화면에 올리기 좋게 다듬는 기술을 이어서 익혀 봅시다.
</p>
</div>
</div>
</div>
);
}