실제 QA 과정에서 만난 흰 화면 사례를 바탕으로 썼다. 특정 조직과 서비스, 내부 주소는 제거하고 재사용할 수 있는 구조만 남겼다.
기능을 켜고 끄기 위해 사용한 Feature Flag가 오히려 화면 전체를 멈추게 했다.
로그인 화면은 원격 설정에서 인증 정책을 읽은 뒤 렌더링하도록 만들어져 있었다. 평소에는 문제가 없었다. 설정을 빠르게 받아오면 사용자는 중간 과정을 거의 느끼지 못한다. 그런데 원격 설정 서버가 불안정해진 날, 로그인 페이지에는 아무것도 나오지 않았다. 오류 안내도, 다시 시도할 버튼도 없는 흰 화면이었다.
문제를 줄이면 다음과 비슷한 코드였다.
const isPolicyLoading = useAuthenticationPolicyLoading();
if (isPolicyLoading) {
return null;
}
return <LoginPage />;
return null 자체가 잘못은 아니다. 잠깐의 로딩 상태를 숨기는 데 사용할 수도 있다. 문제는 로딩이 끝난다는 전제가 원격 시스템의 정상 동작에 기대고 있었다는 것이다.
TL;DR
- Feature Flag와 원격 설정은 핵심 화면의 단일 장애점이 될 수 있다.
- 로딩 중, 조회 실패, 값 없음, 비활성 상태를 같은 상태로 취급하면 안 된다.
- 보안 정책은 무조건 기본값으로 우회할 수 없다. 동작은 차단하더라도 화면과 복구 수단은 보여줘야 한다.
return null은 짧고 확실하게 끝나는 상태에서만 안전하다.- 흰 화면은 예외가 발생하지 않을 수도 있으므로 별도의 후보 감지와 알림이 필요하다.
기능의 실패와 화면의 실패가 묶여 있었다
Feature Flag는 보통 기능 노출을 제어한다.
원격 설정 정상
-> 정책 값 확인
-> 기능 활성화 또는 비활성화
-> 화면 렌더링
이 흐름만 생각하면 정책을 받기 전까지 화면을 감추는 코드가 자연스럽게 보인다. 그러나 네트워크와 원격 서비스에는 실패 경로가 있다.
원격 설정 요청
-> 타임아웃 또는 서버 장애
-> 로딩 상태가 오래 유지됨
-> 컴포넌트가 계속 null 반환
-> 사용자는 흰 화면을 봄
원래의 요구사항은 “정책을 확인한 뒤 특정 기능을 결정한다”였다. 구현 결과는 “정책 서버가 응답해야 로그인 화면을 보여준다”가 됐다. 보조적인 제어 시스템이 핵심 사용자 동선의 선행 조건으로 승격된 것이다.
이런 의존은 코드에서 잘 드러나지 않는다. 훅 이름과 return null만 보면 단순한 로딩 처리처럼 보인다. 실제 장애 구조를 보려면 다음 질문이 필요하다.
이 로딩 상태를 끝내는 주체는 누구이며, 그 주체가 실패하면 사용자는 무엇을 보는가?
네 가지 상태를 분리해야 한다
원격 정책을 사용하는 화면에는 적어도 네 가지 상태가 있다.
| 상태 | 의미 | 화면의 선택 |
|---|---|---|
| loading | 아직 응답을 기다리는 중 | 짧은 스켈레톤 또는 로딩 표시 |
| success | 정책을 정상적으로 받음 | 정책에 맞는 화면 렌더링 |
| empty | 응답은 성공했지만 값이 없음 | 명시한 기본 정책 또는 설정 오류 처리 |
| error | 타임아웃, 네트워크, 서버 오류 | 실패 화면, 재시도, 제한된 대체 동작 |
loading과 error를 구분하지 않으면 영원히 기다리는 화면이 생긴다. empty와 disabled를 구분하지 않으면 설정 누락을 정상적인 기능 비활성으로 오해한다.
상태를 분리하면 코드는 조금 길어진다. 대신 실패했을 때 무엇을 할지가 코드에 드러난다.
const policyQuery = useAuthenticationPolicy();
if (policyQuery.isLoading) {
return <LoginPageSkeleton />;
}
if (policyQuery.isError) {
return (
<AuthenticationPolicyError
onRetry={policyQuery.refetch}
/>
);
}
return <LoginPage policy={policyQuery.data} />;
여기서 중요한 건 스켈레톤의 모양이 아니다. 실패를 로딩과 다른 상태로 다루고, 사용자가 복구할 수 있는 표면을 남긴 것이다.
보안 정책은 단순히 fail-open 할 수 없다
가용성만 생각하면 원격 설정에 실패했을 때 기본값을 사용하면 된다.
const policy = remotePolicy ?? DEFAULT_POLICY;
하지만 인증, 결제, 권한 같은 정책은 무조건 기본값으로 우회하면 위험하다. 예를 들어 추가 인증 여부를 결정하지 못했다고 해서 인증 단계를 생략할 수는 없다.
이때 필요한 것은 화면과 정책 판단의 분리다.
- 정책 판단: 안전한 값을 확인할 수 없으면 민감한 동작을 제한한다.
- 화면 렌더링: 제한 이유, 재시도 방법, 문의 또는 이전 경로를 보여준다.
즉, 보안상 fail-closed를 선택하더라도 사용자 경험까지 닫을 필요는 없다.
정책 확인 실패
-> 민감한 동작은 실행하지 않음
-> 오류 상태와 재시도 버튼은 렌더링
-> 관측 시스템에 실패 원인과 경로 기록
마지막 정상값을 사용하는 전략도 가능하지만 조건이 필요하다.
- 값의 유효 기간이 정의돼 있는가
- 정책 변경이 즉시 반영돼야 하는 보안 요구가 있는가
- 마지막 정상값이 없을 때의 동작이 정해져 있는가
- 캐시된 값의 출처와 시각을 관측할 수 있는가
기본값, 마지막 정상값, 동작 차단 중 무엇이 맞는지는 도메인마다 다르다. 중요한 것은 원격 요청 실패를 우연한 null 반환에 맡기지 않고 명시적인 실패 정책으로 정하는 것이다.
흰 화면은 예외 없이도 만들어진다
프론트엔드 오류 알림은 대개 던져진 예외를 잡는다. 그러나 return null은 정상적인 React 동작이다. 콘솔 오류가 없고 Sentry에도 예외가 남지 않을 수 있다. 사용자에게는 장애지만 시스템에는 성공한 렌더링처럼 보인다.
그래서 핵심 페이지에는 흰 화면 후보를 별도로 관측할 필요가 있다.
const snapshot = {
bodyContentLength: document.body?.innerText?.trim().length ?? 0,
elementCount: document.querySelectorAll('*').length,
hasReactRoot: Boolean(document.querySelector('#__next')),
interactiveElementCount: document.querySelectorAll(
'a, button, input, select, textarea'
).length,
pathname: window.location.pathname,
timestamp: new Date().toISOString(),
};
이 값만으로 흰 화면을 확정하면 오탐이 많다. 로딩 직후, 리다이렉트 직전, 의도적으로 비어 있는 페이지도 있기 때문이다. 따라서 다음 조건을 함께 사용하는 편이 낫다.
- 라우트 진입 후 일정 시간이 지났는가
- React 루트는 존재하지만 본문과 상호작용 요소가 거의 없는가
- 로딩 표시도 존재하지 않는가
- 같은 사용자 세션에서 상태가 연속으로 관측되는가
- 네트워크 또는 Feature Flag 조회 실패가 함께 기록됐는가
조건에 맞으면 즉시 장애로 단정하기보다 blank-page-candidate 같은 태그로 수집한다. 발생량과 경로를 본 뒤 알림 임계치를 조정할 수 있다.
배포 전에도 같은 실패를 만들 수 있다
이 문제는 운영 알림만으로 막을 필요가 없다. 배포 전 스모크 테스트에서 원격 정책 실패를 의도적으로 주입할 수 있다.
1. 핵심 페이지 진입
2. Feature Flag 요청을 타임아웃 또는 500으로 응답
3. 본문 또는 오류 안내가 렌더링되는지 확인
4. 재시도 버튼이 동작하는지 확인
5. 민감한 기능이 안전한 상태로 제한되는지 확인
정상 응답만 확인하는 E2E는 이 종류의 장애를 잡지 못한다. 의존 서비스가 실패해도 최소한의 화면과 복구 경로가 남는지를 함께 확인해야 한다.
배포 후 카나리 검증도 페이지의 HTTP 200만 봐서는 부족하다. HTML과 JavaScript가 정상적으로 내려왔어도 React가 빈 화면을 렌더링할 수 있기 때문이다. 핵심 경로에서는 다음 조건이 더 직접적이다.
- 의미 있는 본문이 보이는가
- 주요 CTA가 존재하고 동작하는가
- 오류 경계 또는 대체 화면이 정상적으로 보이는가
- 흰 화면 후보 이벤트가 새 버전에서 증가하지 않았는가
return null을 볼 때 물어볼 것
return null을 없애자는 이야기는 아니다. 다음 조건을 만족한다면 충분히 적절할 수 있다.
- 상태가 짧은 시간 안에 확실히 종료된다.
- 종료 조건이 외부 시스템 하나에만 의존하지 않는다.
- 빈 화면이어도 사용자 동선이 막히지 않는다.
- 실패 시 별도의 화면이나 상위 오류 경계가 처리한다.
반대로 핵심 페이지에서 원격 데이터 로딩을 이유로 null을 반환한다면 다음을 확인해야 한다.
- 요청이 실패했을 때
isLoading은 끝나는가 - 타임아웃이 있는가
- 실패와 값 없음을 구분하는가
- 사용자가 다시 시도하거나 빠져나갈 수 있는가
- 예외가 없어도 빈 화면을 감지할 수 있는가
- 해당 실패 경로를 E2E에서 재현하는가
마치며
Feature Flag의 목적은 기능의 위험을 분리하는 것이다. 그런데 원격 설정을 받기 전까지 핵심 화면 전체를 감추면, 기능의 위험을 분리하는 도구가 서비스 가용성까지 쥐게 된다.
어제의 흰 화면은 거대한 렌더링 오류가 아니었다. 정상적인 return null과 실패가 끝날 것이라는 낙관적인 전제가 만난 결과였다.
교훈은 단순하다.
원격 정책을 기다릴 수는 있다. 하지만 실패했을 때 무엇을 보여줄지는 미리 정해야 한다.
기능을 안전하게 끄는 것과 화면을 계속 사용할 수 있게 만드는 것은 서로 다른 문제다. Feature Flag를 붙일 때는 정상적인 ON/OFF뿐 아니라 로딩, 실패, 값 없음과 마지막 정상값까지 함께 설계해야 한다.