본문으로 건너뛰기

사용자를 불안하게 만들지 않는 프론트엔드 디테일

· 약 4분
Dongkyu Kim
Front-end Developer

사용자는 내부 시스템을 알지 못하기 때문에, 사소한 멈칫거림이나 불친절한 에러 메시지 하나에도 쉽게 불안함을 느낍니다.
그리고 이 불안은 곧 "이 서비스는 믿을 수 없다" 는 인상으로 이어지곤 합니다.

프론트엔드 개발자는 단순히 API를 연결하고 화면을 그리는 것을 넘어, 사용자가 서비스를 안심하고 사용할 수 있도록 돕는 역할도 해야 한다고 생각합니다.

이 글에서는 우리가 무심코 놓치기 쉬운, 하지만 사용자의 불안을 덜어줄 수 있는 몇 가지 디테일을 살펴보려고 합니다.

사소한 에러가 불안을 만드는 순간

사용자가 서비스를 이용하며 마주하는 '불안한 순간'들은 거창한 버그가 아닐 때가 많습니다.

  • 버튼을 눌렀는데 로딩 표시가 없어 클릭이 된 건지 모르겠다.
  • 에러가 났는데 "알 수 없는 오류"라고만 뜨고 어떻게 해야 할지 모르겠다.
  • 뒤로가기를 눌렀더니 작성 중이던 글이 다 날아가 버렸다.

이런 경험들이 쌓이면 사용자는 서비스를 신뢰하지 못하게 되고, 중요한 순간(예: 결제)에 주저하게 됩니다.
반대로 말하면, 이런 사소한 부분만 잘 챙겨도 서비스의 완성도와 신뢰도는 크게 올라갑니다.


우리가 놓치기 쉬운 예외 상황들

기획서나 디자인 시안에는 '정상적인 흐름(Happy Path)'만 그려져 있는 경우가 많습니다.
하지만 현실은 항상 예외가 가득하죠. 우리는 다음과 같은 상황들을 미리 대비해야 합니다.

예기치 못한 에러

  • 갑작스러운 네트워크 끊김
  • 서버의 일시적인 장애
  • API 응답 지연

사용자의 예측 불가능한 행동

  • 성격 급한 사용자의 버튼 연타
  • 작업 도중 브라우저 뒤로가기나 탭 닫기
  • 입력 폼 제출 직전 이탈

비동기 작업의 공백

  • 데이터가 로딩되는 동안의 빈 화면
  • 작업이 실패했을 때의 멍하니 멈춘 화면

불안을 안심으로 바꾸는 피드백

1. "오류" 대신 "방법"을 알려주기

개발자 콘솔에나 어울릴 법한 에러 메시지는 사용자를 당황하게 만듭니다.
상황을 설명하고, 다음에 무엇을 하면 되는지를 알려주세요.

  • Bad: Error: 500 Internal Server Error

  • Good: "서버에 잠시 문제가 생겼습니다. 잠시 후 다시 시도해 주세요."

  • Bad: Network Error

  • Good: "인터넷 연결이 불안정합니다. 와이파이를 확인해 주세요."

2. 실패해도 괜찮다는 느낌 주기

작업이 실패했을 때 사용자가 처음부터 다시 해야 한다면 큰 스트레스를 받습니다.
실패하더라도 쉽게 복구할 수 있는 장치를 마련해두면 사용자는 안심하고 서비스를 이용할 수 있습니다.

  • 작성 중이던 내용은 localStorage 등에 임시 저장
  • 삭제나 변경 작업 후 "실행 취소(Undo)" 버튼 제공
  • 에러 화면에 "새로고침"이나 "재시도" 버튼 배치
정보

Suspensive 라이브러리 활용하기

React에서 비동기 상태(로딩, 에러)를 선언적으로 처리하고 싶다면 @suspensive/react 같은 라이브러리가 큰 도움이 됩니다. 복잡한 분기 처리 없이도 로딩 스피너나 에러 폴백을 깔끔하게 보여줄 수 있습니다.

@suspensive/react 공식문서


사용자 인터랙션을 안전하게 받아주기

사용자가 실수를 하더라도 시스템이 안전하게 막아준다면, 그것이 곧 좋은 UX입니다.

버튼 연타 방지하기 (중복 요청 방지)

"결제하기" 버튼을 두 번 눌러서 결제가 두 번 된다면 끔찍하겠죠?
사용자가 버튼을 여러 번 클릭하더라도 한 번만 처리되도록 막아주는 것은 필수입니다.

const [isSubmitting, setIsSubmitting] = useState(false);

const handleClick = async () => {
if (isSubmitting) return; // 이미 처리 중이라면 무시

setIsSubmitting(true);
try {
await submitForm();
} finally {
setIsSubmitting(false); // 작업이 끝나면 다시 활성화
}
};

이 코드는 단순히 중복 요청을 막는 기능을 넘어, 사용자에게 "시스템이 안정적으로 동작하고 있다" 는 믿음을 줍니다.
더 좋은 방법은 isSubmitting 상태일 때 버튼을 disabled 처리하고, 로딩 스피너를 보여주는 것입니다.

리액트 쿼리(TanStack Query) 활용하기

useMutation을 사용하면 isPending 상태를 통해 더 쉽게 중복 방지 처리를 할 수 있습니다.

const { mutate, isPending } = useMutation({ mutationFn: createPost });

return (
<button
onClick={() => mutate(data)}
disabled={isPending} // 로딩 중 클릭 방지
>
{isPending ? '등록 중...' : '등록'}
</button>
);
  • 버튼이 비활성화됨으로써 사용자는 "아, 지금 처리 중이구나"라고 명확히 인지하게 됩니다.
  • 불필요한 서버 요청도 줄어드니 일석이조입니다.

개발자의 실수가 사용자 경험이 되지 않도록

우리는 언제나 완벽한 코드를 작성할 수는 없습니다.
하지만 사용자가 우리의 실수를 불편함으로 느끼지 않게 만들 수는 있습니다.

  • 에러가 나더라도 예쁜 일러스트와 함께 홈으로 가는 버튼을 보여주는 것
  • 로딩이 길어지더라도 지루하지 않게 스켈레톤 UI를 보여주는 것
  • 실수를 하더라도 쉽게 되돌릴 수 있는 기능을 제공하는 것

이런 작은 배려들이 모여 "이 서비스는 참 쓰기 편하다" 는 느낌을 만듭니다.

UX 디테일 체크리스트 ✅

내가 만든 화면이 사용자를 불안하게 하지는 않는지 한번 점검해 보세요.

  • 버튼을 눌렀을 때 '반응(로딩 등)'이 즉시 나타나는가?
  • 에러 메시지가 사용자 입장에서 이해하기 쉬운가?
  • 중요한 작업(삭제 등) 전에 한 번 더 물어보거나, 되돌릴 수 있는가?
  • 버튼을 빠르게 여러 번 눌러도 안전한가?
  • 네트워크가 끊겼을 때도 앱이 멈추지 않고 안내를 보여주는가?

마치며

기획자와 디자이너는 화면의 흐름과 모양에 집중하느라, 비동기 에러나 중복 클릭 같은 '개발적인 예외 상황'까지는 미처 챙기지 못할 때가 많습니다.

그 빈틈을 채우는 것이 바로 프론트엔드 개발자의 몫이자 능력이라고 생각합니다.

"이 작업은 실패했을 때 재시도 버튼이 있으면 좋겠어요."
"로딩 중에는 다른 버튼을 못 누르게 막아둘게요."

이런 제안들을 하나씩 더해갈 때, 서비스는 단단해지고 사용자는 비로소 마음 편히 서비스를 즐길 수 있게 될 것입니다.