
앱이 자꾸 흰 화면으로 죽어요 (Error Boundary의 구원)
컴포넌트 하나에서 에러가 났는데 전체 페이지가 흰색이 됩니다. `react-error-boundary`로 에러를 격리하고, 우아한 폴백 UI를 보여주는 방법.

컴포넌트 하나에서 에러가 났는데 전체 페이지가 흰색이 됩니다. `react-error-boundary`로 에러를 격리하고, 우아한 폴백 UI를 보여주는 방법.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

습관적으로 모든 변수에 `useMemo`를 감싸고 있나요? 그게 오히려 성능을 망치고 있습니다. 메모이제이션 비용과 올바른 최적화 타이밍.

React나 Vue 프로젝트를 빌드해서 배포했는데, 홈 화면은 잘 나오지만 새로고침만 하면 'Page Not Found'가 뜨나요? CSR의 원리와 서버 설정(Nginx, Apache, S3)을 통해 이를 해결하는 완벽 가이드.

진짜 집을 부수고 다시 짓는 건 비쌉니다. 설계도(가상돔)에서 미리 그려보고 바뀐 부분만 공사하는 '똑똑한 리모델링'의 기술.

쇼핑몰 사이트를 만들었습니다. 오픈 첫날, 두근거리는 마음으로 로그를 보고 있는데 CS가 폭주하기 시작했습니다. "결제 버튼만 누르면 화면이 하얗게 변해요." "새로고침 해도 계속 흰 화면만 나와요."
확인해 보니 '장바구니 담기' 기능에 사소한 오타가 있었습니다.
Uncaught Error: Cannot read properties of undefined (reading 'push')
그런데 문제는, 이 작은 에러 하나 때문에 헤더, 푸터, 상품 목록, 심지어 내비게이션 바까지 모든 UI가 사라지고 하얀 화면(White Screen of Death)만 덩그러니 남았다는 겁니다. 사용자들은 "사이트가 망했나 봐"라며 이탈했습니다. 고작 버튼 하나 고장 났다고 집 전체가 무너지다니, 너무 억울했습니다.
저는 "에러 난 부분만 안 보이면 되는 거 아냐?"라고 순진하게 생각했습니다.
과거 jQuery나 순수 HTML 시절을 생각해 보면, 이미지 태그 하나가 깨진다고(<img> 엑박) 해서 본문 텍스트가 사라지진 않았습니다. 자바스크립트 에러가 나도 콘솔에 빨간 줄만 뜨고 나머지 버튼은 잘 동작했죠.
하지만 React는 JavaScript 기반의 단일 트리 구조입니다.
렌더링 도중(Render Phase)에 트리 어딘가에서 에러가 발생하면, React는 "잘못된 UI를 보여주느니 아예 안 보여주는 게 낫다"는 철학을 가지고 있습니다. 데이터가 꼬인 상태로 UI를 남겨두면, 사용자가 엉뚱한 버튼을 눌러 더 큰 데이터 손실(예: 잘못된 결제)을 유발할 수 있기 때문입니다.
그래서 React는 에러가 발생한 컴포넌트부터 최상위(Root)까지 거슬러 올라가며 전체 앱을 언마운트(Unmount) 시켜버립니다. 이게 바로 '흰 화면'의 정체였습니다.
이걸 "두꺼비집(Circuit Breaker)"에 비유하니 완벽하게 이해가 됐습니다.
우리는 에러가 났을 때 "흰 화면" 대신, "잠시 오류가 발생했습니다. 새로고침 해주세요"라는 안내판(Fallback UI)을 보여주는 안전장치가 필요합니다. 이것이 바로 Error Boundary입니다.
React 공식 문서에는 componentDidCatch 생명주기 메서드를 사용하여 클래스형 컴포넌트로 직접 Error Boundary를 구현하는 예제가 나옵니다. 하지만 2025년에 클래스 컴포넌트를 쓰고 싶진 않았습니다.
그래서 가장 널리 쓰이는 표준 라이브러리인 react-error-boundary를 도입했습니다.
에러가 터질 것 같은 위험한 컴포넌트, 특히 외부 API를 호출하거나 복잡한 로직이 있는 부분을 감쌉니다. 저는 전체 페이지가 죽는 걸 막기 위해, 주요 섹션별로 격리했습니다.
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<Layout>
{/* 1. 사이드바는 죽어도 본문은 살린다 */}
<ErrorBoundary FallbackComponent={SidebarFallback}>
<Sidebar />
</ErrorBoundary>
{/* 2. 본문이 죽어도 헤더는 살린다 */}
<ErrorBoundary FallbackComponent={MainFallback}>
<ProductList />
</ErrorBoundary>
</Layout>
);
}
이제 ProductList에서 에러가 발생해도, Sidebar와 Layout은 그대로 살아있습니다. 사용자는 내비게이션을 통해 다른 페이지로 이동할 수 있습니다. 이것만으로도 사용자 경험(UX)은 천지개벽합니다.
그냥 "에러남"이라고 띄우는 건 개발자한테나 편한 겁니다. 사용자에게는 "무엇이 잘못됐고, 어떻게 해야 하는지"를 알려줘야 합니다.
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-container">
<h3>앗! 상품을 불러오는 중 문제가 생겼어요. 😵</h3>
<p>잠시 후 다시 시도해 주시겠어요?</p>
{/* 개발 환경에서만 구체적인 에러 보여주기 */}
{process.env.NODE_ENV === 'development' && (
<pre>{error.message}</pre>
)}
{/* 가장 중요한 '다시 시도' 버튼 */}
<button onClick={resetErrorBoundary}>
다시 시도
</button>
</div>
);
}
여기서 resetErrorBoundary가 핵심입니다. 이 함수가 호출되면, Error Boundary는 에러 상태를 초기화하고 자식 컴포넌트(ProductList)를 다시 렌더링(Re-mount) 하려고 시도합니다. 일시적인 네트워크 오류였다면, 이 버튼 한 번으로 정상 복구될 수 있습니다.
만약 React Query를 쓰고 있다면, 단순히 재렌더링만 해서는 안 됩니다. 캐시 된 에러 데이터를 비워줘야 다시 fetch를 시도하기 때문입니다.
react-error-boundary와 tanstack-query는 찰떡궁합입니다.
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset} // 여기서 쿼리 에러를 초기화!
FallbackComponent={ErrorFallback}
>
<ProductList />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
이제 "다시 시도" 버튼을 누르면 ① 쿼리 에러 캐시가 초기화되고, ② 컴포넌트가 다시 마운트되면서, ③ API 요청을 다시 보냅니다. 완벽한 재시도 메커니즘입니다.
Error Boundary를 적용하고 신나게 테스트를 하는데, 이상한 점을 발견했습니다.
useEffect 안에서 API를 호출하다가 에러가 났는데, Error Boundary가 잡지 못하고 흰 화면이 뜨는 겁니다.
// ❌ Error Boundary가 못 잡음
useEffect(() => {
fetch('/api/data').then(res => {
if (!res.ok) throw new Error('API Fail'); // 여기서 에러 발생!
});
}, []);
React Error Boundary는 렌더링 도중(Render Phase), 생명주기 메서드, 자식 컴포넌트 생성자에서 발생한 에러만 포착합니다.
비동기 콜백(setTimeout, Promise), 이벤트 핸들러(onClick), 서버 사이드 렌더링(SSR) 도중의 에러는 포착하지 못합니다. 이것들은 React의 렌더링 흐름 밖에서 일어나기 때문입니다.
react-error-boundary가 제공하는 훅을 사용해 에러를 렌더링 사이클 안으로 던져버릴 수 있습니다.
import { useErrorBoundary } from 'react-error-boundary';
function MyComponent() {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
fetch('/api/data').catch((error) => {
// 에러를 Error Boundary로 위임!
showBoundary(error);
});
}, []);
// ...
}
가장 우아한 방법은 React Query의 suspense: true 옵션을 사용하는 것입니다. (React Query v5부터는 useSuspenseQuery).
이 방식을 쓰면 데이터 로딩 중엔 Suspense가 동작하고, 에러 발생 시엔 데이터 fetching 에러가 렌더링 에러로 전파되어 ErrorBoundary가 자연스럽게 잡습니다.
{/* 3. 에러 발생 시 처리 */}
<ErrorBoundary fallback={<div>에러가 발생했습니다</div>}>
{/* 2. 로딩 중 처리 */}
<Suspense fallback={<Skeleton />}>
{/* 1. 성공 시 데이터 렌더링 */}
<AsyncProductList />
</Suspense>
</ErrorBoundary>
이 코드를 보세요. if (isLoading) return ..., if (isError) return ... 같은 지저분한 분기문이 하나도 없습니다.
오직 성공했을 때의 UI만 작성하면 됩니다. 로딩과 에러는 부모(Suspense, ErrorBoundary)가 책임지는 구조입니다. 이것이 바로 선언형 프로그래밍(Declarative Programming)의 정수입니다.
Error Boundary는 에러를 잡아내서 UI를 보여주는 역할도 하지만, 로그를 수집하는 최적의 장소이기도 합니다. 사용자가 에러 화면을 보고 "안 돼요"라고 문의하기 전에, 개발자가 먼저 알 수 있어야 합니다.
onError 프로퍼티를 사용하면 에러가 포착될 때마다 특정 로직을 실행할 수 있습니다.
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
console.error("에러 포착:", error);
// Sentry, LogRocket 등으로 전송
Sentry.captureException(error, {
extra: {
componentStack: info.componentStack,
userAction: "장바구니 클릭", // 컨텍스트 추가 가능
}
});
}}
>
<App />
</ErrorBoundary>
이렇게 하면 "어떤 컴포넌트에서", "어떤 스택 트레이스로" 에러가 났는지 정확하게 기록됩니다. 슬랙으로 알림이 오게 해 두면, 자고 있다가도 벌떡 일어나서 고칠 수 있습니다. (좋은 건가요...?)
ErrorBoundary로 에러를 구획별로 격리하고, Suspense와 조합해 선언적으로 처리해라. 사용자에게 '흰 화면' 대신 '다시 시도' 버튼을 쥐여주는 것이 개발자의 마지막 양심이다.