- 분야: FE
- 기술 스택: React · TypeScript · Fetch API
- 관련 기술: 요청 순번 · 부분 로딩 · 비동기 상태
문제와 원인
SW 이론을 다시 요청하면 contentLoading 분기가 현재 목록·본문보다 먼저 반환해 화면을 비웠다. 요청 A 다음에 B를 시작하고 B가 먼저 완료돼도, 늦은 A의 성공·오류·종료 처리가 B의 콘텐츠와 로딩 상태를 바꿨다.
수정 전 코드
loadTheories()
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
const response = await fetch(
`/api/sw-study?view=theories&subjects=${encodeURIComponent(selectedSubjectQuery())}`,
);
const payload = await response.json() as { theories?: SwTheoryItem[]; error?: string };
if (!response.ok || !payload.theories) throw new Error(payload.error || "이론을 불러오지 못했습니다.");
setTheoryItems(payload.theories);
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
} catch (error) {
setContentError(learnerSafeErrorMessage(error, "이론을 불러오지 못했습니다."));
setContentRetry(() => () => {
void loadTheories();
});
} finally {
setContentLoading(false);
}
로딩 렌더링
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
if (contentLoading) {
return (
<div className="sw-curriculum-page" aria-busy="true">
<span className="sr-only" role="status">SW 학습 콘텐츠를 불러오고 있습니다.</span>
</div>
);
}
수정 후 코드
요청 시작 시 순번을 증가시키고, 성공·오류·종료 처리 전에 현재 순번과 비교하도록 바꿨다. 세션 초기화에서도 순번을 증가시켜 이전 화면의 요청을 무효화한다.
loadTheories()
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
const requestId = ++swContentRequestRef.current;
setContentLoading(true);
setContentError("");
setContentRetry(null);
try {
void loadMarkdownRenderer();
const payload = await requestSwStudyData<{ theories?: SwTheoryItem[] }>({
view: "theories",
params: { subjects: selectedSubjectQuery() },
cacheMode: "reuse",
});
if (requestId !== swContentRequestRef.current) return;
if (!payload.theories) throw new Error("이론을 불러오지 못했습니다.");
setTheoryItems(payload.theories);
setSelectedTheory(null);
onTheoryChange(null);
showView("theories");
window.scrollTo({ top: 0, behavior: "smooth" });
} catch (error) {
if (requestId !== swContentRequestRef.current) return;
setContentError(learnerSafeErrorMessage(error, "이론을 불러오지 못했습니다."));
setContentRetry(() => () => { void loadTheories(); });
} finally {
if (requestId === swContentRequestRef.current) setContentLoading(false);
}
로딩 렌더링
전체 반환 분기를 제거하고 기존 콘텐츠 안에 부분 로딩 표시를 넣었다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
<div className="page-stack sw-curriculum-page sw-content-page" aria-busy={contentLoading}>
{contentLoading && <SwContentLoadingIndicator />}
{/* 기존 탐색·검색·목록 렌더링 */}
</div>
검증 결과
응답 순서별 결과
macOS arm64, Node.js, TypeScript에서 전후 원본 loadTheories·openTheory 스냅샷의 네트워크·상태 setter를 제어 가능한 대역으로 연결했다. 두 함수마다 A·B 요청을 시작하고 합성 ID 1·2의 성공·실패·완료 순서 3종을 전후 각각 1회 적용했다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 각 함수의 완료 조건 | 수정 전 | 수정 후 |
|---|---|---|
| B 성공 → A 성공 | 표시 ID 2 → 1 | ID 2 유지 |
| B 성공 → A 실패 | old request failed 표시 | 오류 없음·ID 2 유지 |
| B 대기 중 A 종료 | loading=false | loading=true 유지 |
| 전체 6조건의 기대 상태 유지 | 0/6개 | 6/6개 |
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 코드 위치 집계 | 수정 전 | 수정 후 |
|---|---|---|
| 전체 로딩 반환 분기 | 1개 | 0개 |
| 부분 로딩 표시 | 0곳 | 7곳 |
| 두 이론 함수의 순번 검사 | 0곳 | 6곳 |
본문 보존과 최신 응답 유지
로컬 React 재현 화면은 같은 원본 함수·로딩 컴포넌트와 합성 응답을 연결했다. 화면 조건은 1,280×720 CSS px·DPR 2이며, 요청 전 콘텐츠 ID 99에서 두 함수의 순서 3종을 전후 각각 1회 적용했다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 화면 조건 | 수정 전 DOM | 수정 후 DOM |
|---|---|---|
| 본문 요청 A 대기 | 현재 본문 없음·aria-busy=true | ID 99 유지·aria-busy=true |
| B 성공 → A 성공 | 목록·본문 ID 1 | 목록·본문 ID 2 |
| B 성공 → A 실패 | 목록·본문에 요청 A의 합성 네트워크 오류 | 오류 없음·ID 2 유지 |
| B 대기 중 A 성공 | 목록·본문 ID 1·aria-busy=false | ID 99·aria-busy=true |
A 응답 대기 중 기존 구현은 현재 본문을 지웠고, 수정 후에는 현재 본문과 부분 로딩 표시를 함께 유지했다.
B 성공 뒤 늦게 도착한 A 성공은 기존 구현의 본문을 A로 덮었다. 순번 검사 후에는 B 본문이 유지됐다.
늦은 A 실패는 기존 구현의 B 본문에 오류를 남겼다.