
프론트엔드 개발을 하면서 가장 많이 놓쳤던 부분은 “정상 응답일 때 화면이 잘 보이는가”만 확인했다는 점입니다. API가 빠르게 응답하고, 이미지가 정상 로딩되고, localStorage 데이터가 깨지지 않는 상황에서는 대부분의 화면이 문제없이 보였습니다. 하지만 실제 사용자는 항상 좋은 네트워크에서만 접속하지 않습니다. API가 늦게 오거나, 지하철에서 네트워크가 끊기거나, 이미지 CDN이 실패하거나, 브라우저 저장소가 비정상 상태가 될 수도 있습니다.
그래서 2026년 3월 1일부터 2026년 3월 31일까지 한 달 동안 프론트엔드 화면에 일부러 장애를 주입해봤습니다. 테스트 환경은 React, Chrome DevTools, MSW, Playwright, Windows 11이었습니다. 총 12개 화면을 대상으로 실험했고, 주입한 장애 시나리오는 38회였습니다. API 응답 지연 테스트 12회, 네트워크 오프라인 테스트 8회, 이미지 로딩 실패 테스트 7회, localStorage 손상 테스트 5회, 500 에러 응답 테스트 6회를 진행했습니다.
처음에는 서버 장애만 생각했다
처음에는 카오스엔지니어링이라고 하면 서버를 일부러 죽이거나 API 응답을 실패시키는 것만 떠올렸습니다. 하지만 프론트엔드 입장에서 더 중요한 것은 서버가 실패했을 때 사용자가 화면에서 무엇을 보느냐였습니다. 같은 500 에러라도 어떤 화면은 안내 메시지를 보여줬고, 어떤 화면은 빈 화면만 남겼습니다.
실험 초반에 발견한 UI 문제는 총 19건이었습니다. 그중 상당수는 기능 버그라기보다 실패 상황에서 사용자가 길을 잃는 문제였습니다. 버튼이 사라지거나, 로딩 스피너가 멈추거나, 이미지 실패 때문에 레이아웃이 밀리거나, localStorage 값이 깨져 전체 화면 렌더링이 중단되는 식이었습니다.
가장 큰 실패: API 지연 5초에서 빈 화면만 남았다
가장 크게 당황했던 사례는 API 응답을 일부러 5초 지연시킨 테스트였습니다. 정상 상황에서는 목록 화면이 1초 안에 렌더링됐기 때문에 문제가 없어 보였습니다. 하지만 MSW로 응답을 5초 늦추자 처음 2초 동안은 로딩 스피너가 보이다가, 이후 스피너가 사라지고 빈 화면만 남았습니다.
사용자 입장에서 보면 페이지가 끝난 것인지, 아직 불러오는 중인지, 오류가 난 것인지 알 수 없는 상태였습니다. 콘솔에는 에러가 없었고, 네트워크 탭에서는 요청이 pending 상태였습니다. 문제는 로딩 상태를 컴포넌트 내부에서만 짧게 관리하고 있었고, 일정 시간이 지나면 빈 상태 UI로 넘어가도록 되어 있었다는 점입니다.
이 문제를 해결하기 위해 skeleton UI와 timeout 처리를 추가했습니다. 1초 이내에는 skeleton UI를 보여주고, 4초 이상 응답이 없으면 “데이터를 불러오는 중입니다. 네트워크 상태에 따라 시간이 걸릴 수 있습니다.”라는 메시지를 표시했습니다. 개선 후 평균 복구 메시지 노출 시간은 초기 0초에서 1.2초 이내로 표시되도록 바뀌었습니다.
장애 시나리오 테스트표
| 장애 유형 | 테스트 횟수 | 발견한 문제 | 사용자에게 보인 증상 | 개선 방식 | 개선 후 결과 |
|---|---|---|---|---|---|
| API 응답 지연 | 12회 | 로딩 상태가 중간에 사라짐 | 5초 지연 상황에서 빈 화면만 표시됨 | skeleton UI, timeout 처리, 지연 안내 메시지 추가 | 1.2초 이내 복구 메시지 표시 |
| 네트워크 오프라인 | 8회 | 오프라인 상태를 사용자가 알 수 없음 | 버튼 클릭 후 아무 반응이 없는 것처럼 보임 | offline banner, 재시도 버튼 추가 | 오프라인 상태와 재시도 방법을 화면에 표시 |
| 이미지 로딩 실패 | 7회 | 이미지 영역 높이가 무너짐 | 카드 하단 버튼이 위로 밀리거나 겹침 | fallback image, 고정 aspect-ratio 적용 | 이미지 실패에도 레이아웃 유지 |
| localStorage 손상 | 5회 | 잘못된 JSON 파싱으로 렌더링 중단 | 설정 화면이 열리지 않거나 흰 화면 발생 | try-catch, 기본값 복구, 손상 데이터 초기화 | 손상된 값이 있어도 기본 설정으로 복구 |
| 500 에러 응답 | 6회 | 에러 메시지가 개발자용 문구로 노출 | 사용자가 이해하기 어려운 에러 코드만 표시 | error boundary, 사용자용 메시지, retry button 적용 | 실패 원인과 재시도 동작을 명확히 제공 |
이미지 실패 하나로 버튼 위치가 밀렸다
또 다른 실패 사례는 이미지 로딩 실패였습니다. 카드형 UI에서 이미지가 로딩되지 않으면 단순히 대체 이미지만 보이면 될 줄 알았습니다. 그런데 실제로 이미지 URL을 실패시키자 이미지 영역의 높이가 0에 가깝게 줄어들었고, 아래에 있던 설명 문구와 버튼이 위로 밀렸습니다.
특히 모바일 뷰에서는 버튼이 카드 밖으로 튀어나오는 것처럼 보였습니다. 이미지 실패 자체보다 레이아웃이 무너지는 것이 더 큰 문제였습니다. 사용자 입장에서는 “이미지가 안 보인다”가 아니라 “화면 전체가 깨졌다”고 느낄 수 있는 상태였습니다.
이후 이미지 컴포넌트에는 fallback image를 적용하고, 이미지 로딩 실패 여부와 관계없이 같은 높이를 유지하도록 aspect-ratio를 고정했습니다. 이미지가 실패해도 카드 크기와 버튼 위치가 유지되도록 바꿨습니다.
오프라인 테스트에서 가장 많이 보인 증상
Chrome DevTools로 네트워크를 offline 상태로 바꿔 8회 테스트했습니다. 가장 많이 보인 문제는 사용자가 실패를 알 수 없다는 것이었습니다. 저장 버튼을 눌렀는데 아무 변화가 없거나, 목록 새로고침 버튼을 눌러도 그대로 멈춰 있는 화면이 있었습니다.
기술적으로는 fetch가 실패했지만, 화면에는 아무 메시지가 없었습니다. 이런 경우 사용자는 다시 누르거나 새로고침하거나 앱을 닫습니다. 그래서 offline banner를 추가했습니다. 네트워크 상태를 감지해 상단에 “오프라인 상태입니다. 저장되지 않은 변경사항이 있을 수 있습니다.”라는 안내를 보여주고, 재시도 버튼을 넣었습니다.
중요한 점은 오프라인 상태를 너무 크게 경고하지 않는 것이었습니다. 모든 화면을 막아버리면 사용성이 더 나빠졌습니다. 읽기 가능한 화면은 유지하되, 저장이나 새로고침처럼 네트워크가 필요한 행동에만 명확한 안내를 붙였습니다.
localStorage 손상은 생각보다 쉽게 발생했다
localStorage 손상 테스트는 5회 진행했습니다. 처음에는 별문제 없을 줄 알았습니다. 하지만 설정값을 잘못된 JSON 문자열로 바꾸거나, 예상 타입과 다른 값을 넣자 일부 화면에서 렌더링이 중단됐습니다.
예를 들어 사용자가 선택한 필터 값을 localStorage에서 읽어오는 화면이 있었습니다. 원래는 배열이 들어와야 하는데 문자열이 들어오자 map 함수에서 오류가 발생했습니다. 그 결과 화면 전체가 흰 화면으로 변했습니다.
이 문제는 try-catch와 기본값 복구로 해결했습니다. localStorage에서 읽은 값이 예상 형식이 아니면 기본값으로 초기화하고, 손상된 키를 제거했습니다. 이후 error boundary를 적용해 컴포넌트 하나의 실패가 전체 화면 실패로 번지지 않도록 바꿨습니다.
500 에러는 개발자가 아니라 사용자 언어로 보여야 했다
500 에러 응답 테스트는 6회 진행했습니다. 초기에는 에러 메시지가 너무 개발자 중심이었습니다. “Internal Server Error”나 API 응답 코드만 표시되는 화면도 있었습니다. 개발자는 원인을 짐작할 수 있지만, 사용자는 무엇을 해야 할지 알 수 없습니다.
그래서 에러 메시지를 사용자 행동 중심으로 바꿨습니다. “일시적으로 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해주세요.”처럼 원인을 단정하지 않고, 사용자가 할 수 있는 행동을 같이 제공했습니다. 재시도 버튼도 추가했습니다.
이 변화는 작아 보였지만 체감이 컸습니다. 실패 상황에서 사용자가 완전히 멈추지 않고 다시 시도할 수 있었기 때문입니다.
개선 후에도 재발한 문제 4건
개선 후 재발한 문제는 4건이었습니다. 대부분 새로 추가된 컴포넌트에서 기존 기준을 적용하지 않은 경우였습니다. 예를 들어 새 카드 컴포넌트에 fallback image를 빼먹거나, 새 목록 화면에서 loading timeout 처리를 빠뜨린 식입니다.
이 경험 때문에 장애 대응 UI 기준을 문서로 만들었습니다. 새 화면을 만들 때 API 지연, 오프라인, 이미지 실패, 500 에러, localStorage 손상을 최소 한 번씩 확인하도록 체크리스트에 넣었습니다. 테스트 자동화도 일부 적용했습니다.
Playwright로 자동화 가능한 부분과 어려운 부분
Playwright는 반복 확인에 도움이 됐습니다. 네트워크를 가로채 API 지연이나 500 응답을 만들고, 특정 이미지 요청을 실패시키는 방식으로 자동화할 수 있었습니다.
await page.route('**/api/products', async route => {
await new Promise(resolve => setTimeout(resolve, 5000));
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [] })
});
});
await page.route('**/*.jpg', route => route.abort());
await page.goto('/products');
await expect(page.getByText('데이터를 불러오는 중입니다')).toBeVisible();
다만 모든 실패 상황을 자동화하기는 어려웠습니다. 특히 사용자가 메시지를 이해할 수 있는지, 레이아웃이 시각적으로 안정적인지는 단순 assertion만으로 판단하기 어려웠습니다. 그래서 자동화 테스트와 실제 눈으로 확인하는 과정을 함께 사용했습니다.
비교 기준별 실제 체감
빈 화면 발생 여부
초기에는 API 지연이나 localStorage 오류에서 빈 화면이 나왔습니다. 개선 후에는 skeleton UI, error boundary, 기본값 복구를 적용해 흰 화면 발생을 줄였습니다.
사용자가 이해할 수 있는 메시지
처음에는 에러 코드나 빈 화면이 많았습니다. 이후에는 오프라인, 지연, 실패 상황을 사용자 언어로 안내했습니다. 평균 복구 메시지 노출 시간도 0초에서 1.2초 이내로 개선했습니다.
재시도 가능성
실패 후 사용자가 할 수 있는 행동이 중요했습니다. retry button을 넣은 뒤에는 새로고침 없이도 다시 요청할 수 있었습니다.
레이아웃 안정성
이미지 실패 시 레이아웃이 무너지는 문제가 컸습니다. fallback image와 고정 비율을 적용한 뒤 버튼 밀림 현상이 줄었습니다.
테스트 자동화 가능성
API 지연, 500 응답, 이미지 실패는 Playwright와 MSW로 자동화하기 쉬웠습니다. 하지만 메시지의 명확성이나 사용성은 여전히 수동 확인이 필요했습니다.
결론: 프론트엔드 카오스엔지니어링은 사용자가 길을 잃지 않게 만드는 작업이었다
2026년 3월 1일부터 3월 31일까지 React 프로젝트에서 38회 장애 시나리오를 주입해본 결론은 분명합니다. 프론트엔드 카오스엔지니어링은 서버 장애를 흉내 내는 것이 아니라, 사용자가 실패 상황에서도 길을 잃지 않게 만드는 작업이었습니다.
12개 화면에 대해 API 응답 지연 12회, 네트워크 오프라인 8회, 이미지 로딩 실패 7회, localStorage 손상 5회, 500 에러 응답 6회를 테스트했습니다. 처음 발견한 UI 문제는 19건이었고, 개선 후 재발한 문제는 4건이었습니다.
가장 큰 실패는 API 지연 5초 상황에서 로딩 스피너가 사라지고 빈 화면만 남은 경험이었습니다. 또 이미지 로딩 실패 시 레이아웃 높이가 무너져 버튼이 밀리는 문제도 있었습니다. 이런 문제는 서버가 완전히 죽지 않아도 사용자에게는 충분히 큰 실패로 보였습니다.
결국 필요한 것은 skeleton UI, fallback image, retry button, offline banner, timeout 처리, error boundary였습니다. 중요한 것은 장애를 완벽히 막는 것이 아니라, 실패했을 때 사용자가 현재 상태를 이해하고 다음 행동을 할 수 있게 만드는 것이었습니다.
프론트엔드 장애 대응 UI 체크리스트
- API 응답이 3~5초 지연돼도 빈 화면이 나오지 않는가?
- 로딩 스피너가 중간에 사라지지 않는가?
- skeleton UI가 실제 콘텐츠 크기와 비슷하게 잡혀 있는가?
- 네트워크 오프라인 상태에서 사용자에게 안내가 보이는가?
- 저장이나 제출 실패 시 재시도 버튼이 있는가?
- 이미지 로딩 실패 시 fallback image가 표시되는가?
- 이미지가 실패해도 카드 높이와 버튼 위치가 유지되는가?
- 500 에러가 개발자용 문구가 아니라 사용자용 메시지로 표시되는가?
- localStorage 값이 깨져도 기본값으로 복구되는가?
- JSON 파싱 실패가 전체 화면 렌더링 중단으로 이어지지 않는가?
- error boundary가 주요 화면 단위로 적용되어 있는가?
- 장애 상황을 Playwright나 MSW로 반복 테스트할 수 있는가?
- 새 컴포넌트에도 기존 장애 대응 UI 기준을 적용했는가?
- 사용자가 실패 상황에서 무엇을 해야 하는지 화면에 드러나는가?
이번 실험을 하기 전에는 장애 대응을 서버와 인프라의 문제로만 생각했습니다. 하지만 프론트엔드도 실패를 다루는 방식이 필요했습니다. API가 늦고, 네트워크가 끊기고, 이미지가 실패하고, 저장소 값이 깨져도 화면은 사용자를 버려두면 안 됩니다. 실패 상황에서 빈 화면 대신 설명과 선택지를 보여주는 것, 그것이 이번 실험에서 얻은 가장 큰 기준이었습니다.