뒤로가기
에러 메시지를 잘 쓰면 CS가 줄어든다

March 21, 2026

frontendessay

"오류가 발생했습니다. 다시 시도해주세요."

이 문장이 우리 서비스의 에러 메시지 중 70%를 차지하고 있었다. 결제 실패도 이거, 로그인 실패도 이거, 파일 업로드 실패도 이거. 유저 입장에서는 뭐가 잘못된 건지 알 수가 없다. 그러니까 CS팀에 전화한다. "저 결제가 안 되는데요, 뭔가 오류라고 뜨는데 뭔지 모르겠어요."

CS팀이 확인하면 카드 유효기간 만료인 경우가 대부분이었다. 에러 메시지에 "카드 유효기간이 만료되었습니다"라고 한 줄만 써줬으면 CS팀에 전화할 이유가 없었을 텐데.

이게 내가 에러 메시지에 관심을 갖게 된 계기다.

에러 메시지는 UX 라이팅이다#

Nielsen Norman Group의 연구에 따르면, 좋은 에러 메시지는 세 가지 조건을 충족해야 한다. 첫째, 눈에 보여야 한다. 둘째, 무슨 문제인지 명확해야 한다. 셋째, 해결 방법을 알려줘야 한다.

당연한 소리 같지만, 실제로 이 세 가지를 다 만족하는 에러 메시지를 쓰는 서비스가 얼마나 될까? 솔직히 개발자 대부분은 에러 메시지를 "귀찮은 예외 처리"로 본다. 기능 개발에 시간을 쓰지 에러 메시지 문구에 시간을 쓰는 건 비효율적이라고 느낀다.

나도 그랬다. catch 블록에 alert("오류가 발생했습니다")를 넣고 넘어갔다. 기획서에도 에러 메시지 문구가 없는 경우가 많았다. 기획자도 "에러 나면 적당히 알려주세요" 정도로 끝내는 거다. 결과적으로 서비스 전체가 "오류가 발생했습니다"로 도배된다.

Josh Comeau가 자신의 교육 사이트를 만들면서 보여준 것처럼, 사용자 경험에서 "명확함은 기능이지 부가 요소가 아니다." 에러 메시지도 마찬가지다. 에러 상황에서 유저가 느끼는 감정은 혼란과 불안이다. 이 순간에 유저에게 보여주는 텍스트가 서비스에 대한 신뢰를 결정한다.

실제로 해본 것#

작년 하반기에 우리 서비스의 에러 메시지를 전면 개선하는 작업을 했다. 계기는 CS팀에서 나온 데이터였다. 월간 CS 문의 중 "오류가 떠서 뭔지 모르겠다" 유형이 전체의 35%를 차지하고 있었다. CS 한 건 처리하는 데 평균 7분이 소요되니까, 이게 다 비용이다.

프로젝트 매니저를 설득할 때 이 숫자를 썼다. "에러 메시지를 개선하면 CS 비용을 줄일 수 있다"는 말에 바로 승인이 떨어졌다. 기술적으로 어려운 작업이 아니라 문구를 바꾸는 거니까 개발 리소스도 크지 않았다.

1단계: 에러 메시지 전수 조사#

먼저 코드베이스를 뒤져서 catch 블록과 에러 처리 로직을 전부 찾았다. 총 147개의 에러 핸들링 포인트가 있었고, 그중 98개가 "오류가 발생했습니다" 또는 그 변형이었다. 나머지 49개는 그나마 구체적인 메시지가 있었는데, 문제는 개발자 관점에서 쓰여 있었다는 거다.

text
// 개발자 관점의 에러 메시지
"네트워크 타임아웃이 발생했습니다"
"400 Bad Request"
"세션이 만료되었습니다"

일반 유저가 "네트워크 타임아웃"을 이해할까? "400 Bad Request"는? 이건 개발자끼리 쓰는 언어다. 유저에게는 아무 의미가 없다.

2단계: 프레임워크 만들기#

NNGroup의 가이드라인을 참고해서 팀 내부 프레임워크를 만들었다. 모든 에러 메시지는 세 가지 요소를 포함해야 한다.

무엇이 (What happened) — 어떤 문제가 발생했는지를 유저가 이해할 수 있는 말로. 왜 (Why) — 가능하다면 원인을 알려준다. 어떻게 (What to do next) — 유저가 다음에 뭘 해야 하는지.

이 프레임워크를 적용하면 에러 메시지가 이렇게 바뀐다.

변경 전:

text
오류가 발생했습니다. 다시 시도해주세요.

변경 후:

text
결제 카드의 유효기간이 만료되었습니다.
카드 정보를 업데이트하거나 다른 카드로 결제해주세요.
[카드 관리로 이동]

변경 전:

text
네트워크 타임아웃이 발생했습니다.

변경 후:

text
인터넷 연결이 불안정합니다.
Wi-Fi 또는 데이터 연결을 확인한 후 다시 시도해주세요.
[다시 시도]

변경 전:

text
세션이 만료되었습니다.

변경 후:

text
오랫동안 사용하지 않아 자동으로 로그아웃되었습니다.
보안을 위한 조치이며, 다시 로그인하면 이어서 사용할 수 있습니다.
[로그인하기]

차이가 보이는가? 변경 후의 메시지들은 유저를 멍하게 만들지 않는다. 무슨 문제인지 알려주고, 유저가 뭘 해야 하는지를 안내한다.

3단계: 에러 코드 기반 분기#

백엔드 API에서 내려오는 에러 코드에 따라 프론트엔드에서 다른 메시지를 보여주도록 했다. 이전에는 이런 식이었다.

typescript
try {
  await api.processPayment(data);
} catch (error) {
  alert("오류가 발생했습니다.");
}

바꾼 뒤에는 이렇게 됐다.

typescript
try {
  await api.processPayment(data);
} catch (error) {
  const message = getErrorMessage(error);
  showErrorToast(message);
}

function getErrorMessage(error: ApiError): ErrorMessage {
  const messages: Record<string, ErrorMessage> = {
    CARD_EXPIRED: {
      title: "카드 유효기간 만료",
      description: "등록된 카드의 유효기간이 지났습니다.",
      action: "카드 정보를 업데이트해주세요.",
      actionLink: "/settings/payment",
    },
    INSUFFICIENT_BALANCE: {
      title: "잔액 부족",
      description: "결제 금액이 카드 한도를 초과했습니다.",
      action: "다른 카드로 결제하거나 한도를 확인해주세요.",
      actionLink: "/settings/payment",
    },
    NETWORK_ERROR: {
      title: "연결 문제",
      description: "인터넷 연결이 불안정합니다.",
      action: "연결을 확인한 후 다시 시도해주세요.",
    },
    // ...
  };

  return messages[error.code] ?? {
    title: "일시적인 문제",
    description: "잠시 후 다시 시도해주세요.",
    action: "문제가 계속되면 고객센터로 문의해주세요.",
    actionLink: "/support",
  };
}

폴백 메시지도 "오류가 발생했습니다"가 아니라 "잠시 후 다시 시도해주세요"로 바꿨다. 그리고 반드시 고객센터 링크를 포함시켰다. 유저가 스스로 해결할 수 없는 상황이라도, 최소한 어디로 가야 하는지는 알려줘야 한다.

결과#

에러 메시지 개선 후 2개월간 데이터를 측정했다.

  • "오류 관련" CS 문의: 41% 감소
  • 결제 관련 CS 문의: 52% 감소 (가장 큰 폭)
  • 에러 발생 후 유저 이탈률: 18% 감소

가장 극적인 변화는 결제 관련이었다. 카드 만료, 잔액 부족, 한도 초과 같은 케이스는 에러 메시지만 제대로 보여줘도 유저가 스스로 해결할 수 있는 문제였다. 이전에는 그냥 "오류가 발생했습니다"만 보여주니까 유저가 무력감을 느끼고 CS에 전화하거나 아예 이탈하고 있었던 거다.

CS팀에서 가장 고마워했다. "이제 결제 문의 전화가 확 줄었어요"라는 말을 들었을 때, 에러 메시지 하나 바꾸는 게 이렇게 임팩트가 크다는 걸 실감했다.

에러 메시지를 쓸 때 주의할 점#

2개월간 작업하면서 배운 것들을 정리하면 이렇다.

유저를 탓하지 않는다#

text
// 나쁜 예
"잘못된 이메일 형식입니다"

// 좋은 예
"이메일 주소에 @가 포함되어야 합니다"

"잘못된"이라는 단어는 유저에게 "니가 틀렸어"라는 느낌을 준다. NNGroup 연구에서도 "invalid", "illegal" 같은 비난조 단어를 피하라고 권고한다. 대신 구체적으로 뭘 고치면 되는지를 알려주는 게 낫다.

전문 용어를 쓰지 않는다#

text
// 나쁜 예
"CORS 정책에 의해 차단되었습니다"

// 좋은 예
"현재 이 기능을 사용할 수 없습니다. 잠시 후 다시 시도해주세요."

CORS 에러가 유저에게 뭘 의미하는가? 아무것도 아니다. 유저가 해결할 수 있는 문제가 아니라면, 기술적 용어를 보여줄 필요가 전혀 없다.

에러가 발생한 곳 가까이에 표시한다#

회원가입 폼에서 비밀번호 규칙을 틀렸는데 에러가 페이지 상단에 표시되면, 유저는 스크롤을 올려서 에러를 읽고, 다시 스크롤을 내려서 비밀번호 필드를 찾아야 한다. 에러 메시지는 문제가 생긴 필드 바로 아래에 보여줘야 한다.

tsx
<div>
  <input
    type="password"
    value={password}
    onChange={handleChange}
    aria-invalid={!!error}
    aria-describedby="password-error"
  />
  {error && (
    <p id="password-error" role="alert">
      비밀번호는 8자 이상이어야 합니다 (현재 {password.length}자)
    </p>
  )}
</div>

"현재 몇 자"를 보여주는 것만으로도 유저의 행동이 달라진다. "8자 이상"만 보여주면 유저는 자기가 몇 자 쳤는지 세야 한다. 현재 길이를 알려주면 바로 판단이 된다.

유저의 입력을 보존한다#

에러가 발생했다고 폼을 초기화해버리면 유저는 처음부터 다시 입력해야 한다. 이건 생각보다 큰 스트레스다. 특히 모바일에서 긴 폼을 작성하다가 하나 틀렸는데 전부 날아가면 — 유저는 그냥 앱을 닫는다.

에러가 나도 유저가 입력한 값은 유지하고, 문제가 된 필드만 강조 표시하는 게 기본이다. 이건 기술적으로 어렵지 않은데, 의외로 많은 서비스가 안 지키고 있다.

개발자가 에러 메시지에 관심을 가져야 하는 이유#

솔직히 말해서, 에러 메시지는 PM이나 UX 라이터가 써야 하는 거 아니냐는 생각이 들 수 있다. 맞다. 이상적으로는 그래야 한다. 근데 현실적으로 한국 스타트업에서 UX 라이터가 있는 곳이 얼마나 되나? 대부분 개발자가 catch 블록 안에서 문구를 결정한다.

그렇다면 차라리 잘 쓰자. 기획서에 에러 메시지가 없으면 직접 제안하자. "이 경우에는 이런 메시지를 보여주면 어떨까요?"라고 기획자에게 슬랙 한 줄 보내는 게 CS 문의 100건보다 비용이 적다.

에러 메시지는 가장 적은 노력으로 가장 큰 사용자 경험 개선을 만들어낼 수 있는 영역이다. 코드 한 줄 리팩토링으로 성능을 1% 개선하는 것보다, 에러 메시지 한 문장을 고치는 게 유저에게 더 큰 영향을 줄 때가 많다.

결국 프론트엔드 개발자의 역할은 유저와 서비스 사이의 인터페이스를 만드는 거다. 에러 상황도 그 인터페이스의 일부다. 오히려 에러 상황에서의 인터페이스가 더 중요할 수도 있다. 정상적으로 작동할 때는 다들 비슷하니까. 차이를 만드는 건 뭔가 잘못됐을 때다.

그 잘못된 순간에, "오류가 발생했습니다" 한 줄을 보여줄 건지, 유저가 스스로 문제를 해결할 수 있게 안내할 건지. 그 차이가 CS 문의 40%를 좌우한다.