지난 포스팅에 이은 에러 핸들링 개선하기 작업 두번째 파트이다.
저번 파트에서는 API 통신 중 일어나는 에러에 대해서 다루었다면 이번에는 사용자 입력 에러이다.
"사용자 입력 에러"란 사용자가 애플리케이션에 잘못된 형식이나 부적절한 데이터를 입력했을 때 발생하는 오류를 의미한다.
사용자 입력 에러가 발생하는 상황은 다음과 같다.
나열하고 보니 꽤나 다양하다.
하지만 중요한 것은 에러가 발생한 상황에서도 사용자가 혼란스럽지 않도록 명확하고 친절하게 안내하는 것이다.
에러가 발생했을 때 피드백은 가능한 빨리, 하지만 너무 심각하게 하지 않는 것이 중요하다고 생각한다. 이유는 사용자로 하여금 현재의 문제를 사소한 상황이라고 인지하게 하여 다음 페이지로 가볍게 넘어가게 하기 위함이다.
다양한 상황의 에러가 발생했을 때 아래 사항들을 지킬 수만 있다면 위와 같은 목적을 충분히 달성할 수 있으리라 생각한다.
형식 오류는 사용자가 입력하는 즉시 오류를 감지하고 무엇이 어떻게 틀렸는지 바로 보여줄 수 있어야 한다. 사용자가 딜레이 되는 현 상황을 인지하지 못하도록 빠른 수정을 유도해야 한다.
필수 입력 누락은 무엇을 놓쳤는가?에 대한 명확한 설명이 필요하다. 사용자가 현재 무엇 때문에 안되고 있는지 명확하고 구체적인 오류 메시지를 제공해야 한다.
입력 필드에 placeholder나 설명 텍스트를 사용하여 사용자가 어떤 형식으로 입력해야 하는지 미리 알려준다면 사전에 사용자가 이를 가이드 삼아 빠르고 정확하게 데이터를 기입할 수 있을 것이다.
필수적으로 입력하는 데이터가 모두 유효한 데이터 형식으로 들어왔을 때에만 제출 버튼을 활성화 하는 방법이다. 만약 사용자가 잘못된 데이터를 기입한 채로 제출 버튼을 누른다면 이전에 썼던 내용을 다시 작성해야 하는 수고가 생길 수 있다.
앞에서 다룬 내용들을 Wikied에도 적용시켜 보자.
Wikied에서 사용자가 데이터 값을 제출하는 상황들을 찾아보기 위해서 Swagger API에서 POST요청이 무엇이 있는지 확인해 보았다.

1. 프로필 생성
2. 이미지 업로드
3. 로그인, 회원가입
4. 게시글 작성
5. 게시글에 좋아요
6. 게시글 댓글 작성
위에 7가지 케이스 정도가 되는 것 같다.
케이스 하나씩 살펴보며, 사용자가 값을 어떻게 넣어야 할지, 어떤 상황에 에러가 발생할 수 있을지, 어떻게 대처하면 바람직할지에 대한 고민을 해보자.
첫번째, 사용자가 프로필을 생성하는 postProfile이다.
postProfile 함수는 현재 updateProfile이라는 메소드에서 실행되고 있다.
updateProfile 함수는 useAuthStore라는 전역 객체에 정의해 둔 메소드인데
이는 "/quiz-settings" 페이지에서 onSubmit으로 실행되는 함수이다.
const useAuthStore = create<AuthStore>()(
persist(
(set, get) => ({
(...중략)
updateProfile: async (securityAnswer, securityQuestion) => {
const { user } = get();
if (user) {
const updatedProfileData = await postProfile({
securityAnswer,
securityQuestion,
});
set({
user: {
...user,
profile: {
...user.profile,
...updatedProfileData,
},
},
});
return {
status: 200,
message: "프로필 업데이트에 성공했습니다.",
ok: true,
};
}
},
}),
)
);
export default useAuthStore;
(...중략)
<FormInput
id="securityQuestion"
placeholder="질문을 입력해 주세요"
type="text"
register={register("securityQuestion", {
required: "질문을 입력해 주세요",
})}
onKeyDown={handleKeyDown}
error={errors.securityQuestion}
/>
};
react-hook-form의 useform을 사용하여 유효성 검사를 해주고 있지만 앞서 설명한 5가지 점검 사항을 꼼꼼하게 지키고 있지 않는 모습이며, required 옵션 하나만 있어서, 데이터가 없었을 때에만 에러 메시지를 출력할 것 같다.
(...중략)
<FormInput
id="securityQuestion"
placeholder="질문을 입력해 주세요"
type="text"
register={register("securityQuestion", {
required: "질문을 입력해 주세요",
maxLength: {
value: 10,
message: "질문은 최대 10자 이하이어야 합니다.",
},
pattern: {
value: /^[가-힣a-zA-Z\s]+[?]$/,
message: `한글 또는 영문이어야 하며 "?"로 끝나야 합니다.`,
},
})}
onKeyDown={handleKeyDown}
error={errors.securityQuestion}
/>
react-hook-form의 register 함수는 require 이외로도 여러가지 규칙을 설정해줄 수 있다.
기존에는 required 하나만 있었다면 최대 길이를 10자로 제한하기 위한 maxLength와, 한/영, "?" 기호가 마지막에 들어가게 하기 위한 pattern이 추가되었다.
최종적으로 사용자가 프로필을 생성할 때의 Ux는 다음과 같이 바뀌었다.

두번째 케이스 postImage API 함수이다.
Wikied에서 발생하는 이미지 업로드, 프로젝트에 저장하는 이미지들은 모두 이 엔드포인트를 통해 업로드한 후 URL을 획득하여 사용하였다.

postImage에서 점검해야 하는 사항은 형식오류, 데이터의 범위 오류 두가지가 전부인 것 같다.
현재 postImage API는 내 위키 페이지의 ProfileCard 컴포넌트에서만 사용되고 있으며 중요 로직은 다음과 같다.
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const maxSizeInMB = 5;
if (file.size / 1024 / 1024 > maxSizeInMB) {
alert("파일 크기가 너무 큽니다. 5MB 이하의 파일을 선택해주세요!");
return;
}
const newFileName = convertFileName(file.name);
const newFile = new File([file], newFileName, { type: file.type });
const formData = new FormData();
formData.append("image", newFile); // 이미지 파일을 FormData에 추가
try {
// postImage 함수 호출 및 반환된 URL을 상태에 저장
const result = await postImage(formData);
if (result && result.url) {
setImageUrl(result.url);
}
} catch (err) {
console.error("이미지 업로드 중 에러 발생:", err);
}
}
};
file을 인자로 받아 크기가 5가 넘어갈 경우 alert로 경고 메시지를 잘 띄어주고 있는 모습이다. 데이터의 범위 오류는 잘 지켜지고 있는 것 같고 형식 오류만 개선해주자.
<input
id="image"
type="file"
accept="image/png, image/jpeg"
className="hidden"
onChange={handleImageChange}
/>
이미지를 업로드하는 input에 accpet 속성을 png와 jepg로 제한함으로써 사용자가 exe 파일과 같이, 이미지 파일이 아닌 다른 파일을 업로드하는 상황을 방지해 주었다.
세번째, 사용자가 로그인을 하는 postSign 요청의 상황이다.
1.형식 오류 : 이메일 형식이 맞지 않거나, ID에 특수문자가 포함되는 경우
-> “이메일 주소 형식이 올바르지 않습니다”, “아이디는 영문과 숫자만 사용할 수 있습니다” 메시지 경고
2.필수 입력 누락 : 아이디나 비밀번호 필드를 비운 채 로그인하는 경우
-> “아이디를 입력해주세요”, “비밀번호를 입력해주세요” 메시지 경고
3.데이터의 범위 오류 : 회원가입 때 비밀번호 길이를 제한하면 되기에 해당 x
4.유효성 검사 실패 : 없는 계정이거나 비밀번호 불일치
-> “아이디 또는 비밀번호가 올바르지 않습니다” 메시지 경고
5.데이터 타입 오류 : 해당 x
postSignIn에서 점검해야 하는 사항은 형식 오류, 필수 입력 오류, 유효성 검사 두가지 인 것 같다.
<FormInput
id="email"
label="이메일"
placeholder="이메일을 입력해주세요"
type="text"
register={register("email", {
required: true,
pattern: {
value: /^\S+@\S+\.\S+$/,
message: "이메일 형식으로 작성해 주세요.",
},
})}
onError={onClearSubmitError}
onKeyDown={handleKeyDown} // 엔터 키를 체크하는 이벤트 추가
error={errors.email}
/>
register에서 pattern 객체를 사용해서 이메일 형식으로 받도록 잘 검사하고 있다. 하지만 특수문자가 들어갔을 때에 처리는 따로 없는 것으로 보인다.
required가 있지만 true라는 boolean 값만 있기에 false가 될 때 렌더링 해줘야 할 err.message 객체가 undefined로 할당된다. 이 또한 수정이 필요해 보인다.
<FormInput
id="email"
label="이메일"
placeholder="이메일을 입력해 주세요."
type="text"
register={register("email", {
required: "이메일을 입력해 주세요.",
validate: {
isEmailFormat: (value) =>
/^\S+@\S+\.\S+$/.test(value) ||
"이메일 형식으로 작성해 주세요.",
noSpecialChars: (value) =>
/^[a-zA-Z0-9@.]+$/.test(value) ||
"@를 제외한 특수문자는 사용할 수 없습니다.",
},
})}
onError={onClearSubmitError}
onKeyDown={handleKeyDown} // 엔터 키를 체크하는 이벤트 추가
error={errors.email}
/>
required 속성에 falsy가 되었을 때 전해줄 err.message 문자열을 할당해주었고, validate라는 속성으로 여러 메소드들을 넣어 하나 이상의 validation을 추가해주었다.(이메일 형식이 아닌 경우, 아이디에 특수문자가 들어간 경우)
최종적으로 개선된 ux는 다음과 같다.

<FormInput
id="name"
label="이름"
placeholder="이름을 입력해 주세요"
type="text"
register={register("name", {
required: true,
pattern: {
value: /^[가-힣a-zA-Z]+$/,
message: "한글 또는 영문으로 입력해 주세요",
},
maxLength: {
value: 10,
message: "10자 이하로 작성해 주세요",
},
})}
onKeyDown={handleKeyDown}
error={errors.name}
/>
<FormInput
id="email"
label="이메일"
placeholder="이메일을 입력해주세요"
type="text"
register={register("email", {
required: true,
pattern: {
value: /^\S+@\S+\.\S+$/,
message: "이메일 형식으로 작성해 주세요.",
},
maxLength: {
value: 50,
message: "50자 이하로 작성해 주세요",
},
})}
onKeyDown={handleKeyDown}
error={errors.email}
/>
<FormInput
id="password"
label="비밀번호"
placeholder="비밀번호를 입력해주세요"
type="password"
register={register("password", {
required: true,
minLength: {
value: 8,
message: "8자 이상 20자 이하로 작성해 주세요.",
},
maxLength: {
value: 20,
message: "8자 이상 20자 이하로 작성해 주세요.",
},
})}
onKeyDown={handleKeyDown}
error={errors.password}
/>
<FormInput
id="passwordConfirmation"
label="비밀번호확인"
placeholder="비밀번호를 입력해주세요"
type="password"
register={register("passwordConfirmation", {
required: true,
validate: {
isPasswordCorrect: (value) =>
value === password || "비밀번호가 일치하지 않습니다",
},
})}
onKeyDown={handleKeyDown}
error={errors.passwordConfirmation}
/>
최종적으로 수정된 코드이다. 추가된 코드가 많지는 않지만, 데이터의 형식 오류나 범위 오류 등 중요한 규칙들이 일부 보완된 모습이다.
이번 포스팅은 FE가 UX를 개선하기 위해서 사용자 입력 에러에 어떻게 피드백할 것인가? 에 대한 내용이었다.
작성한 코드는 모두 비슷하고, 난이도도 그렇게 높지 않았지만 분명 중요한 문제라는 것은 분명하다. 때문에 실무에서도 놓치기 쉬운 것 중 하나일 것으로 추측이 된다.
이런 사소하지만 중요한 것들을 하나하나 꼼꼼하게 체크하고 넘어갈 줄 알아야 진정 UX를 중요하게 생각하는 개발자라고 스스로 생각할 수 있지 않을까?? 싶다.