Skip to content

Conversation

@CoBool
Copy link
Collaborator

@CoBool CoBool commented Nov 5, 2025

폼 검증 로직 리팩토링

📋 변경 사항 요약

폼 검증 로직을 config 객체 기반에서 data-validate 속성 기반의 선언적 검증 시스템으로 리팩토링했습니다.

🎯 주요 개선 사항

1. 선언적 검증 시스템 도입

변경 전:

  • config 객체로 login/signup 분리 관리
  • form.dataset.type으로 폼 타입 구분
  • 각 필드별 validator 함수와 isInvalid 플래그로 상태 관리

변경 후:

  • HTML에서 data-validate="required|email|max:50" 형태로 검증 규칙 선언
  • rules 객체로 규칙 기반 검증 (required, email, min, max, match)
  • validateInput() 함수로 각 input을 독립적으로 검증

2. 코드 구조 개선

  • 규칙별 통합 메시지: 함수 지원으로 동적 메시지 생성
    min: (len) => `최소 ${len}자 이상 입력해주세요.`
  • 개선된 상태 관리: invalid/valid 클래스로 시각적 피드백
  • 버튼 상태 관리: updateButtonState() 함수로 모든 필드 검증 후 버튼 활성화/비활성화
  • 반복 코드 제거 및 헬퍼 함수 활용

3. 검증 규칙 예시

<!-- 로그인 페이지 -->
<input data-validate="required|email|max:50" />

<!-- 회원가입 페이지 -->
<input data-validate="required|min:8|max:20" />
<input data-validate="required|match:password" />

📁 변경된 파일

  • assets/js/form.js - 검증 로직 리팩토링
  • pages/login.html - data-validate 속성 추가
  • pages/signup.html - data-validate 속성 추가
  • README.md - 변경사항 문서화

✅ 테스트 체크리스트

  • 로그인 폼 검증 동작 확인
  • 회원가입 폼 검증 동작 확인
  • 실시간 검증 및 에러 메시지 표시 확인
  • 버튼 활성화/비활성화 동작 확인
  • 비밀번호 보기/숨기기 토글 기능 확인
  • 반응형 디자인 유지 확인

🔍 변경 전후 비교

변경 전 구조

const config = {
  login: {
    email: { validator: validateEmail, isInvalid: true },
    password: { validator: validatePassword, isInvalid: true },
  },
  signup: { ... }
};

변경 후 구조

const rules = {
  required: (val) => val.trim() !== "",
  email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
  min: (val, len) => val.trim().length >= Number(len),
  max: (val, len) => val.trim().length <= Number(len),
  match: (val, targetName, input) => { ... }
};

💡 장점

  1. 유지보수성 향상: HTML에서 검증 규칙을 명확하게 확인 가능
  2. 확장성: 새로운 검증 규칙 추가가 용이
  3. 재사용성: 동일한 규칙을 다양한 필드에 적용 가능
  4. 가독성: 코드 구조가 더 명확하고 이해하기 쉬움

⚠️ 단점 및 제한사항

  1. HTML과 JavaScript 결합도 증가: data-validate 속성으로 인해 HTML과 JavaScript가 밀접하게 결합됨
  2. 타입 안전성 부족: 문자열 기반 규칙 파싱으로 런타임 오류 가능성 존재
  3. IDE 지원 부족: data-validate 속성의 자동완성 및 타입 체크 지원이 제한적
  4. 규칙 파싱 오류 위험: 잘못된 형식의 규칙 문자열 입력 시 예상치 못한 동작 가능
  5. 복잡한 검증 로직 표현 어려움: 복잡한 비즈니스 로직은 문자열 규칙으로 표현하기 어려움
  6. 규칙 추가 시 수정 범위 확대: 새로운 검증 조건이 추가되면 rules 객체에 추가하고, HTML의 data-validate 속성에도 규칙을 추가해야 함. 조건이 복잡해질수록 유지보수 비용이 증가함

🚀 향후 개선 계획

1. name 기반 검증으로 전환

  • 현재: HTML의 data-validate 속성 기반 검증
  • 개선 방향: form 내부의 name 속성을 기준으로 각 필드를 명시적으로 검증
  • 구현 방식: focusout 이벤트에서 e.target.name을 기준으로 분기 처리
    form.addEventListener('focusout', (e) => {
      if (!e.target.matches('input')) return;
      
      const name = e.target.name;
      if (name === 'email') {
        validateEmailField(e.target);
      } else if (name === 'password') {
        validatePasswordField(e.target);
      }
      // ...
    });
  • 장점:
    • HTML과 JavaScript의 결합도 감소
    • 검증 로직을 JavaScript에서 명시적으로 제어
    • HTML은 마크업만 담당하여 관심사 분리

2. 각 input에 직접 이벤트 추가

  • 현재: form 레벨에서 focusout 이벤트를 한 곳에서 처리
  • 개선 방향: 각 input 필드에 직접 이벤트 리스너를 추가하여 명시적으로 검증
  • 구현 방식:
    const form = document.querySelector('form');
    const email = form.querySelector('[name="email"]');
    const password = form.querySelector('[name="password"]');
    
    email.addEventListener('focusout', () => {
      validateEmailField(email);
      updateSubmitButton(form);
    });
    
    password.addEventListener('focusout', () => {
      validatePasswordField(password);
      updateSubmitButton(form);
    });
  • 기대 효과:
    • 읽기 쉬운 코드: 각 필드의 검증 로직을 한눈에 파악 가능
    • 뛰어난 유지보수성: 규칙이 추가되거나 변경되는 경우 해당 input만 수정하면 됨
    • 명시적이고 절차적: 각 필드가 독립적으로 관리되어 코드 흐름이 명확함
  • 단점:
    • 중복되는 부분이 많아 코드량이 증가할 수 있음
    • 하지만 이는 명확성과 유지보수성을 위한 합리적인 트레이드오프

3. 검증 규칙 모듈화

  • 현재: rules 객체가 form.js 내부에 포함
  • 개선 방향: 검증 규칙을 별도 모듈로 분리하여 모듈화
  • 구조 예시:
    assets/js/
      - form.js          (폼 검증 로직)
      - validation-rules.js (검증 규칙 모듈)
    
  • 장점:
    • 검증 규칙과 로직의 분리
    • 재사용성 향상
    • 유지보수성 개선
    • 테스트 용이성 증가

4. input type별 이벤트 처리 분기

  • 현재: 모든 input에 대해 focusout 이벤트만 처리
  • 개선 방향: input type에 따라 적절한 이벤트를 사용하여 검증
  • 구현 방식:
    • text, email, password 등: focusout 이벤트
    • checkbox, radio: change 이벤트
    • select: change 이벤트
  • 장점:
    • 각 input type의 특성에 맞는 검증 시점 설정
    • checkbox, radio 등에서도 정확한 검증 가능
    • 사용자 경험 개선

- common.css 추가하여 공통 컴포넌트 분리
- 페이지별 헤더 네이밍 분리 (page-header, form-header)
- CSS 변수 확대 (색상, 간격)
- 비밀번호 토글 버튼 wrapper 구조 개선
- README 업데이트
- 폼 검증 로직을 설정 기반으로 구현
- 비밀번호 보기/숨기기 토글 기능 구현
- passwordConfirm 검증 로직 개선 (password 비어있을 때 처리)
- null 안전성 체크 추가 (optional chaining)
- README.md 업데이트 (완료된 작업 반영)
@CoBool CoBool added bug Something isn't working good first issue Good for newcomers 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. labels Nov 5, 2025
Copy link
Collaborator

@humonnom humonnom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 전체적으로 요구사항도 잘 구현하셨고
코드도 깔끔하게 잘 짜셨어요

그런데 form.js에서 하나 건의 드리자면,
UI 업데이트가 두번 일어나게 되는 부분을 수정해보시면 어떨까 하는데요.

UI 업데이트 중복 발생 이유

form.addEventListener("focusout", (e) => {
  if (e.target.matches("input")) {
    validateInput(e.target); // validateInput에서 UI변경이 일어남
    updateButtonState(); // 함수 내부에서 validateInput을 호출하기 때문에 UI변경이 또 발생
  }
})

UI 업데이트가 두번 일어나는 것 확인하기

// form.js에서 아래 부분을 고치면
// line 58(validateInput)
      showError(input, msg + Date.now()); // 에러메세지를 띄울때 시간을 같이 표기
// line 77(focusout 이벤트 리스너)
    setTimeout(() => {
        updateButtonState(); // 
    },3000)
default.mov
  • 입력값을 하나 지워서 7글자로 만듬
  • 포커스 아웃 -> 에러 메시지와 함께 ...40로 끝나는 숫자 표시됨(첫번째 UI업데이트)
  • 5초후 -> 버튼이 disabled되며 메시지 뒤의 숫자가 ...42로 바뀝니다.(두번째 UI업데이트)

해결방법(pseudo코드)

  • UI상태 업데이트 함수와 validation을 담당하는 함수를 분리합니다.
// validate 담당
function validateInput(input) {
  // 이 함수에서는 UI업데이트x, validation만 담당
  return true or false // 결과를 boolean으로 리턴
}

// 업데이트 담당
function updateMessage(input) {
  const isValid = validateInput(input)
  if (isValid) { 성공 표시 }
  else { 에러 표시 }
}

function updateButtonState() {
  const inputs = form.querySelectorAll("input[data-validate]");
  const allValid = [...inputs].every(validateInput); // 검증
  submitButton.disabled = !allValid; // 버튼 상태만 업데이트
}

form.addEventListener("focusout", (e) => {
  if (!e.target.matches("input[data-validate]")) return;
  // 현재 필드 메시지 업데이트
  updateMessage(e.target)
  // 버튼 상태만 업데이트
  updateButtonState();
});

이렇게 바꾸면 에러 메시지 UI 업데이트가 한번만 일어나게 할 수 있을 것 같은데
한번 수정을 고려해주세요.

나머지는 코멘트로 남겨두었으니 참고 부탁드릴게요!

}

const messages = {
required: "필수 입력 항목입니다.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(권장) 메세지를 요구사항과 정확하게 따라주시면 더 좋을 것 같아요. 지금은 괜찮지만 나중에 과제 테스트 등을 수행하실 때 정확히 일치하지 않으면 오답 처리가 될 수 있어서 가능한 정확히 따르는 습관을 들여주세요!

스크린샷 2025-11-07 오후 6 07 16

Copy link
Collaborator Author

@CoBool CoBool Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 강사님 :)

해당부분 확인했습니다.
계속 변경하고 리팩토링하는 과정에서 요구사항을 놓친거같습니다.

이 부분은 조금 더 신경쓰겠습니다.
이전 요구사항도 몇가지 놓쳤던것같네요.

에러메시지가 2번 호출되는현상도 분리하여 처리하겠습니다.

확인감사합니다.

</div>
</div>
</div>
<script src="../assets/utility/validation.js"></script>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(필수) 사용하지 않는 js 파일은 가져오지 않도록 수정해주세요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당부분은 기존에 별도 함수로 분리해서 가지고 있다가 프로젝트가 작아서 하나로 합치는 과정에서 수정누락되었습니다.

모듈형태로 리팩토링하여 수정하도록 하겠습니다 :)

const isValid = param ? fn(value, param, input) : fn(value);

if (!isValid) {
const msg = typeof messages[name] === "function" ? messages[name](param) : messages[name];
Copy link
Collaborator

@humonnom humonnom Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const msg = typeof messages[name] === "function" ? messages[name](param) : messages[name];
const msg = messages[name](param)
  • messages를 아래처럼 함수 형태로 통일하면, type을 검사할 필요가 없어지지 않을까요?
const messages = {
  required: () => "필수 입력 항목입니다.",
  email: () => "이메일 형식이 올바르지 않습니다.",
  min: (len) => `최소 ${len}자 이상 입력해주세요.`,
  max: (len) => `최대 ${len}자 이하 입력해주세요.`,
  match: () => "비밀번호가 일치하지 않습니다.",
};
  • 그리고 만약에 함수형태로 통일한다면, 이름도 바뀌면 좋을 거 같네요. 이름만 보고도 함수라는 걸 알 수 있게요.
const getErrorMessage = {...}
  • 아니면 switch case로 바꿔도 될 것 같네요.
function getErrorMessage(name, param) {
  switch (name) {
    case "required": return "필수 입력 항목입니다.";
    case "email": return "이메일 형식이 올바르지 않습니다.";
    case "min": return `최소 ${param}자 이상 입력해주세요.`;
    case "max": return `최대 ${param}자 이하 입력해주세요.`;
    case "match": return "비밀번호가 일치하지 않습니다.";
    default: return "";
  }
}

// 사용부
const msg = formatValidationMessage(name, param);

<div class="form-block__input-wrapper">
<input class="form-block__input" type="password" id="password" name="password" placeholder="비밀번호를 입력해주세요." data-validate="required|min:8|max:20">
<button class="form-block__button-eye" type="button">
<img class="form-block__button-eye-icon" src="../assets/images/icons/eye.svg" alt="눈 열기">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(권장) a가 아닌 button으로 감싸주신것 잘하셨는데, 조금 더 명확한 의미 전달이 되면 좋을 것 같네요.
동작 중심으로 짧게 서술해주시면 좋아요.
• 권장: “비밀번호 표시” 또는 “비밀번호 숨기기”

토글 상태에 따라 바꾸면 더 좋습니다. 버튼이 비밀번호를 보여주는 상태면 “비밀번호 숨기기”, 숨긴 상태면 “비밀번호 표시”

<span class="oauth-block__text">간편 로그인하기</span>

<div class="oauth-block__group">
<a class="oauth-block__button" href="https://www.google.com/" target="_blank">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<a class="oauth-block__button" href="https://www.google.com/" target="_blank">
<a class="oauth-block__button" href="https://www.google.com/" target="_blank" aria-label="Google로 로그인">

(질문) 이렇게 aria-label 사용하면 스크린리더 대응은 충분할 것 같은데, sr-only 적용된 span 사용하신 이유가 따로 있을까요?

@humonnom humonnom closed this Nov 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working good first issue Good for newcomers 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants