에러가 나도 화면은 죽지 않게
Article
이 블로그의 콘텐츠는 외부 GitHub 저장소의 마크다운을 API로 가져온다. 그러다 며칠 전, GitHub API가 잠깐 응답하지 않는 순간에 페이지를 새로고침했다. 화면이 통째로 하얗게 떴다. 콘텐츠 하나 못 가져온 것 때문에 페이지 전체가 죽은 것이다.
이건 기술 문제이기 이전에 UX 문제다. 방문자 입장에선 내가 뭘 잘못한 것도 아닌데 사이트가 부서진 것처럼 보인다. 그래서 에러를 대하는 방식을 바꿨다.
throw 대신 빈 배열
가장 먼저 고친 건 fetch 함수다. 실패하면 예외를 던지는 대신, 비어 있지만 유효한 값을 반환하게 했다.
export async function getPosts(): Promise<Post[]> {
try {
const res = await fetch(CONTENT_API_URL, {
next: { revalidate: 3600 },
});
if (!res.ok) {
// 401, 403, 5xx 등 — 던지지 않고 빈 배열
console.error(`콘텐츠 fetch 실패: ${res.status}`);
return [];
}
return await res.json();
} catch (err) {
// 네트워크 자체가 끊긴 경우
console.error("콘텐츠 fetch 예외:", err);
return [];
}
}
핵심은 호출하는 쪽이 항상 배열을 받는다는 보장이다. getPosts()의 결과는 성공이든 실패든 언제나 Post[]다. 그러면 페이지 컴포넌트는 에러 분기를 신경 쓸 필요 없이 목록을 그리는 코드 하나만 유지하면 된다.
export default async function DevPage() {
const posts = await getPosts();
// posts가 비어 있어도 이 컴포넌트는 절대 터지지 않는다
return <DevGrid posts={posts} />;
}
에러가 페이지 밖으로 새어 나가지 않으니, 사이트 전체가 하얗게 뜨는 일이 사라진다. 최악의 경우에도 "콘텐츠가 없는 정상 페이지" 상태로 떨어질 뿐이다.
빈 상태도 디자인한다
여기서 한 걸음 더. posts가 빈 배열일 때 아무것도 안 그리면, 그건 그것대로 고장 난 것처럼 보인다. 그래서 empty state를 하나의 화면으로 디자인했다.
export default function DevGrid({ posts }: { posts: Post[] }) {
if (posts.length === 0) {
return (
<section className={css.empty}>
<p className={css.empty_msg}>
지금은 글을 불러오지 못했다.
</p>
<p className={css.empty_hint}>
잠시 후 새로고침하면 대개 정상으로 돌아온다.
</p>
</section>
);
}
return (
<ul className={css.grid}>
{posts.map((post) => (
<li key={post.slug}>{/* 카드 */}</li>
))}
</ul>
);
}
중요한 건 문구다. 예전 같으면 Error 500 같은 걸 띄웠을 텐데, 그건 개발자한테나 정보지 방문자한테는 아무 의미가 없다. 방문자가 알고 싶은 건 에러 코드가 아니다. "그래서 지금 나는 뭘 하면 되는데?" 그거 하나다.
그래서 문구를 두 줄로 나눴다. 첫 줄은 상황("불러오지 못했다"), 둘째 줄은 다음 행동("새로고침하면 대개 돌아온다"). 사용자에게 통제감을 돌려주는 게 목적이다. 손 쓸 방법이 있다는 것만 알아도 부서진 사이트의 느낌은 사라진다.
에러 상태는 예외가 아니라 일부다
이 작업을 하면서 생각이 바뀌었다. 나는 오래 에러를 **"일어나면 안 되는 예외"**로 취급했다. 그래서 성공 화면만 정성껏 디자인하고 실패 화면은 뒷전이었다. 하지만 외부 API에 의존하는 순간, 실패는 예외가 아니라 언젠가 반드시 오는 정상 시나리오다.
그러면 에러 상태도 로딩 상태, 성공 상태와 나란히 디자인 대상이 된다. 데이터가 있을 때 어떻게 보일지 그리는 만큼, 데이터가 없을 때 어떻게 보일지도 그려야 한다.
정리하면 세 가지다. 첫째, 데이터 계층에서 에러를 잡아 안전한 기본값을 반환한다. 둘째, 빈 상태를 하나의 화면으로 디자인한다. 셋째, 문구는 코드가 아니라 다음 행동을 준다. 화면이 죽지 않는 것만으로도 사용자의 신뢰는 꽤 지켜진다.