38장. 로딩, 실패, 빈 화면 처리하기
![[7부] 로딩, 실패, 빈 화면 처리하기](https://images.tilnote.io/pages/b611c31f-8848-47dc-835d-f4fd11f29570.webp)
![[7부] 로딩, 실패, 빈 화면 처리하기](https://images.tilnote.io/pages/2731f425-b969-4301-bc23-2877e605c6a9.webp)
![[7부] 로딩, 실패, 빈 화면 처리하기](https://images.tilnote.io/pages/0caca603-28a1-4f6a-b41c-f1fd3116c069.webp)
잘 되는 경우만 만들면, 아직 서비스가 아니다
초보자가 기능을 처음 붙일 때 가장 먼저 확인하는 장면은 대개 이것이다. 버튼을 눌렀더니 저장된다. 목록 페이지에 들어갔더니 데이터가 보인다. 로그인했더니 내 이름이 뜬다. 이 순간은 분명 기쁘다. 드디어 "된다"는 감각을 얻기 때문이다.
하지만 실제 서비스는 "잘 되는 경우"만으로는 완성되지 않는다. 데이터가 조금 늦게 도착할 수도 있고, 서버가 잠깐 응답하지 않을 수도 있고, 아직 작성된 글이 하나도 없을 수도 있다. 이때 화면이 멈춘 것처럼 보이거나, 아무 말 없이 텅 비어 있거나, 사용자가 무엇을 해야 할지 알 수 없게 만들면 기능은 있어도 서비스답지는 않다.
그래서 잘 되는 경우만 만드는 사람과 서비스답게 만드는 사람의 차이는 예외 처리에서 난다. 예외 처리라고 하면 괜히 어려워 보이지만, 사실은 아주 인간적인 배려에 가깝다. "조금만 기다려 주세요", "문제가 생겼어요", "아직 데이터가 없어요"라고 화면이 친절하게 말해 주는 것이다.
이번 장에서는 이 세 가지 표정을 다룬다. 로딩, 실패, 빈 화면이다. 이 세 가지만 제대로 처리해도 여러분의 서비스는 갑자기 훨씬 믿음직해 보이기 시작한다.
사용자는 내부 사정을 모른다
개발자는 머릿속으로 현재 상황을 알고 있다. 지금은 서버에서 데이터를 가져오는 중인지, 요청이 실패했는지, 데이터베이스에 아무 데이터도 없는지 어느 정도 짐작할 수 있다. 하지만 사용자는 다르다. 사용자는 눈앞의 화면만 본다.
예를 들어 목록 페이지에 들어갔는데 2초 동안 아무것도 안 보인다고 해보자. 개발자는 "아, 아직 fetch가 끝나지 않았구나"라고 생각할 수 있다. 하지만 사용자는 이렇게 느낀다.
앱이 멈췄나?
인터넷이 끊겼나?
내가 잘못 눌렀나?
원래 아무것도 안 뜨는 페이지인가?
즉, 같은 빈 화면이라도 원인은 여러 가지일 수 있다. 문제는 사용자가 그 차이를 알 수 없다는 것이다. 그래서 서비스는 현재 상태를 화면으로 설명해야 한다. 로딩 중이면 로딩 중이라고 보여주고, 실패했으면 실패했다고 알려주고, 데이터가 없으면 없다고 설명해야 한다.
좋은 화면은 단순히 예쁜 화면이 아니다. 지금 무슨 일이 벌어지고 있는지 사용자가 이해할 수 있게 돕는 화면이다.
먼저 기억할 네 가지 상태
데이터를 다루는 화면은 보통 네 가지 상태 중 하나에 있다. 이 네 가지를 머릿속에 넣어 두면 복잡한 기능도 훨씬 단순하게 보인다.
아직 불러오는 중인 상태
불러오기는 끝났고 데이터가 있는 상태
불러오기는 끝났지만 데이터가 없는 상태
요청이 실패한 상태
이 네 가지는 게시글 목록, 상세 페이지, 검색 결과, 내 프로필, 알림 화면처럼 거의 모든 곳에 등장한다. 그래서 화면을 만들 때는 "정상 화면"만 디자인하지 말고, 이 네 가지를 함께 설계해야 한다.
특히 초보자는 성공 상태만 만든 뒤 나머지를 나중으로 미루는 경우가 많다. 하지만 실제로는 로딩, 실패, 빈 화면을 먼저 생각하면 코드도 더 깔끔해진다. 왜냐하면 화면이 어떤 상태를 가질지 미리 정리되기 때문이다.
로딩은 기다림을 견디게 만드는 장치다
로딩 상태는 말 그대로 작업이 아직 끝나지 않았다는 뜻이다. 서버에서 데이터를 가져오는 중일 수도 있고, 글을 저장하는 중일 수도 있고, 파일을 업로드하는 중일 수도 있다.
여기서 가장 중요한 원칙은 하나다. 아무 일도 안 일어나는 것처럼 보이게 두지 말 것.
사용자는 기다릴 수 있다. 하지만 이유 없는 침묵은 견디기 어렵다. 그래서 로딩 상태에서는 화면이 지금 바쁘게 일하고 있다는 신호를 줘야 한다.
목록 화면이라면 로딩 문구나 스켈레톤 화면을 보여줄 수 있다. 스켈레톤은 실제 카드나 리스트가 들어갈 자리에 회색 박스를 먼저 보여 주는 방식이다. 사용자는 "아, 여기에 내용이 곧 들어오겠구나"라고 이해한다. 반면 그냥 하얀 화면만 보이면 브라우저가 멈춘 것처럼 느껴질 수 있다.
버튼을 눌러 저장하는 상황도 마찬가지다. 저장 버튼을 누른 뒤 아무 변화가 없으면 사용자는 같은 버튼을 두세 번 더 누르기 쉽다. 그러면 중복 요청이 생기고, 글이 두 번 저장되거나 결제가 중복되는 더 큰 문제로 이어질 수 있다. 그래서 저장 중에는 버튼 문구를 "저장 중..."으로 바꾸거나, 버튼을 잠시 비활성화하는 것이 좋다.
로딩의 핵심은 멋진 애니메이션이 아니다. 지금 기다리는 이유가 있다는 것을 알려 주는 것이다.
좋은 로딩은 상황에 맞게 다르다
모든 로딩을 똑같이 처리하면 오히려 어색해질 수 있다. 화면 전체가 처음 열릴 때의 로딩과 버튼 하나를 누른 뒤의 로딩은 느낌이 다르기 때문이다.
페이지 첫 진입에서 데이터 전체를 기다리는 상황이라면 비교적 큰 로딩 표시가 자연스럽다. 반면 이미 화면에 내용이 보이는 상태에서 "좋아요" 버튼 하나를 누른 경우라면 전체 화면을 가리지 않는 편이 좋다. 이럴 때는 버튼 근처에서만 작은 로딩 표시를 보여 주는 것이 더 자연스럽다.
검색도 비슷하다. 검색어를 입력할 때마다 요청을 보내는 구조라면 사용자는 결과가 빠르게 바뀌기를 기대한다. 이때 화면 전체를 매번 비우고 큰 로딩을 띄우면 너무 부산스럽다. 기존 결과를 잠깐 유지한 채, "검색 중..." 같은 작은 힌트를 주는 방식이 훨씬 편하다.
즉, 로딩은 하나의 기능이 아니라 맥락에 맞는 반응이다. 사용자가 지금 무엇을 하고 있는지에 따라 로딩의 크기와 위치와 문구를 다르게 잡아야 한다.
실패는 숨길수록 더 불편해진다
많은 초보 프로젝트가 실패 상태를 이상하게 다룬다. 요청이 실패하면 콘솔에만 에러를 찍고, 화면은 그냥 멈춘다. 개발자는 콘솔을 볼 수 있지만, 사용자는 볼 수 없다. 사용자는 그저 "왜 안 되지?"만 반복하게 된다.
실패 상태에서 가장 중요한 것은 사용자를 탓하지 않는 것이다. "잘못된 요청입니다" 같은 딱딱한 말보다, 지금 무슨 문제가 생겼고 다음에 무엇을 하면 되는지를 알려 주는 편이 좋다.
예를 들어 목록을 불러오지 못했다면 "게시글을 불러오지 못했어요. 잠시 후 다시 시도해 주세요."처럼 말할 수 있다. 저장에 실패했다면 "글 저장에 실패했어요. 입력한 내용은 그대로 있으니 다시 시도해 보세요."라고 안내하면 훨씬 안심된다.
실패 화면은 단순히 에러를 선언하는 자리가 아니다. 사용자가 다음 행동을 선택할 수 있도록 도와주는 자리다. 그래서 가능하다면 다시 시도 버튼도 함께 주는 것이 좋다.
좋은 실패 처리는 보통 세 가지를 담고 있다.
무슨 일이 일어났는지
사용자가 얼마나 걱정해야 하는지
지금 무엇을 하면 되는지
이 셋이 빠지면 실패 문구는 쉽게 차갑고 불친절해진다.
빈 화면은 실패가 아니다
초보자가 특히 자주 놓치는 상태가 바로 빈 화면이다. 요청은 성공했다. 서버도 멀쩡하다. 그런데 보여줄 데이터가 없다. 이건 실패가 아니다. 그저 아직 비어 있는 상태다.
예를 들어 새로 만든 서비스라면 게시글이 하나도 없을 수 있다. 검색 결과가 없을 수도 있다. 로그인한 사용자가 아직 작성한 글이 없을 수도 있다. 이런 경우에 화면이 텅 비어 있으면 사용자는 실패로 오해하기 쉽다.
그래서 빈 화면은 분명하게 설명해야 한다. "아직 등록된 글이 없어요"처럼 현재 상황을 알려 주고, 가능하다면 다음 행동도 제안하자. 예를 들어 "첫 글을 작성해 보세요" 버튼을 함께 보여 주면 사용자는 막막하지 않다.
빈 화면은 생각보다 중요한 경험이다. 서비스 초반에는 빈 화면이 자주 나온다. 첫 사용자, 첫 게시글, 첫 검색, 첫 북마크는 전부 빈 상태에서 시작된다. 이때 친절한 한마디가 서비스의 첫인상을 바꾼다.
같은 빈 화면이라도 의미는 다르다
빈 화면도 종류가 다르다. 그래서 문구도 상황에 맞춰야 한다.
게시글이 하나도 없는 홈 화면의 빈 상태는 "서비스에 아직 콘텐츠가 없음"을 뜻한다. 반면 검색 결과가 없는 화면은 "입력한 조건에 맞는 결과가 없음"을 뜻한다. 내 작성글 화면에서 아무것도 보이지 않는다면 "당신이 아직 아무것도 작성하지 않음"을 뜻한다.
겉보기에는 모두 비어 있지만, 사용자에게 전달해야 할 메시지는 조금씩 다르다. 그래서 빈 화면 문구를 한 문장으로 통일해 버리면 아쉬운 경우가 많다.
예를 들어 이런 차이를 생각해 볼 수 있다.
홈 화면: 아직 등록된 글이 없어요.
검색 결과: 검색어와 일치하는 글이 없어요.
내 글 목록: 아직 작성한 글이 없어요.
북마크 화면: 저장해 둔 글이 아직 없어요.
문구가 조금만 달라져도 사용자는 지금 상황을 더 정확하게 이해한다. 이것이 서비스의 디테일이다.
목록 화면은 세 가지 표정을 가져야 한다
목록 화면은 로딩, 실패, 빈 상태를 가장 자주 만나는 장소다. 게시글 목록, 검색 결과, 댓글 목록, 내 알림 같은 화면이 전부 여기에 해당한다.
먼저 페이지에 들어왔을 때는 로딩 상태가 필요하다. 아직 서버 응답을 기다리는 중이라면 리스트 자리를 로딩 카드나 간단한 문구로 채운다. 응답이 실패했다면 "목록을 불러오지 못했어요"라고 알려 주고 재시도 버튼을 둘 수 있다. 응답은 성공했지만 데이터가 비어 있다면 "아직 항목이 없어요" 같은 빈 상태를 보여 주면 된다.
이때 중요한 것은 로딩, 실패, 빈 상태를 모두 "아무것도 안 보임"으로 처리하지 않는 것이다. 아무것도 안 보이는 결과는 같아도, 사용자가 받아들여야 할 의미는 완전히 다르기 때문이다.
목록 화면을 만들 때는 스스로 이렇게 물어보면 좋다. "데이터가 오기 전엔 무엇을 보여 줄까? 실패하면 뭐라고 말할까? 데이터가 0개면 사용자는 어디를 눌러야 할까?" 이 질문에 답할 수 있으면 목록 화면은 훨씬 완성도 높아진다.
상세 화면은 '없는 글'도 처리해야 한다
상세 페이지에서는 또 다른 예외가 등장한다. 예를 들어 사용자가 어떤 게시글 링크를 눌렀는데, 그 글이 이미 삭제되었을 수도 있다. 혹은 주소가 잘못되었을 수도 있다.
이때 상세 화면이 계속 로딩만 하거나, 화면 한가운데에 알 수 없는 에러 문장만 띄우면 사용자는 길을 잃는다. "존재하지 않는 글이에요" 또는 "삭제되었거나 접근할 수 없는 글입니다"처럼 현재 상황을 설명하고, 목록으로 돌아가는 버튼을 제공하는 편이 훨씬 낫다.
상세 페이지에서는 특히 로딩 상태와 없는 데이터 상태를 구분하는 것이 중요하다. 아직 불러오는 중인 것인지, 불러와 봤지만 실제로 없는 것인지가 다르기 때문이다. 이 둘을 섞으면 사용자는 영원히 기다려야 할 것처럼 느낄 수 있다.
폼 화면에서는 저장 중과 저장 실패가 중요하다
글 작성, 회원가입, 로그인, 프로필 수정 같은 폼 화면은 목록 화면과 다른 고민이 있다. 여기서는 "불러오기"보다 "제출하기"가 핵심이다.
사용자가 저장 버튼을 눌렀을 때는 지금 제출 중인지 분명하게 보여줘야 한다. 버튼 문구를 바꾸거나, 중복 클릭을 막거나, 상단에 진행 상태를 잠깐 보여주는 방법이 있다. 핵심은 사용자가 "내 요청이 받아들여졌구나"를 느끼게 만드는 것이다.
실패했을 때도 중요하다. 특히 폼 화면에서는 입력한 내용이 사라지면 안 된다. 저장 실패 후 폼이 초기화되면 사용자는 가장 화가 난다. 한참 적어 둔 내용을 다시 써야 하기 때문이다. 따라서 저장에 실패하더라도 사용자가 입력한 값은 남겨 두고, 다시 시도할 수 있게 하는 것이 좋다.
예를 들어 회원가입에서 비밀번호 조건이 맞지 않거나, 이미 사용 중인 이메일이라면 그 이유를 가능한 한 입력 필드 가까이 알려 주는 편이 좋다. "실패했습니다" 한 줄보다 훨씬 친절하다.
로딩과 실패는 버튼 하나에도 들어간다
상태 처리는 페이지 단위에서만 필요한 것이 아니다. 버튼, 입력창, 모달 같은 작은 요소에도 똑같이 적용된다.
예를 들어 삭제 버튼을 누르는 순간을 생각해 보자. 사용자가 버튼을 눌렀는데 아무 변화가 없으면 "눌린 건가?" 싶어서 다시 누를 수 있다. 반대로 버튼이 바로 사라져 버리면 무엇이 일어나는지 알기 어렵다. 이럴 때는 버튼을 잠시 비활성화하고 "삭제 중..."을 보여 주는 것이 좋다.
로그인 버튼도 마찬가지다. 누르는 순간 버튼이 회색으로 바뀌고 "로그인 중..."이라고 표시되면 사용자는 시스템이 반응하고 있다는 것을 안다. 실패하면 "이메일 또는 비밀번호를 다시 확인해 주세요"처럼 다음 행동으로 이어지는 안내가 필요하다.
결국 상태 처리는 큰 화면만의 일이 아니다. 사용자가 상호작용하는 모든 지점에서 필요하다.
문구는 짧게, 뜻은 분명하게
초보자는 종종 상태 문구를 너무 기술적으로 쓰거나, 반대로 너무 모호하게 쓰는 실수를 한다. 예를 들어 "500 Internal Server Error"는 개발자에게는 힌트가 되지만 일반 사용자에게는 거의 의미가 없다. 반대로 "문제가 발생했습니다"만 써 놓으면 사용자는 무엇을 해야 할지 모른다.
좋은 상태 문구는 짧지만 분명해야 한다. 어려운 용어를 줄이고, 사용자가 이해할 언어로 바꾸는 것이 중요하다. 그리고 가능하면 다음 행동도 함께 제시하면 좋다.
예를 들어 다음처럼 바꿔 볼 수 있다.
나쁜 예: 요청 처리 실패
더 나은 예: 게시글을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.
나쁜 예: 데이터 없음
더 나은 예: 아직 작성된 글이 없어요. 첫 글을 남겨 보세요.
나쁜 예: Unauthorized
더 나은 예: 이 기능은 로그인 후 사용할 수 있어요.
문구 하나에도 서비스의 성격이 드러난다. 딱딱한 시스템처럼 보일지, 친절한 도구처럼 보일지는 이런 작은 차이에서 갈린다.
상태를 섞지 말고 분리해서 생각하자
로딩, 실패, 빈 상태가 꼬이는 가장 흔한 이유는 이 셋을 한 덩어리로 처리하기 때문이다. 예를 들어 데이터가 아직 없다는 이유만으로 무조건 "빈 화면"을 띄우면, 사실은 아직 불러오는 중인데도 잘못된 메시지가 보일 수 있다.
그래서 순서를 분리해 생각하는 습관이 중요하다. 먼저 지금 요청이 끝났는지 본다. 아직 안 끝났다면 로딩이다. 요청이 끝났는데 실패했다면 실패 상태다. 성공했다면 그다음에 데이터가 있는지 본다. 데이터가 없으면 그때 빈 화면이다.
이 순서를 머릿속에 넣어 두면 상태 처리가 훨씬 안정적이다. 화면이 이상하게 깜빡이거나, 로딩과 빈 화면이 번갈아 보이는 문제도 줄어든다.
쉽게 말해, "없다"는 말은 요청이 끝난 뒤에만 할 수 있다. 아직 가져오는 중인데 없다고 말하면 너무 성급한 것이다.
사용자를 덜 지치게 만드는 작은 배려
상태 처리는 기술이기도 하지만, 결국은 사용자 피로를 줄이는 일이다. 몇 가지 작은 원칙만 지켜도 경험이 훨씬 좋아진다.
첫째, 너무 자주 전체 화면을 흔들지 않는다. 작은 동작에는 작은 반응이 좋다.
둘째, 실패했을 때 입력한 내용을 잃게 하지 않는다. 특히 긴 글 작성에서는 치명적이다.
셋째, 다시 시도할 수 있는 길을 남겨 둔다. 막다른 골목 같은 에러 화면은 답답하다.
넷째, 비어 있는 상태에서 다음 행동을 안내한다. "없음"으로 끝내지 말고 "무엇을 할 수 있는지"를 알려 준다.
이런 배려는 화려하지 않다. 하지만 실제 사용자는 이런 부분에서 서비스에 대한 신뢰를 느낀다.
AI에게 요청할 때도 상태를 나눠서 말해야 한다
이 장의 내용은 AI와 협업할 때도 아주 중요하다. 초보자는 종종 "목록 페이지가 이상해요"라고만 말한다. 하지만 그렇게 말하면 AI도 정확히 무엇이 문제인지 알기 어렵다.
대신 상태를 분리해 설명해 보자. 예를 들면 이런 식이다.
"페이지 첫 진입 시 데이터가 올 때까지 스켈레톤 UI를 보여주고 싶어요."
"요청 실패 시 에러 문구와 다시 시도 버튼을 추가해 주세요."
"검색 결과가 0개일 때는 빈 상태 메시지와 검색 초기화 버튼을 보여주고 싶어요."
"글 저장 중에는 버튼을 비활성화하고 문구를 저장 중으로 바꿔 주세요."
이렇게 요청하면 AI도 훨씬 정확한 결과를 준다. 즉, 상태를 나눠 생각하는 습관은 사용자 경험을 좋게 만들 뿐 아니라, AI에게 일을 잘 맡기는 데에도 도움이 된다.
우리의 서비스에 바로 적용해 보자
이 책에서 만드는 작은 풀스택 서비스에도 지금 당장 적용할 수 있다. 게시글 목록 페이지에서는 처음 진입 시 로딩 표시를 보여 주고, 글이 하나도 없으면 "첫 글을 작성해 보세요"라는 메시지를 띄울 수 있다. 게시글 상세 페이지에서는 존재하지 않는 글에 대한 안내와 목록으로 돌아가기 버튼을 둘 수 있다.
로그인 페이지에서는 로그인 중 상태를 버튼에 반영하고, 실패 시에는 너무 무섭지 않은 문구로 다시 입력을 유도할 수 있다. 글 작성 페이지에서는 저장 실패 시 폼 내용을 그대로 유지해 사용자가 다시 시도할 수 있게 만들 수 있다.
이런 처리는 한 줄짜리 기능처럼 보이지 않아서 뒤로 밀리기 쉽다. 하지만 실제 서비스 완성도는 이런 장면에서 크게 차이 난다. 사용자는 잘 만든 구조보다, 덜 불편한 경험을 더 먼저 기억한다.
예외 처리는 신뢰를 만든다
서비스를 쓰다 보면 항상 빠르고 완벽하게 작동하는 순간만 있는 것은 아니다. 네트워크가 느릴 때도 있고, 데이터가 아직 없을 때도 있고, 내가 실수로 잘못 입력할 때도 있다. 좋은 서비스는 이런 현실을 인정한다. 그리고 사용자를 혼자 남겨 두지 않는다.
로딩은 "지금 처리 중이에요"라고 말하는 친절함이다. 실패 처리는 "문제가 생겼지만 다시 할 수 있어요"라고 알려 주는 배려다. 빈 화면은 "아직 아무것도 없지만, 여기서 시작하면 돼요"라고 등을 밀어 주는 안내다.
바로 이 지점에서 서비스는 기능 모음집에서 한 단계 올라선다. 단순히 동작하는 앱이 아니라, 사람이 쓰기 편한 서비스가 된다.
이 장의 핵심 정리
데이터를 다루는 화면은 성공 상태만 있지 않다. 로딩, 실패, 빈 상태를 함께 설계해야 사용자가 현재 상황을 이해할 수 있다.
로딩 상태에서는 시스템이 반응하고 있다는 신호를 줘야 한다. 실패 상태에서는 원인보다 다음 행동을 알려 주는 것이 중요하다. 빈 화면은 에러가 아니라는 점을 분명히 하고, 가능하면 다음 행동으로 이어지는 안내를 제공해야 한다.
결국 예외 처리는 덤이 아니다. 서비스 품질의 중심이다. 잘 되는 경우만 만드는 것은 시작이고, 잘 안 되는 경우까지 다정하게 다루는 것이 완성이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.