원문: Fault Tolerance
현대적인 웹 애플리케이션을 구축하는 일은 많은 구성 요소가 맞물려 있는 복잡한 과정입니다. 때로는 이러한 구성 요소들이 멈추면서 문제가 발생하기 시작합니다.
우리는 이러한 상황을 예방하기 위해 최선을 다하지만, 현실적으로 애플리케이션을 완전히 에러 없이 유지하는 것은 불가능합니다. 즉 예기치 않은 방식으로 문제가 발생할 수 있음을 항상 염두에 두고, 그러한 상황을 우아하게 처리할 수 있어야 합니다.
다시 말해, 장애허용성이 필요합니다.
- 장애허용성이란 시스템을 구성하는 일부 구성 요소에서 장애가 발생하더라도(하나 이상의 결함이 존재하더라도) 시스템이 정상적으로 동작을 계속할 수 있도록 해주는 특성입니다.
제 경험상 웹 애플리케이션에서 장애허용성은 종종 간과되거나 과소평가됩니다. 수백 개의 테스트를 통해 문제가 발생하지 않을 것이라는 어느 정도의 확신은 가질 수 있지만, 불가피한 실패가 실제로 발생했을 때 어떤 일이 일어나는지에 대해서는 충분히 고민하지 않는 경우가 많습니다. 특히 고가용성이 중요한 상황에서는 이 점이 더욱 중요합니다. (그리고 대부분의 시스템에서는 그렇습니다.)
그렇다면 리액트 애플리케이션이 장애허용성을 갖도록 하려면 어떻게 해야 할까요?
에러 바운더리
간단한 대답은 에러 바운더리입니다. 현재 이 API는 클래스 컴포넌트에서만 사용할 수 있으며, 대략 다음과 같은 형태입니다.
componentDidCatch(error) {
// 이 메서드가 호출되면 에러가 발생했음을 알 수 있습니다!
// 장애 감지에 성공하였습니다!
// 이제 에러가 발생했음을 사용자에게 알리기 위해 setState를 호출하여 폴백 UI를 렌더링할 수 있습니다.
// 사용자는 이제 에러가 발생했음을 알 수 있습니다.
this.setState({ error, showFallback: true });
}
리액트에서 에러 바운더리는 componentDidCatch 메서드를 가진 클래스 컴포넌트에 불과합니다. 시작하기에 좋은 출발점이 필요하다면 react-error-boundary를 참고하면 됩니다.
리액트 공식 문서는 에러 바운더리가 무엇인지, 그리고 이를 어떻게 사용하는지에 대해 매우 잘 설명하고 있으므로 여기서는 그 부분을 깊이 다루지 않겠습니다. 기본 개요를 원한다면 먼저 공식 문서를 읽고, 이후에 다시 돌아오면 됩니다.
적절한 경계선 그리기
애플리케이션에 에러 바운더리를 추가하는 일은 쉽습니다. 몇 줄의 코드만 있으면 됩니다. 까다로운 부분은 이를 어디에 배치할지 적절한 위치를 찾는 일입니다. 앞으로 살펴보겠지만, 보통은 골디락스 원칙을 따라 “딱 적당한 정도”의 에러 바운더리를 구현하는 것이 좋습니다. 하지만 “딱 적당한 정도”란 무엇일까요?
먼저 양 극단 두 가지를 살펴보면서 각각의 단점을 확인해 보겠습니다.
에러 바운더리가 충분하지 않은 경우
첫 번째 극단은 애플리케이션의 최상단에 에러 바운더리 하나만 두는 방식입니다.
// ⚠️ 이 예제에서는 react-error-boundary를 사용하겠습니다.
import ErrorBoundary from "react-error-boundary";
import App from "./App.js";
ReactDOM.render(
<ErrorBoundary>
<App />
</ErrorBoundary>,
document.getElementById("root")
);
이는 아마도 대부분의 사람이 하는 방식과 비슷할 것입니다. 서버 사이드 렌더링 애플리케이션이 실패할 때 일어나는 일과 본질적으로 동일합니다. 최악의 경험은 아니지만, 우리가 제공할 수 있는 최선의 경험은 아닙니다. 문제는 한 부분에서 문제가 발생하면 애플리케이션 전체가 함께 무너진다는 점입니다.
이 접근은 애플리케이션의 어느 부분에서든 실패가 발생하면 전체가 사용할 수 없게 되는 경우라면 타당합니다. 실제로 그런 경우도 분명히 존재하지만, 일반적인 경우는 아니라고 생각합니다.
장애허용성의 정의로 돌아가 보면 다음과 같습니다.
- 장애허용성이란 장애가 발생하더라도, 시스템이 정상적으로 동작을 이어갈 수 있게 해주는 특성입니다.
이를 보면 에러 바운더리 하나만 두는 방식은 사실 장애허용성을 제공하지도 못한다는 것을 알 수 있습니다. 실패가 한 번 발생하면 애플리케이션 전체가 함께 내려가기 때문입니다.
에러 바운더리가 너무 많은 경우
반대 극단으로, 모든 컴포넌트를 에러 바운더리로 감싸는 방법을 시도할 수도 있습니다. 이 접근의 문제는 더 미묘하므로, 왜 이것이 에러 바운더리 하나만 두는 방식보다 더 나쁠 수 있는지 이해하기 위해 더 구체적인 예제를 살펴보겠습니다.
사용자가 장바구니에 담긴 상품을 확인하고, 신용카드 정보를 입력하고, 결제를 완료할 수 있게 해주는 CheckoutForm 컴포넌트가 있다고 가정해 보겠습니다.
function CheckoutForm(props) {
return (
<form>
<CartDescription items={props.items} />
<CreditCardInput />
<CheckoutButton cartId={props.id} />
</form>
);
}
이제 여기 있는 모든 컴포넌트를 에러 바운더리로 감싸 보겠습니다.
// 모든 컴포넌트에 에러 바운더리를 추가합니다!
<form>
<ErrorBoundary>
<CartDescription items={props.items} />
</ErrorBoundary>
<ErrorBoundary>
<CreditCardInput />
</ErrorBoundary>
<ErrorBoundary>
<CheckoutButton cartId={props.id} />
</ErrorBoundary>
</form>
실제 사례에서는 각 컴포넌트를 이런 식으로 인라인으로 감싸기보다는, 보통 자신의 export를 에러 바운더리로 감싸는 방식으로 구현할 것입니다(예: react-error-boundary의 withErrorBoundary HOC). 그 부분은 그냥 무시하면 됩니다 🙂
처음에는 이것이 괜찮은 아이디어처럼 보일 수 있습니다. 에러 바운더리를 더 세밀하게 나눌수록 단일 실패가 애플리케이션 전체에 미치는 영향이 줄어들기 때문입니다. 이것은 장애허용성처럼 들립니다. 하지만 문제는 에러의 영향을 최소화하는 것이 장애허용성을 갖춘 것과 같지는 않다는 점입니다.
CreditCardInput 컴포넌트의 무언가가 고장 났다고 가정해 보겠습니다.
<form>
<ErrorBoundary>
<CartDescription items={props.items} />
</ErrorBoundary>
<ErrorBoundary>
{/* 앗! 여기서 무언가 고장 났습니다 😢 */}
<CreditCardInput />
</ErrorBoundary>
<ErrorBoundary>
<CheckoutButton cartId={props.id} />
</ErrorBoundary>
</form>
이것이 사용자 경험(UX)에 어떤 의미를 갖는지 자세히 풀어 보면, 사용자에게는 매우 고통스러운 상황이 될 수 있음을 알 수 있습니다.
반쯤 깨진 UI는 완전히 깨진 UX입니다
CreditCardInput이 자체 에러 바운더리를 가지고 있으므로 에러는 CheckoutForm의 나머지 부분으로 전파되지 않습니다. 하지만 CheckoutForm은 CreditCardInput 없이 사용할 수 없습니다 🤔. CheckoutButton과 CardDescription 컴포넌트는 여전히 마운트되어 있으므로 사용자는 여전히 상품을 확인하고 결제를 시도할 수 있습니다. 하지만 신용카드 정보 입력을 끝내지 못했다면 어떻게 될까요? CreditCardInput이 크래시 나기 전에 신용카드 정보를 입력했다면 그 상태는 유지되었을까요? 사용자가 결제를 시도하면 무슨 일이 일어날까요?
이 경우에 어떤 일이 일어날지 컴포넌트 작성자조차도 알기 어려울 것이라고 봅니다. 하물며 사용자는 더더욱 알 수 없습니다. 이는 사용자에게 혼란과 좌절을 안겨줄 가능성이 큽니다.
범용 에러 바운더리, 범용 폴백
이것이 얼마나 답답하고 혼란스러운 경험이 되는지는 폴백으로 무엇을 렌더링하느냐에 따라서도 달라집니다. 컴포넌트가 아무 경고 없이 화면에서 그냥 사라질까요? 이는 대부분의 사용자에게 매우 혼란스러운 경험이 될 것입니다.
그렇지 않다면 아마도 공통으로 쓰는 폴백 UI를 사용하고 있을 것입니다. 예를 들어 슬픈 얼굴과 함께 에러에 대한 유용한 정보를 보여주는 형태일 수 있습니다. 이는 아무것도 없는 것보다는 낫지만, 모든 컴포넌트를 이 에러 바운더리로 감싼다면 이 폴백은 가능한 모든 UI 요소에 대해 올바르게 렌더링되어야 합니다. 하지만 요소마다 필요한 레이아웃 요구사항이 다르기 때문에 이를 제대로 처리하는 것은 사실상 불가능에 가깝습니다. 헤더처럼 페이지 수준의 섹션에는 적절한 폴백이 작은 아이콘 버튼에는 부적절할 수 있으며, 그 반대도 마찬가지입니다.
이제 문제는 명확해졌을 것입니다. 모든 컴포넌트를 에러 바운더리로 감싸면 혼란스럽고 망가진 사용자 경험으로 이어질 수 있습니다. 이는 애플리케이션을 일관되지 않은 상태로 만들기 쉽고, 그 결과 사용자는 답답함과 혼란을 느끼게 됩니다. 흥미로운 점은 “손상된 상태(corrupted state)”를 피하는 것이 에러 바운더리가 존재하는 주요 이유 중 하나라는 사실입니다.
- 이 결정에 대해서는 많은 논의가 있었지만, 우리의 경험상 손상된 UI를 그대로 남겨두는 것보다 이를 완전히 제거하는 편이 더 낫습니다.
성능 패널티: 에러 바운더리에는 본질적인 오버헤드가 일부 존재하므로, 이를 과도하게 사용하면 성능에 부정적인 영향을 줄 수 있습니다. 다만 이는 에러 바운더리를 애플리케이션 전반에 걸쳐 모든 곳에 사용하는 경우에만 문제가 됩니다. 따라서 이 점 때문에 에러 바운더리 사용 자체를 꺼릴 필요는 없습니다.
적절한 수의 에러 바운더리
정리하자면, 에러 바운더리가 충분하지 않으면 에러가 필요 이상으로 애플리케이션의 더 큰 부분을 무너뜨리게 되고, 에러 바운더리가 너무 많으면 손상된 UI 상태로 이어질 수 있습니다. 그렇다면 에러 바운더리는 어느 정도가 적절할까요?
애플리케이션마다 다르기 때문에 “이 정도가 적절한 수이다”라고 단일한 숫자를 제시할 수는 없습니다. 제가 찾은 가장 좋은 접근은 애플리케이션에서 기능 경계를 식별하고, 그 경계에 에러 바운더리를 배치하는 것입니다.
기능 찾기
임의의 애플리케이션을 살펴보고 그 경계를 식별하는 데 사용할 수 있는 “기능”에 대한 보편적인 정의는 없습니다. 대부분의 경우 “보면 안다”라는 수준이 최선이지만, 가이드라인으로 삼을 수 있는 몇 가지 공통 패턴은 존재합니다.
대부분의 애플리케이션은 여러 개의 개별 섹션으로 구성되며, 이들이 서로 조합되어 하나의 애플리케이션을 이룹니다. 헤더, 내비게이션, 메인 콘텐츠, 사이드바, 푸터 등이 그 예입니다. 이들 각각은 사용자 경험 전체에 기여하지만, 동시에 일정 수준의 독립성도 유지합니다.
예로 트위터를 살펴보겠습니다.

페이지에는 서로 구분되는 섹션과 기능들이 즉시 드러납니다. 트윗의 메인 타임라인, 팔로워 추천 영역, 트렌드 섹션, 내비게이션 바가 그 예입니다. 이러한 섹션들의 레이아웃과 스타일링만 보더라도 섹션 간에 분리가 존재함을 알 수 있으며, 이는 매우 좋은 출발점입니다. 시각적으로 독립적인 섹션은 대개 기능적으로도 독립적인 경우가 많고, 바로 이런 지점이 에러 바운더리를 두기에 적절한 위치입니다.
이들 섹션 중 하나에서 컴포넌트가 에러를 던지더라도, 다른 섹션까지 함께 크래시되어서는 안 된다고 보는 것이 타당합니다. 예를 들어 팔로워 추천 섹션의 팔로우 버튼에서 에러가 발생하더라도, 메인 타임라인까지 함께 내려가서는 안 됩니다.
재귀적인 질문의 흐름
UI는 종종 재귀적인 구조를 가집니다. 페이지 수준에서는 사이드바나 타임라인 같은 큰 섹션이 있고, 그 안에는 다시 헤더나 리스트와 같은 하위 섹션이 있으며, 이들 또한 또 다른 섹션을 포함하는 식으로 이어집니다.
에러 바운더리를 배치할 적절한 위치를 식별할 때 스스로에게 던져볼 수 있는 좋은 질문은 “이 컴포넌트에서 발생한 에러가 형제 컴포넌트에 어떤 영향을 미쳐야 하는가?”입니다. CheckoutForm 예제에서도 바로 이 질문을 고려했습니다. CreditCardInput이 실패했다면, 그 실패는 CheckoutButton과 CardDescription에 어떤 영향을 미쳐야 할까요?
이와 같은 질문을 컴포넌트 트리에 대해 재귀적으로 적용하면, 기능 경계가 어디에 있는지를 빠르게 식별할 수 있고, 그 경계를 기준으로 에러 바운더리를 배치할 수 있습니다.
트위터 심층 분석: 페이지
다시 트위터를 예로 들어 이 방식이 어떻게 동작하는지 살펴보겠습니다. 먼저 페이지 최상단에서 시작한 뒤, 팔로워 추천 섹션으로 더 깊이 들어가 보겠습니다.
이 분석에는 해당 기능들이 어떻게 동작하는지에 대한 제 인식과 기대를 바탕으로 한 의견과 편향이 적지 않게 포함되어 있습니다. 이는 “정답”을 제시하기 위한 것이 아니라, 단지 사고 과정 자체를 살펴보는 것이 목적입니다.

최상단에서 시작하면 세 가지 주요 콘텐츠 섹션을 식별할 수 있습니다. Home, Trends for you, 그리고 Who to follow입니다. 이제 Who to follow 섹션을 더 자세히 살펴보겠습니다.
먼저 다음과 같은 질문을 던집니다.
- 이 컴포넌트에서 발생한 에러는 형제 컴포넌트에 어떤 영향을 미쳐야 할까?
이 질문은 다음과 같이 조금 더 구체적으로 바꿔볼 수 있습니다.
- 이 컴포넌트가 크래시된다면, 형제 컴포넌트도 함께 크래시되어야 할까?
따라서 Who to follow 섹션을 고려할 때 우리는 이렇게 질문하게 됩니다. Who to follow 섹션이 크래시되었을 때 Home과 Trends 섹션도 함께 크래시되어야 할까요? 이는 분명 그렇지 않은 경우라고 생각합니다. 다른 섹션들은 서로 의존하는 것처럼 보이지 않으므로, 이 지점은 에러 바운더리를 배치하기에 매우 적절한 위치입니다.
트위터 심층 분석: Who to follow
이제 동일한 질문의 흐름을 Who to follow 섹션에도 그대로 적용합니다.

Who to follow에 초점을 맞추면 세 가지 분명한 섹션을 확인할 수 있습니다. 제목, 팔로우할 사용자 목록, 그리고 “show more” 버튼입니다. 사용자 목록을 더 자세히 살펴보면서 다시 동일한 질문을 던집니다. 팔로워 목록이 크래시된다면 제목과 “show more” 버튼도 함께 크래시되어야 할까요? 이 경우에는 다소 애매할 수 있지만, 아마도 그렇지 않은 편이 더 타당하다고 생각합니다. 제목을 그대로 유지한다고 해서 큰 문제가 생기지는 않으며, “Show more” 버튼은 다른 페이지로 연결되는데 그 페이지 자체는 정상적으로 동작하고 있을 수도 있습니다. 따라서 여기서도 답은 다시 한 번 ‘예’이며, 또 하나의 에러 바운더리를 추가하는 것이 적절합니다.
트위터 심층 분석: 팔로워 추천
이번에는 팔로워 추천 영역을 대상으로 한 번 더 같은 과정을 적용해 보겠습니다.

여기에는 두 개의 섹션만 있으므로 다음과 같은 질문만 던지면 됩니다. 사용자의 이름과 핸들이 크래시된다면 Follow 버튼도 함께 크래시되어야 할까요? 반대로 Follow 버튼이 크래시된다면 사용자의 이름과 핸들도 함께 크래시되어야 할까요?
이 경우에는 그 답이 ‘예’라고 느껴집니다. 사용자의 이름과 핸들이 사라진다면 우리가 누구를 팔로우하고 있는지 알 수 없게 됩니다. 반대로 Follow 버튼이 사라진다면, 아무런 행동도 할 수 없는 추천을 받게 되어 사용자에게 답답한 경험이 될 수 있습니다.
장애허용성 테스트하기
이제 에러 바운더리와 장애허용성을 사용하는 방법에 대해 조금 더 알게 되었으니, 이 전체 주제에서 제가 가장 좋아하는 부분 중 하나를 공유하고자 합니다. 바로 이를 어떻게 테스트할 수 있는가입니다. 제가 찾은 장애허용성을 테스트하는 가장 간단하고 수고가 적은 방법은, 애플리케이션에 직접 들어가서 의도적으로 무언가를 망가뜨려 보는 것입니다.
function CreditCardInput(props) {
// 여기서 제가 실수를 했다면 무슨 일이 일어날까요? 직접 확인해 봅시다.
throw new Error("oops, I made a mistake!");
return <input className="credit-card" />;
}
이것은 제가 새로운 컴포넌트를 추가할 때마다 하기 시작한 방법이며, 애플리케이션이 실패를 어떻게 처리하는지 확인하는 데 매우 도움이 되었습니다. 다만 이러한 throw 구문을 커밋하지 않도록 주의해야 합니다 🙂
장애허용성을 테스트하기 위해 의도적으로 에러를 던지는 것은 카오스 엔지니어링의 아주 기본적인 예시입니다. 임의로 일부 컴포넌트를 랜덤하게 고장 내면서 장애허용성을 테스트할 수 있도록 해주는 React.ChaosMode 같은 것이 있다면 어떨까요?
마무리
정리하자면, 이 글에서 말하고자 한 핵심은 다음과 같습니다.
- 애플리케이션 최상단에 에러 바운더리 하나만 두는 방식은 피해야 합니다. 실패를 처리하는 최선의 방법인 경우는 드뭅니다.
- 에러 바운더리를 과도하게 사용하는 것도 피해야 합니다. 이는 사용자 경험을 저해하고, 잠재적으로 성능에 악영향을 줄 수 있습니다.
- 애플리케이션의 기능 경계를 식별하고, 그 경계에 에러 바운더리를 배치해야 합니다. 리액트 애플리케이션은 트리 구조로 이루어져 있으므로, 최상단에서 시작해 아래로 내려가며 살펴보는 방식이 효과적입니다.
- “이 컴포넌트가 크래시된다면, 형제 컴포넌트도 함께 크래시되어야 할까?”라는 질문을 재귀적으로 던져 보아야 합니다. 이는 기능 경계를 찾는 데 유용한 휴리스틱입니다.
- 에러 상태를 전제로 애플리케이션을 의도적으로 설계해야 합니다. 기능 경계에 에러 바운더리를 배치하면 보기 좋은 커스텀 폴백 UI를 만들기 쉬워지고, 사용자에게 무언가 잘못되었음을 명확하게 전달할 수 있습니다. 나아가 페이지 전체를 새로 고치지 않고도 특정 섹션만 다시 시도할 수 있도록 기능별 재시도 로직을 구현할 수도 있습니다.
- 의도적으로 무언가를 망가뜨려 보고, 어떤 일이 일어나는지 직접 확인해 보아야 합니다.
'개발 > 번역' 카테고리의 다른 글
| [번역] 모든 것을 배열로 바꾸는 것을 그만두세요 (대신 일을 덜 하세요) (1) | 2026.01.24 |
|---|---|
| [번역] HTTP 범위 요청(Range Requests)을 통한 동영상 제공하기 (1) | 2025.12.31 |
| [번역] 왜 타입스크립트는 당신을 구해주지 못하는가 (1) | 2025.12.03 |
| [번역] 상태 기반 렌더링 vs 시그널 기반 렌더링 (2) | 2025.11.06 |
| [번역] CSS 길이 단위 이해하기 (1) | 2025.09.02 |