체크리스트 앱에 수정 기능까지 붙이고 나면, 그다음에는 거의 비슷한 문제가 보입니다. 입력창은 잘 뜨고 저장 버튼도 있는데, 막상 써보면 빈칸 저장, 공백만 있는 항목, 같은 값 반복 저장, 연속 클릭으로 두 번 저장 같은 어색한 장면이 하나씩 나오기 시작합니다. 초보자 입장에서는 이때 "이제 검증도 넣어야겠구나"라고 느끼게 됩니다.

문제는 여기서 많은 사람이 검증을 하나의 큰 if 문으로만 생각한다는 점입니다. 예를 들면 저장 버튼 안에서 무조건 한 번 막아보는 식입니다. 물론 그 자체가 틀린 건 아닙니다. 하지만 조금만 기능이 붙으면 금방 애매해집니다. 입력 중에는 무엇을 보여줘야 하는지, 저장 직전에는 무엇을 막아야 하는지, 렌더링 단계에서는 무엇을 하면 안 되는지가 섞이기 시작하기 때문입니다.

이번 글에서는 왜 입력 검증이 단순한 부가 기능이 아니라 데이터 흐름을 안정시키는 핵심인지, 빈값과 공백과 중복을 왜 같은 규칙처럼 다루면 안 되는지, 그리고 검증을 입력 중, 저장 직전, 화면 표시 단계로 나눠서 보는 편이 왜 훨씬 덜 흔들리는지 차근차근 정리해보겠습니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
빈칸도 저장된다 저장 직전 최종 검증이 없거나, 공백 정리가 빠져 있다 trim 처리와 최종 저장 검증이 있는지 본다
취소했는데도 값이 어색하게 남는다 입력 중간값을 실제 데이터처럼 다루고 있다 임시 입력값과 실제 저장값을 분리했는지 확인한다
중복 저장 규칙이 들쭉날쭉하다 추가와 수정, 클릭과 엔터 입력에서 서로 다른 검증이 돈다 검증 함수를 공통으로 묶는 편이 낫다
에러는 막았는데 사용자 입장에서는 왜 안 되는지 모르겠다 코드 안에서만 return 하고 화면에는 아무 설명이 없다 에러 메시지도 별도 화면 상태로 다뤄야 한다

이번 글의 핵심 한 줄
입력 검증은 "나쁜 값을 나중에 고치는 일"보다 "문제가 될 값을 실제 데이터 안으로 들이기 전에 막는 일"에 더 가깝다.


목차

  1. 왜 입력 검증이 생각보다 중요해질까
  2. 검증은 무엇을 막는 일인가
  3. 검증은 어디서 해야 할까
  4. 빈값, 공백, 중복은 똑같이 다루면 안 된다
  5. 에러 메시지는 데이터가 아니라 화면 상태다
  6. 초보자가 자주 하는 실수 6가지
  7. 마무리

왜 입력 검증이 생각보다 중요해질까

입력 검증은 처음에는 사소해 보입니다. "빈칸이면 저장하지 않는다" 정도만 떠오르기 때문입니다. 그런데 실제 앱에서는 작은 이상값 하나가 다른 기능까지 같이 흔들 수 있습니다. 빈 항목이 목록에 들어가면 검색 결과가 어색해질 수 있고, 공백만 있는 값이 정렬이나 필터에서 이상하게 보일 수 있고, 너무 긴 텍스트가 들어가면 모바일 화면이 갑자기 지저분해질 수도 있습니다.

즉, 입력 검증은 단순히 보기 싫은 값을 막는 기능이 아니라 앱 전체가 다뤄야 할 데이터를 일정한 형태로 유지하는 장치에 가깝습니다. 값이 한 번 todos 안으로 들어간 뒤에는 렌더링, 검색, 정렬, 저장, 재정렬 같은 모든 흐름이 그 값을 기준으로 돌아갑니다. 그래서 초반에 막을 수 있는 문제라면, 가능한 한 그 지점에서 막는 편이 훨씬 낫습니다.

작은 입력 문제도 나중에는 여러 기능에 번질 수 있다
입력 문제 겉으로는 사소해 보여도 나중에 영향을 받는 곳
빈값 저장 목록에 내용 없는 줄이 생긴다 검색, 개수 계산, 화면 정렬
공백만 있는 값 눈에는 거의 안 보여도 실제 데이터로 남는다 중복 판단, 필터, 수정 흐름
너무 긴 입력 모바일에서 레이아웃이 흐트러질 수 있다 렌더링, 줄바꿈, 버튼 정렬
연속 저장 같은 값이 두 번 들어갈 수 있다 중복 데이터, 저장 흐름, 버튼 상태

핵심 포인트
입력 검증은 한 줄짜리 if 문처럼 보여도, 실제로는 todos 안으로 어떤 값이 들어갈 수 있는지 정하는 데이터 규칙에 가깝다.


검증은 무엇을 막는 일인가

"검증"이라고 하면 흔히 "틀린 값을 막는다" 정도로만 받아들이기 쉽습니다. 그런데 실제로는 검증 안에도 결이 조금 다릅니다. 어떤 것은 값을 정리하는 단계이고, 어떤 것은 값을 거절하는 단계이고, 어떤 것은 값이 아니라 행동 자체를 막는 단계입니다. 이걸 구분해두면 코드가 훨씬 덜 엉킵니다.

입력 검증도 종류가 조금 다르다
종류 예시 왜 따로 보면 좋은가
값 정리 앞뒤 공백 제거 같은 의미의 값을 일정한 형태로 맞출 수 있다
값 검증 빈값, 길이 제한, 허용하지 않는 문자 실제 데이터로 받아도 되는지 판단한다
행동 검증 저장 중일 때 연속 클릭 막기 값은 맞아도 지금 실행하면 안 되는 상황을 걸러낸다

예를 들어 " 장보기 "라는 값이 들어왔다고 해보겠습니다. 이건 보통 "틀린 입력"이라기보다 정리하면 되는 입력에 가깝습니다. 반면 ""이나 공백만 있는 값은 정리한 뒤에도 내용이 없으므로, 그때는 실제 저장을 막아야 합니다. 또 사용자가 저장 버튼을 두 번 연속 누르는 경우는 값 문제라기보다 행동 문제입니다. 값이 멀쩡해도 지금 한 번 더 저장하면 안 될 수 있습니다.

이런 차이를 생각하지 않으면 모든 걸 한 if 문에 몰아넣게 됩니다. 그러면 나중에 왜 어떤 경우는 trim만 하고, 어떤 경우는 저장을 막고, 어떤 경우는 버튼 자체를 잠가야 하는지가 흐려집니다.

쉽게 말하면
검증은 "값을 정리할 것인지", "값을 거절할 것인지", "지금 행동 자체를 막을 것인지"를 구분해서 보는 편이 훨씬 덜 헷갈린다.


검증은 어디서 해야 할까

입문자에게 가장 중요한 질문이 바로 여기입니다. 같은 검증이라도 입력 중에 보여주는 것이 있고, 저장 직전에 막아야 하는 것이 있고, 렌더링 단계에서는 아예 하면 안 되는 것도 있습니다. 이걸 나눠서 보면 구조가 훨씬 선명해집니다.

검증은 보통 세 지점으로 나눠서 보는 편이 좋다
지점 무엇을 맡기면 좋은가 왜 여기서 하는가
입력 중 문자 수 표시, 가벼운 경고, 저장 버튼 활성화 여부 사용자가 지금 무엇이 문제인지 미리 볼 수 있다
저장 직전 trim, 빈값 검사, 길이 검사, 중복 행동 막기 실제 데이터로 들어가기 전 마지막 방어선이기 때문이다
렌더링 단계 에러 메시지 표시, 비활성화 상태 반영 렌더링은 보여주는 역할이지, 데이터를 고치는 역할은 아니기 때문이다

여기서 특히 중요한 건 마지막 줄입니다. 렌더링 단계에서 빈값을 "자동으로 고쳐버리거나", 잘못된 값을 보고 그제야 todos를 다시 바꾸는 식은 처음엔 편해 보여도 구조를 금방 흐립니다. 렌더링은 원칙적으로 현재 상태를 보여주는 단계로 두는 편이 훨씬 낫습니다. 검증과 데이터 변경은 저장 직전 단계에서 끝내는 쪽이 훨씬 안정적입니다.

입문자용으로는 아래처럼 공통 검증 함수를 하나 두는 방식이 꽤 유용합니다.

function validateTodoText(rawValue) {

const value = rawValue.trim();

if (!value) {
return {
ok: false,
reason: "empty"
};
}

if (value.length > 50) {
return {
ok: false,
reason: "tooLong"
};
}

return {
ok: true,
value
};
}

그리고 저장 직전에는 이 함수를 통해 최종 판단을 합니다.

function saveEdit() {

const result = validateTodoText(uiState.editDraft);

if (!result.ok) {
uiState.formError = result.reason;
renderTodos();
return;
}

// 여기서부터 실제 데이터 반영
}

핵심 정리
입력 중 검증은 안내에 가깝고, 저장 직전 검증은 차단에 가깝고, 렌더링은 그 결과를 보여주는 단계에 가깝다. 이 셋의 역할이 섞이면 금방 구조가 흐려진다.


빈값, 공백, 중복은 똑같이 다루면 안 된다

초보자가 가장 자주 하는 오해 중 하나는, 검증 규칙을 전부 같은 강도로 다뤄야 한다고 생각하는 것입니다. 하지만 실제로는 그렇지 않습니다. 어떤 것은 거의 항상 막는 편이 좋고, 어떤 것은 앱의 목적에 따라 달라질 수 있습니다.

같은 검증처럼 보여도 성격이 다르다
규칙 처음 버전에서 보통 어떻게 보는가 왜 같은 취급을 하면 안 되나
빈값 대개 막는 편이 좋다 항목 의미 자체가 없어지기 때문이다
공백만 있는 값 trim 후 빈값처럼 본다 겉으로는 안 보여도 실제 데이터로 남기면 더 헷갈린다
너무 긴 값 길이 제한을 둘 수 있다 레이아웃과 사용성을 해칠 수 있지만, 허용 범위는 앱마다 다를 수 있다
같은 내용 중복 상황에 따라 다르다 "우유 사기"가 두 번 필요한 날도 있을 수 있다
연속 저장 막는 편이 좋다 값 문제라기보다 행동 중복 문제다

특히 중복 규칙은 함부로 "무조건 금지"라고 말하기 어렵습니다. 개인용 체크리스트에서는 같은 항목이 두 번 들어가는 것이 자연스러울 수도 있습니다. 반면 태그 목록이나 고정된 명칭 입력 같은 곳에서는 중복을 막는 편이 더 자연스러울 수 있습니다. 즉, 중복은 거의 항상 막아야 하는 빈값과는 성격이 다릅니다.

수정과 추가도 완전히 같지는 않습니다. 예를 들어 수정에서 원래 값과 같은 내용을 다시 저장하려는 경우, 이것을 "오류"로 볼 필요는 없을 수도 있습니다. 그냥 변경 없음으로 보고 편집 모드만 닫아도 충분한 경우가 많습니다.

추가와 수정은 같은 검증을 써도 결과 해석은 다를 수 있다
상황 처음 버전에서 자주 쓰는 해석
추가할 값이 빈칸이다 저장을 막는다
수정 중 값이 빈칸이다 저장을 막고 편집 상태는 유지할 수 있다
수정 중 값이 원래와 똑같다 오류라기보다 변경 없음으로 보고 닫을 수 있다

핵심 포인트
빈값은 거의 항상 막는 편이 좋지만, 중복은 앱 목적에 따라 달라질 수 있다. 모든 검증 규칙을 같은 성격으로 보면 오히려 이상한 제약이 생긴다.


에러 메시지는 데이터가 아니라 화면 상태다

검증을 붙이다 보면 자연스럽게 "그럼 사용자에게는 어떻게 알려주지?"라는 질문이 나옵니다. 여기서도 초보자가 자주 흔들리는 부분이 있습니다. 에러 메시지를 todo 데이터 안에 넣거나, 반대로 코드 안에서만 return 하고 화면에는 아무것도 안 보여주는 경우입니다. 둘 다 조금씩 불편합니다.

입문 단계에서는 에러 메시지도 화면 상태로 보는 편이 가장 무난합니다. 즉, todos 안에 저장할 필요는 없고, 지금 현재 입력창 아래에 잠깐 보여줄 정보로 생각하면 됩니다.

let uiState = {

editingId: null,
editDraft: "",
formError: "",
isSaving: false
};

이 구조가 있으면 저장 실패 시 formError를 채우고, 저장이 성공하거나 취소하면 비우는 식으로 흐름을 만들 수 있습니다.

function saveEdit() {

const result = validateTodoText(uiState.editDraft);

if (!result.ok) {
if (result.reason === "empty") {
uiState.formError = "내용을 입력해 주세요.";
}

if (result.reason === "tooLong") {
  uiState.formError = "너무 긴 내용은 저장할 수 없습니다.";
}

renderTodos();
return;

}

uiState.formError = "";

// 실제 저장 처리
}

화면에서는 이 상태를 읽어 입력창 아래에 안내 문구를 보여주면 됩니다. 이때 중요한 건 에러 메시지도 현재 상태에 따라 사라져야 한다는 점입니다. 사용자가 다시 입력을 시작해서 조건을 만족하게 됐다면, 이전 에러를 계속 남겨둘 이유가 없습니다.

에러 메시지를 다룰 때도 역할을 나누는 편이 좋다
역할 무엇을 하나
검증 함수 ok 여부와 reason만 돌려준다
저장 함수 reason을 바탕으로 formError를 채운다
렌더링 formError를 읽어 화면에 보여준다

이렇게 나누면 검증 자체는 깔끔하게 유지되고, 메시지 문구는 화면 요구에 맞춰 바꾸기 쉬워집니다. 나중에 같은 reason에 대해 다른 문구를 보여주고 싶어도 검증 함수까지 다시 손댈 필요가 줄어듭니다.

핵심 정리
에러 메시지는 todo 데이터가 아니라 현재 화면에서 잠깐 보여줄 안내에 가깝다. 그래서 formError 같은 화면 상태로 다루는 편이 훨씬 자연스럽다.


초보자가 자주 하는 실수 6가지

입력 검증을 붙이기 시작하면 구조가 조금 더 또렷해지지만, 동시에 비슷한 실수도 반복해서 보입니다. 대부분은 검증 위치가 섞이거나, 실제 데이터와 임시값 구분이 다시 흐려질 때 생깁니다.

실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 보는 게 좋은가
render 단계에서 빈값을 몰래 고친다 화면은 맞는 것 같은데 실제 저장 흐름이 더 헷갈려진다 렌더링이 데이터 수정까지 떠안고 있다 렌더링은 보여주는 역할에 가깝게 두는 편이 좋다
추가와 수정에 서로 다른 검증 규칙을 흩어 놓는다 한쪽은 빈값이 막히고 다른 쪽은 그대로 저장된다 검증 기준이 공통 함수로 안 모여 있다 validateTodoText 같은 공통 검증 함수부터 두는 편이 낫다
trim 없이 길이나 빈값만 검사한다 공백만 있는 항목이 슬쩍 들어온다 값 정리 단계가 빠졌다 검증 전에 최소한의 값 정리는 먼저 하는 편이 좋다
중복 금지를 무조건 기본값처럼 넣는다 사용자 입장에서는 합리적인 같은 항목도 저장이 안 된다 중복을 빈값과 같은 절대 규칙으로 봤다 중복은 앱 목적에 따라 다르게 정하는 편이 낫다
저장 버튼 연속 클릭을 값 검증 문제로만 본다 같은 항목이 두 번 생기거나 중복 저장이 된다 행동 검증이 빠져 있다 isSaving처럼 동작 자체를 잠그는 기준도 같이 본다
에러 메시지를 보여주고도 안 지운다 입력이 이미 정상인데도 계속 에러처럼 보인다 formError 초기화 기준이 없다 수정 시작, 저장 성공, 취소, 유효한 입력 변화에서 정리하는 편이 좋다

특히 다섯 번째 실수는 꽤 자주 놓칩니다. 사용자는 같은 버튼을 두 번 눌렀을 뿐인데, 개발자는 "값 검증은 통과했는데 왜 중복 저장됐지?"라고 생각하기 쉽습니다. 이건 값 문제가 아니라 행동 문제에 가깝습니다. 그래서 입력 검증을 생각할 때는 텍스트 값만 볼 게 아니라, 지금 이 행동이 중복 실행돼도 되는가까지 같이 보는 편이 훨씬 현실적입니다.

실전에서 가장 많이 흔들리는 부분
검증이 어색하게 느껴질 때는 "규칙이 없어서"보다 "검증을 해야 할 위치가 섞여 있어서"인 경우가 더 많다. 입력 중, 저장 직전, 렌더링의 역할을 다시 나눠서 보면 훨씬 선명해진다.


마무리

입력 검증은 앱을 더 엄격하게 만드는 기능이라기보다, 실제 데이터 안으로 어떤 값이 들어갈 수 있는지 기준을 세우는 작업에 가깝습니다. 이 기준이 생기면 빈값과 공백을 더 일찍 막을 수 있고, 저장과 취소의 의미도 분명해지고, 다른 기능들이 이상한 데이터 때문에 같이 흔들리는 일도 줄어듭니다.

 

이번 글에서 꼭 가져가면 좋은 건 세 가지입니다.
첫째, 검증은 값 정리, 값 검증, 행동 검증으로 나눠서 보는 편이 좋다는 점.

둘째, 입력 중 검증과 저장 직전 검증, 렌더링의 역할은 서로 다르다는 점.

셋째, 에러 메시지는 실제 데이터가 아니라 화면 상태로 두는 편이 훨씬 자연스럽다는 점입니다.

 

여기까지 오면 체크리스트 앱은 단순히 기능이 많은 화면이 아니라, 나쁜 값이 어디서 막히고 좋은 값이 어디서 확정되는지 설명할 수 있는 구조를 갖추기 시작합니다. 다음 편에서는 이 흐름을 이어서, 저장 버튼을 누른 순간 바로 localStorage나 서버 반영까지 같이 묶일 때 어떤 순서로 처리하는 편이 좋은지, 즉 저장 흐름을 좀 더 안전하게 나누는 구조를 다루겠습니다.