폼 DOM 계층 구조와 렌더링 메커니즘
Layui 폼 컴포넌트는 명확한 위계 기반 마크업 규칙을 따릅니다. 올바른 클래스 네이밍과 컨테이너 분리는 스타일 충돌을 방지하고 호환성을 확보하는 핵심 요소입니다.
<!-- 폼 기본 템플릿 구조 -->
<form class="layui-form" lay-filter="appMainForm">
<div class="layui-form-item">
<label class="layui-form-label">사용자 ID</label>
<div class="layui-input-block">
<input type="text" name="userId" lay-verify="required|userNameRule" placeholder="ID를 입력하세요" class="layui-input">
</div>
</div>
</form>
| CSS 클래스 | 역할 | 필수 여부 |
|---|---|---|
layui-form | 폼 전역 컨테이너 | 예 |
layui-form-item | 입력 필드 래퍼 | 예 |
layui-form-label | 레이블 엘리먼트 | 선택 |
layui-input-block | 블록 단위 입력 영역 | 선택 |
layui-input-inline | 인라인 정렬 입력 영역 | 선택 |
자동 초기화 대신 명시적인 뷰 리프레싱 루틴을 호출해야 최신 DOM 트리를 반영합니다. 필요 최소한의 범위만 업데이트하면 렌더링 오버헤드를 줄일 수 있습니다.
// 전역 폼 인스턴스 로드
layui.use('form', function() {
var uiForm = layui.form;
// 전체 재렌더링
uiForm.render();
// 특정한 타입만 우선 적용
uiForm.render('select');
// 지정된 필터명으로 영역 한정 갱신
uiForm.render(null, 'appMainForm');
});
데이터 무결성 검증을 위한 검증 체계
내장 검증 규칙을 조합하거나 정규표현식을 매핑하여 비즈니스 로직에 맞는 검사 파이프라인을 구성할 수 있습니다. 필수 항목뿐만 아니라 조건부 검증도 유연하게 처리됩니다.
<input type="text" lay-verify="required|emailRule|phonePattern" placeholder="이메일 또는 휴대전화번호">
| 규칙명 | 용도 | 속성 예시 |
|---|---|---|
required | 빈값 차단 | lay-verify="required" |
email | 이메일 형식 체크 | lay-verify="email" |
phone | 휴대전화 숫자 유효성 | lay-verify="phone" |
url | URI 문법 검증 | lay-verify="url" |
number | 숫자 전용 입력 제한 | lay-verify="number" |
date | 날짜 포맷 일치 여부 | lay-verify="date" |
identity | 주민등록/신분증 검증 | lay-verify="identity" |
커스텀 검증 함수는 값을 받아서 오류 메시지를 반환하거나 빈 문자열(또는 false)을 반환하는 형태로 동작합니다. 이 기능으로 복잡한 패턴 매칭을 안전하게 제어할 수 있습니다.
uiForm.verify({
userNameRule: function(value) {
var usernameRegex = /^[a-zA-Z0-9_]{4,16}$/;
if (!usernameRegex.test(value)) {
return '아이디는 알파벳, 숫자, 언더스코어를 섞어 4~16자리로 작성해야 합니다.';
}
},
pwdStrength: function(value) {
if (!value) return; // 선택사항일 경우 조기 종료
var strongRegex = /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/;
if (value.length < 8 || !strongRegex.test(value)) {
return '비밀번호는 대소문자와 숫자를 포함해 8자 이상이어야 합니다.';
}
}
});
프로그래밍 방식으로 특정 필드 또는 전체 서브미션을 검증할 수도 있습니다. 이 방식은 버튼 클릭 트리거 시 즉시 확인 로직을 실행할 때 유용합니다.
// 단일 필드 즉시 검사
var isFieldValid = uiForm.validate('#targetInput');
if (!isFieldValid) {
layer.msg('입력값을 다시 확인해주세요.');
return;
}
// 여러 필드 일괄 점검 루프
function checkAllFields() {
var targetInputs = document.querySelectorAll('.layui-form input[lay-verify]');
var overallStatus = true;
for (var el of targetInputs) {
if (!uiForm.validate($(el))) {
overallStatus = false;
break;
}
}
return overallStatus;
}
반응형 그리드를 활용한 레이아웃 설계
데스크톱과 모바일 간 화면 비율 차이를 해결하기 위해 내장 컬럼 배치를 폼과 결합합니다. 여백 제어를 통해 시각적 조화를 맞추고 가독성을 높일 수 있습니다.
<form class="layui-form layui-row layui-col-space20">
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">닉네임</label>
<div class="layui-input-block">
<input type="text" name="nickname" class="layui-input">
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">연락처</label>
<div class="layui-input-block">
<input type="text" name="contact" lay-verify="phone" class="layui-input">
</div>
</div>
</div>
<div class="layui-col-xs12">
<div class="layui-form-item">
<label class="layui-form-label">배송 주소</label>
<div class="layui-input-block">
<input type="text" name="address" class="layui-input">
</div>
</div>
</div>
</form>
복합 검색 패널을 구성할 때는 입력창, 셀렉트박스, 날짜 픽커, 액션 버튼을 균등하게 분배하는 패턴이 효과적입니다.
<form class="layui-form layui-row layui-col-space16">
<div class="layui-col-sm6 layui-col-md3">
<input type="text" name="keyword" placeholder="검색어" class="layui-input">
</div>
<div class="layui-col-sm6 layui-col-md3">
<select name="category">
<option value="">전체 구간</option>
<option value="tech">기술 문서</option>
<option value="design">디자인 이슈</option>
</select>
</div>
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix"><i class="layui-icon layui-icon-date"></i></div>
<input type="text" name="dateRange" class="layui-input">
</div>
</div>
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-btn-container">
<button class="layui-btn" lay-submit>조회</button>
<button type="reset" class="layui-btn layui-btn-primary">초기화</button>
</div>
</div>
</form>
입력창 액세서리(Affix) 기능 확장
버전 2.8 이후부터 지원되는 액세서리 모듈은 입력 영역 내부에 기능 아이콘을 직접 삽입하여 사용자 경험을 단순화합니다. 숫자 스피너, 비밀번호 토글, 내용 지우기 등을 별도의 JS 없이 속성만으로 적용 가능합니다.
<!-- 숫자 입력 컨트롤 -->
<input type="text" lay-affix="number" step="5" min="0" max="500" class="layui-input">
<!-- 비밀번호 표시/숨기기 토글 -->
<div class="layui-input-wrap">
<input type="password" lay-affix="eye" placeholder="비밀번호 입력" class="layui-input">
</div>
<!-- 입력 완료 후 한 번 클릭으로 초기화 -->
<div class="layui-input-wrap">
<input type="text" lay-affix="clear" placeholder="내용 삭제" class="layui-input">
</div>
특정 액세서 유형이 활성화될 때 발생하는 이벤트를 구독하여 커스텀 워크플로우를 연결할 수 있습니다.
uiForm.on('input-affix(search)', function(payload) {
var activeField = payload.elem; // 대상 인풋 DOM
var accentType = payload.affix; // 적용된 아이콘 타입
var clickTarget = $(this); // 클릭된 아이콘 요소
if (accentType === 'search' && activeField.value.trim()) {
fetchSearchResults(activeField.value);
} else {
layer.tips('키워드를 먼저 입력해주세요.', clickTarget);
}
});
폼 컨트롤 고급 활용법
드롭다운은 대규모 옵션 목록 처리 시 검색과 자동 생성 기능이 유용합니다. 그룹화된 카테고리나 커스텀 테마 적용 역시 속성 하나로 빠르게 구현됩니다.
<!-- 실시간 필터링 셀렉트박스 -->
<select lay-search>
<option value="">도시 또는 지역 검색</option>
<option value="seoul">서울특별시</option>
<option value="busan">부산광역시</option>
</select>
<!-- 사용자가 직접 새로운 옵션 추가 가능 -->
<select lay-search lay-creatable>
<option value="">항목 선택 또는 신규 입력</option>
<option value="opt1">기존 레코드 1</option>
</select>
<!-- 카테고리별 그룹 분류 -->
<select>
<optgroup label="영역 A">
<option value="a1">세부 항목 A1</option>
</optgroup>
<optgroup label="영역 B">
<option value="b1">세부 항목 B1</option>
</optgroup>
</select>
체크박스와 라디오 버튼은 시각적 변형을 지원합니다. 태그 형태의 멀티 셀렉션이나 스위치 단위의 ON/OFF 토글 등을 프로젝트 디자인 가이드라인에 맞게 적용할 수 있습니다.
<!-- 기본 체크박스 -->
<input type="checkbox" name="skill" value="frontend" title="프론트엔드" lay-skin="primary">
<!-- 태그 형태 멀티 선택 -->
<input type="checkbox" name="tag" value="fullstack" title="풀스택" lay-skin="tag" checked>
<!-- 토글 스위치 -->
<input type="checkbox" name="systemMode" lay-skin="switch" title="시스템 활성화 | 비활성화">
<!-- 라디오 버튼 그룹 -->
<input type="radio" name="role" value="admin" title="관리자">
<input type="radio" name="role" value="viewer" title="참관자" checked>
데이터 바인딩과 이벤트 구독
폼 객체를 통해 직렬화된 데이터 객체를 쉽게 할당하거나 추출할 수 있습니다. 동적으로 채워진 값은 반드시 렌더링 루틴을 거쳐야 UI가 동기화됩니다.
// 기존 값 덮어쓰기
uiForm.val('editForm', {
account: 'dev_user',
region: 'asia-pacific',
theme: 'dark'
});
// 현재 폼 데이터 추출
var currentSnapshot = uiForm.val('editForm');
console.log(currentSnapshot);
// 외부 API 응답으로 폼 갱신
function refreshFormPayload(apiData) {
uiForm.val('editForm', apiData);
uiForm.render();
}
중요 상호작용마다 이벤트를 등록하면 사용자의 행동에 따라 다음 단계를 제어하거나 서버 통신을 준비할 수 있습니다.
// 셀렉트 변경 감지
uiForm.on('select(category-filter)', function(context) {
console.log('현재 선택:', this.innerHTML, context.value);
loadSubCategories(context.value);
});
// 약관 동의 체크박스 제어
uiForm.on('checkbox(access-agree)', function(ctx) {
var allowed = ctx.elem.checked;
$('#actionBtn').prop('disabled', !allowed);
});
// 폼 제출 흐름 처리
uiForm.on('submit(registerFlow)', function(formData) {
var fields = formData.field;
// 클라이언트 측 최종 검증 거치
if (!validateFinalSubmission(fields)) return false;
$.ajax({
url: '/api/v1/users/register',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(fields),
success: function(res) {
layer.msg('계정이 생성되었습니다.', {icon: 1});
},
error: function() {
layer.msg('요청 처리 중 오류가 발생했습니다.', {icon: 2});
}
});
return false; // 브라우저 기본 리다이렉트 차단
});
정의된 서브밋 플로우를 다른 버튼이나 함수 호출에서도 트리거할 수 있습니다. 이를 통해 모달 창이나 사이드바에서 폼을 재사용할 때 코드를 중복하지 않습니다.
$('#triggerExternalSubmit').on('click', function() {
uiForm.submit('registerFlow', function(submitData) {
if (validateFinalSubmission(submitData.field)) {
persistUserData(submitData.field);
}
});
});
실습: 회원가입 양식 완전 구현
위에서 설명한 검증, 그리드, 액세서리, 이벤트 구독을 종합하여 실제 서비스 수준의 등록 페이지를 구성합니다.
<form class="layui-form layui-row layui-col-space20" lay-filter="createAccountPanel">
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">아이디</label>
<div class="layui-input-block">
<div class="layui-input-wrap">
<div class="layui-input-prefix"><i class="layui-icon layui-icon-username"></i></div>
<input type="text" name="accountId" lay-verify="required|idChecker" placeholder="영문/숫자 4~12자리" class="layui-input">
</div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">이메일</label>
<div class="layui-input-block">
<input type="text" name="emailAddr" lay-verify="required|email" placeholder="example@mail.com" class="layui-input">
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">비밀번호</label>
<div class="layui-input-block">
<div class="layui-input-wrap">
<input type="password" name="secretPwd" lay-verify="required|pwdStrength" lay-affix="eye" placeholder="대소문자+숫자 혼합" class="layui-input">
</div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">비밀번호 확인</label>
<div class="layui-input-block">
<input type="password" name="confirmPwd" lay-verify="required|matchPassword" placeholder="다시 입력하세요" class="layui-input">
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-form-item">
<label class="layui-form-label">휴대폰</label>
<div class="layui-input-block">
<input type="text" name="mobileNum" lay-verify="phone" placeholder="하이픈 제거" class="layui-input">
</div>
</div>
</div>
<div class="layui-col-xs12">
<div class="layui-form-item">
<div class="layui-input-block">
<input type="checkbox" name="acceptTerms" lay-verify="required" title="이용약관에 동의합니다" lay-skin="primary">
</div>
</div>
</div>
<div class="layui-col-xs12">
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="accountSubmitBtn">회원가입 진행</button>
</div>
</div>
</div>
</form>
각 필드의 검증 로직과 비동기 제출 흐름을 집중 관리합니다. 피드백 UI를 병행하여 대기 시간을 체감하지 않도록 설계합니다.
layui.use(['form', 'layer'], function() {
var uiForm = layui.form;
var noticeLayer = layui.layer;
uiForm.verify({
idChecker: function(val) {
if (!/^[a-zA-Z0-9_]{4,12}$/.test(val)) {
return '아이디는 영문, 숫자, _ 기호만 사용해 4~12자로 설정하세요.';
}
},
matchPassword: function(val) {
var original = $('input[name="secretPwd"]').val();
if (val !== original) {
return '두 비밀번호가 서로 다릅니다.';
}
}
});
uiForm.on('submit(accountSubmitBtn)', function(payload) {
var submittedData = payload.field;
var loadingHandle = noticeLayer.load(2);
// 네트워크 요청 시뮬레이션
setTimeout(function() {
noticeLayer.close(loadingHandle);
noticeLayer.msg('정상 가입 처리되었습니다.', {icon: 1, time: 2000});
document.querySelector('form').reset();
uiForm.render();
}, 1200);
return false;
});
// 실시간 onBlur 검증 적용
$('input[lay-verify]').on('blur', function() {
uiForm.validate($(this));
});
});
성능 튜닝과 개발 관례
방대한 필드가 포함된 대형 폼에서는 무분별한 리렌더링이 메인 스레드를 점유할 수 있습니다. 필요한 부분만 점진적으로 그려주고 불필요한 리스너는 제거하는 것이 안정성에 도움이 됩니다.
// 가시 영역 내 필드만 우선 렌더링
function progressiveRenderForm() {
uiForm.render(document.querySelectorAll('.visible-fields'));
// 스크롤 시 나머지 영역 탐지 및 적용
window.addEventListener('scroll', function() {
var hiddenParts = document.querySelectorAll('.deferred-section');
hiddenParts.forEach(function(section) {
if (isElementInViewport(section)) {
uiForm.render(section);
section.classList.remove('deferred-section');
}
});
});
}
// 대량 데이터 한번에 주입 및 수동 리프레싱
function bulkInjectValues(dataMap) {
uiForm.set({autoRefresh: false}); // 임시 렌더링 차단
uiForm.val('batchForm', dataMap);
uiForm.render(); // 일괄 적용
uiForm.set({autoRefresh: true});
}
// 컴포넌트 언마운트 시 자원 정리
function disposeFormResources() {
uiForm.off('submit(createAccountPanel)');
uiForm.off('select(anyDropdown)');
}