검증
검증 (Validation)
브라우저 기본 동작이나 커스텀 스타일과 JavaScript를 통해, HTML5 폼 검증으로 사용자에게 가치 있고 실행 가능한 피드백을 제공해 보아요.
출처: 문서
본문
현재 클라이언트 사이드 커스텀 검증 스타일과 툴팁은 보조 기술에 노출되지 않아 접근할 수 없다는 점을 알고 있어요. 해결책을 작업하는 동안, 서버 사이드 옵션이나 기본 브라우저 검증 방법을 사용하는 것을 권장해요.
작동 방식 (How it works)
다음은 Bootstrap에서 폼 검증이 작동하는 방식이에요:
- HTML 폼 검증은 CSS의 두 의사 클래스인
:invalid과:valid를 통해 적용돼요. 이는<input>,<select>,<textarea>요소에 적용돼요. - Bootstrap은
:invalid과:valid스타일을 부모.was-validated클래스(보통<form>에 적용)에 범위를 지정해요. 그렇지 않으면 값이 없는 필수 필드가 페이지 로드 시 유효하지 않은 것으로 표시돼요. 이렇게 하면 활성화 시점(보통 폼 제출을 시도한 후)을 선택할 수 있어요. - 폼의 외형을 재설정하려면(Ajax를 사용한 동적 폼 제출의 경우 등), 제출 후
<form>에서.was-validated클래스를 다시 제거해요. - 폴백으로, 서버 사이드 검증에는 의사 클래스 대신
.is-invalid과.is-valid클래스를 사용할 수 있어요. 이들은.was-validated부모 클래스가 필요하지 않아요. - CSS 작동 방식의 제약으로 인해, DOM에서 폼 컨트롤 앞에 오는
<label>에 커스텀 JavaScript의 도움 없이는 스타일을 적용할 수 없어요 (현재). - 모든 최신 브라우저는 폼 컨트롤 검증을 위한 일련의 JavaScript 메서드인 제약 검증 API(constraint validation API)를 지원해요.
- 피드백 메시지는 브라우저 기본값(브라우저마다 다르고 CSS로 스타일링할 수 없음)이나 추가 HTML과 CSS를 사용한 우리의 커스텀 피드백 스타일을 활용할 수 있어요.
- JavaScript에서
setCustomValidity로 커스텀 유효성 메시지를 제공할 수 있어요.
이를 염두에 두고, 커스텀 폼 검증 스타일, 선택적 서버 사이드 클래스, 브라우저 기본값에 대한 다음 데모를 살펴보세요.
커스텀 스타일 (Custom styles)
커스텀 Bootstrap 폼 검증 메시지를 사용하려면 <form>에 novalidate 불리언 속성을 추가해야 해요. 이는 브라우저 기본 피드백 툴팁을 비활성화하지만 JavaScript의 폼 검증 API 접근은 여전히 제공해요. 아래 폼을 제출해 보세요. 우리의 JavaScript가 제출 버튼을 가로채 피드백을 전달할 거예요. 제출을 시도하면 :invalid과 :valid 스타일이 폼 컨트롤에 적용된 것을 볼 수 있어요.
커스텀 피드백 스타일은 커스텀 색상, 테두리, 포커스 스타일, 배경 아이콘을 적용해 피드백을 더 잘 전달해요. <select>의 배경 아이콘은 .form-select에서만 사용할 수 있고 .form-control에서는 사용할 수 없어요.
<form class="row g-3 needs-validation" novalidate>
<div class="col-md-4">
<label for="validationCustom01" class="form-label">First name</label>
<input type="text" class="form-control" id="validationCustom01" value="Mark" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="col-md-4">
<label for="validationCustom02" class="form-label">Last name</label>
<input type="text" class="form-control" id="validationCustom02" value="Otto" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="col-md-4">
<label for="validationCustomUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-group-text" id="inputGroupPrepend">@</span>
<input type="text" class="form-control" id="validationCustomUsername" aria-describedby="inputGroupPrepend" required>
<div class="invalid-feedback">
Please choose a username.
</div>
</div>
</div>
<div class="col-md-6">
<label for="validationCustom03" class="form-label">City</label>
<input type="text" class="form-control" id="validationCustom03" required>
<div class="invalid-feedback">
Please provide a valid city.
</div>
</div>
<div class="col-md-3">
<label for="validationCustom04" class="form-label">State</label>
<select class="form-select" id="validationCustom04" required>
<option selected disabled value="">Choose...</option>
<option>...</option>
</select>
<div class="invalid-feedback">
Please select a valid state.
</div>
</div>
<div class="col-md-3">
<label for="validationCustom05" class="form-label">Zip</label>
<input type="text" class="form-control" id="validationCustom05" required>
<div class="invalid-feedback">
Please provide a valid zip.
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="invalidCheck" required>
<label class="form-check-label" for="invalidCheck">
Agree to terms and conditions
</label>
<div class="invalid-feedback">
You must agree before submitting.
</div>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Submit form</button>
</div>
</form>
// Example starter JavaScript for disabling form submissions if there are invalid fields
(() => {
'use strict'
// Fetch all the forms we want to apply custom Bootstrap validation styles to
const forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
브라우저 기본값 (Browser defaults)
커스텀 검증 피드백 메시지나 폼 동작을 변경하는 JavaScript 작성에 관심이 없나요? 괜찮아요, 브라우저 기본값을 사용할 수 있어요. 아래 폼을 제출해 보세요. 브라우저와 OS에 따라 약간 다른 스타일의 피드백을 볼 수 있어요.
이 피드백 스타일은 CSS로 스타일링할 수 없지만, JavaScript로 피드백 텍스트는 여전히 커스터마이즈할 수 있어요.
<form class="row g-3">
<div class="col-md-4">
<label for="validationDefault01" class="form-label">First name</label>
<input type="text" class="form-control" id="validationDefault01" value="Mark" required>
</div>
<div class="col-md-4">
<label for="validationDefault02" class="form-label">Last name</label>
<input type="text" class="form-control" id="validationDefault02" value="Otto" required>
</div>
<div class="col-md-4">
<label for="validationDefaultUsername" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text" id="inputGroupPrepend2">@</span>
<input type="text" class="form-control" id="validationDefaultUsername" aria-describedby="inputGroupPrepend2" required>
</div>
</div>
<div class="col-md-6">
<label for="validationDefault03" class="form-label">City</label>
<input type="text" class="form-control" id="validationDefault03" required>
</div>
<div class="col-md-3">
<label for="validationDefault04" class="form-label">State</label>
<select class="form-select" id="validationDefault04" required>
<option selected disabled value="">Choose...</option>
<option>...</option>
</select>
</div>
<div class="col-md-3">
<label for="validationDefault05" class="form-label">Zip</label>
<input type="text" class="form-control" id="validationDefault05" required>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="invalidCheck2" required>
<label class="form-check-label" for="invalidCheck2">
Agree to terms and conditions
</label>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Submit form</button>
</div>
</form>
서버 사이드 (Server-side)
클라이언트 사이드 검증을 권장하지만, 서버 사이드 검증이 필요하다면 .is-invalid과 .is-valid로 유효하지 않거나 유효한 폼 필드를 표시할 수 있어요. .invalid-feedback도 이 클래스들과 함께 지원된다는 점에 유의하세요.
유효하지 않은 필드의 경우, aria-describedby를 사용해 유효하지 않은 피드백/오류 메시지가 관련 폼 필드와 연결되도록 해야 해요 (이 속성은 필드가 이미 추가 폼 텍스트를 가리키는 경우 둘 이상의 id를 참조할 수 있다는 점에 유의하세요).
테두리 반경 문제를 해결하려면 입력 그룹에 추가 .has-validation 클래스가 필요해요.
<form class="row g-3">
<div class="col-md-4">
<label for="validationServer01" class="form-label">First name</label>
<input type="text" class="form-control is-valid" id="validationServer01" value="Mark" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="col-md-4">
<label for="validationServer02" class="form-label">Last name</label>
<input type="text" class="form-control is-valid" id="validationServer02" value="Otto" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="col-md-4">
<label for="validationServerUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-group-text" id="inputGroupPrepend3">@</span>
<input type="text" class="form-control is-invalid" id="validationServerUsername" aria-describedby="inputGroupPrepend3 validationServerUsernameFeedback" required>
<div id="validationServerUsernameFeedback" class="invalid-feedback">
Please choose a username.
</div>
</div>
</div>
<div class="col-md-6">
<label for="validationServer03" class="form-label">City</label>
<input type="text" class="form-control is-invalid" id="validationServer03" aria-describedby="validationServer03Feedback" required>
<div id="validationServer03Feedback" class="invalid-feedback">
Please provide a valid city.
</div>
</div>
<div class="col-md-3">
<label for="validationServer04" class="form-label">State</label>
<select class="form-select is-invalid" id="validationServer04" aria-describedby="validationServer04Feedback" required>
<option selected disabled value="">Choose...</option>
<option>...</option>
</select>
<div id="validationServer04Feedback" class="invalid-feedback">
Please select a valid state.
</div>
</div>
<div class="col-md-3">
<label for="validationServer05" class="form-label">Zip</label>
<input type="text" class="form-control is-invalid" id="validationServer05" aria-describedby="validationServer05Feedback" required>
<div id="validationServer05Feedback" class="invalid-feedback">
Please provide a valid zip.
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input is-invalid" type="checkbox" value="" id="invalidCheck3" aria-describedby="invalidCheck3Feedback" required>
<label class="form-check-label" for="invalidCheck3">
Agree to terms and conditions
</label>
<div id="invalidCheck3Feedback" class="invalid-feedback">
You must agree before submitting.
</div>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Submit form</button>
</div>
</form>
지원 요소 (Supported elements)
검증 스타일은 다음 폼 컨트롤과 컴포넌트에서 사용할 수 있어요:
.form-control이 있는<input>과<textarea>(입력 그룹의.form-control최대 하나 포함).form-select가 있는<select>.form-check
<form class="was-validated">
<div class="mb-3">
<label for="validationTextarea" class="form-label">Textarea</label>
<textarea class="form-control" id="validationTextarea" placeholder="Required example textarea" required></textarea>
<div class="invalid-feedback">
Please enter a message in the textarea.
</div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="validationFormCheck1" required>
<label class="form-check-label" for="validationFormCheck1">Check this checkbox</label>
<div class="invalid-feedback">Example invalid feedback text</div>
</div>
<div class="form-check">
<input type="radio" class="form-check-input" id="validationFormCheck2" name="radio-stacked" required>
<label class="form-check-label" for="validationFormCheck2">Toggle this radio</label>
</div>
<div class="form-check mb-3">
<input type="radio" class="form-check-input" id="validationFormCheck3" name="radio-stacked" required>
<label class="form-check-label" for="validationFormCheck3">Or toggle this other radio</label>
<div class="invalid-feedback">More example invalid feedback text</div>
</div>
<div class="mb-3">
<select class="form-select" required aria-label="select example">
<option value="">Open this select menu</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
<div class="invalid-feedback">Example invalid select feedback</div>
</div>
<div class="mb-3">
<input type="file" class="form-control" aria-label="file example" required>
<div class="invalid-feedback">Example invalid form file feedback</div>
</div>
<div class="mb-3">
<button class="btn btn-primary" type="submit" disabled>Submit form</button>
</div>
</form>
툴팁 (Tooltips)
폼 레이아웃이 허용한다면 .{valid|invalid}-feedback 클래스를 .{valid|invalid}-tooltip 클래스로 바꿔 스타일링된 툴팁에 검증 피드백을 표시할 수 있어요. 툴팁 위치 지정을 위해 position: relative가 있는 부모가 있어야 해요. 아래 예시에서는 컬럼 클래스가 이미 이것을 갖고 있지만, 프로젝트에는 다른 설정이 필요할 수도 있어요.
<form class="row g-3 needs-validation" novalidate>
<div class="col-md-4 position-relative">
<label for="validationTooltip01" class="form-label">First name</label>
<input type="text" class="form-control" id="validationTooltip01" value="Mark" required>
<div class="valid-tooltip">
Looks good!
</div>
</div>
<div class="col-md-4 position-relative">
<label for="validationTooltip02" class="form-label">Last name</label>
<input type="text" class="form-control" id="validationTooltip02" value="Otto" required>
<div class="valid-tooltip">
Looks good!
</div>
</div>
<div class="col-md-4 position-relative">
<label for="validationTooltipUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-group-text" id="validationTooltipUsernamePrepend">@</span>
<input type="text" class="form-control" id="validationTooltipUsername" aria-describedby="validationTooltipUsernamePrepend" required>
<div class="invalid-tooltip">
Please choose a unique and valid username.
</div>
</div>
</div>
<div class="col-md-6 position-relative">
<label for="validationTooltip03" class="form-label">City</label>
<input type="text" class="form-control" id="validationTooltip03" required>
<div class="invalid-tooltip">
Please provide a valid city.
</div>
</div>
<div class="col-md-3 position-relative">
<label for="validationTooltip04" class="form-label">State</label>
<select class="form-select" id="validationTooltip04" required>
<option selected disabled value="">Choose...</option>
<option>...</option>
</select>
<div class="invalid-tooltip">
Please select a valid state.
</div>
</div>
<div class="col-md-3 position-relative">
<label for="validationTooltip05" class="form-label">Zip</label>
<input type="text" class="form-control" id="validationTooltip05" required>
<div class="invalid-tooltip">
Please provide a valid zip.
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Submit form</button>
</div>
</form>
CSS
변수 (Variables)
v5.3.0에서 추가됨
Bootstrap의 진화하는 CSS 변수 접근 방식의 일부로, 폼은 이제 검증을 위해 로컬 CSS 변수를 사용해 향상된 실시간 커스터마이즈를 제공해요. CSS 변수의 값은 Sass로 설정되므로 Sass 커스터마이즈도 여전히 지원돼요.
scss/_root.scss
--#{$prefix}form-valid-color: #{$form-valid-color};
--#{$prefix}form-valid-border-color: #{$form-valid-border-color};
--#{$prefix}form-invalid-color: #{$form-invalid-color};
--#{$prefix}form-invalid-border-color: #{$form-invalid-border-color};
이 변수들은 색상 모드 적응형이기도 해서, 다크 모드에서 색상이 변경돼요.
Sass 변수 (Sass variables)
scss/_variables.scss
$form-feedback-margin-top: $form-text-margin-top;
$form-feedback-font-size: $form-text-font-size;
$form-feedback-font-style: $form-text-font-style;
$form-feedback-valid-color: $success;
$form-feedback-invalid-color: $danger;
$form-feedback-icon-valid-color: $form-feedback-valid-color;
$form-feedback-icon-valid: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'><path fill='#{$form-feedback-icon-valid-color}' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/></svg>");
$form-feedback-icon-invalid-color: $form-feedback-invalid-color;
$form-feedback-icon-invalid: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='#{$form-feedback-icon-invalid-color}'><circle cx='6' cy='6' r='4.5'/><path stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/><circle cx='6' cy='8.2' r='.6' fill='#{$form-feedback-icon-invalid-color}' stroke='none'/></svg>");
scss/_variables.scss
$form-valid-color: $form-feedback-valid-color;
$form-valid-border-color: $form-feedback-valid-color;
$form-invalid-color: $form-feedback-invalid-color;
$form-invalid-border-color: $form-feedback-invalid-color;
scss/_variables-dark.scss
$form-valid-color-dark: $green-300;
$form-valid-border-color-dark: $green-300;
$form-invalid-color-dark: $red-300;
$form-invalid-border-color-dark: $red-300;
Sass 믹스인 (Sass mixins)
두 개의 믹스인이 우리의 루프를 통해 결합되어 폼 검증 피드백 스타일을 생성해요.
scss/mixins/_forms.scss
@mixin form-validation-state-selector($state) {
@if ($state == "valid" or $state == "invalid") {
.was-validated #{if(&, "&", "")}:#{$state},
#{if(&, "&", "")}.is-#{$state} {
@content;
}
} @else {
#{if(&, "&", "")}.is-#{$state} {
@content;
}
}
}
@mixin form-validation-state(
$state,
$color,
$icon,
$tooltip-color: color-contrast($color),
$tooltip-bg-color: rgba($color, $form-feedback-tooltip-opacity),
$focus-box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity),
$border-color: $color
) {
.#{$state}-feedback {
display: none;
width: 100%;
margin-top: $form-feedback-margin-top;
@include font-size($form-feedback-font-size);
font-style: $form-feedback-font-style;
color: $color;
}
.#{$state}-tooltip {
position: absolute;
top: 100%;
z-index: 5;
display: none;
max-width: 100%; // Contain to parent when possible
padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x;
margin-top: .1rem;
@include font-size($form-feedback-tooltip-font-size);
line-height: $form-feedback-tooltip-line-height;
color: $tooltip-color;
background-color: $tooltip-bg-color;
@include border-radius($form-feedback-tooltip-border-radius);
}
@include form-validation-state-selector($state) {
~ .#{$state}-feedback,
~ .#{$state}-tooltip {
display: block;
}
}
.form-control {
@include form-validation-state-selector($state) {
border-color: $border-color;
@if $enable-validation-icons {
padding-right: $input-height-inner;
background-image: escape-svg($icon);
background-repeat: no-repeat;
background-position: right $input-height-inner-quarter center;
background-size: $input-height-inner-half $input-height-inner-half;
}
&:focus {
border-color: $border-color;
@if $enable-shadows {
@include box-shadow($input-box-shadow, $focus-box-shadow);
} @else {
// Avoid using mixin so we can pass custom focus shadow properly
box-shadow: $focus-box-shadow;
}
}
}
}
// stylelint-disable-next-line selector-no-qualifying-type
textarea.form-control {
@include form-validation-state-selector($state) {
@if $enable-validation-icons {
padding-right: $input-height-inner;
background-position: top $input-height-inner-quarter right $input-height-inner-quarter;
}
}
}
.form-select {
@include form-validation-state-selector($state) {
border-color: $border-color;
@if $enable-validation-icons {
&:not([multiple]):not([size]),
&:not([multiple])[size="1"] {
--#{$prefix}form-select-bg-icon: #{escape-svg($icon)};
padding-right: $form-select-feedback-icon-padding-end;
background-position: $form-select-bg-position, $form-select-feedback-icon-position;
background-size: $form-select-bg-size, $form-select-feedback-icon-size;
}
}
&:focus {
border-color: $border-color;
@if $enable-shadows {
@include box-shadow($form-select-box-shadow, $focus-box-shadow);
} @else {
// Avoid using mixin so we can pass custom focus shadow properly
box-shadow: $focus-box-shadow;
}
}
}
}
.form-control-color {
@include form-validation-state-selector($state) {
@if $enable-validation-icons {
width: add($form-color-width, $input-height-inner);
}
}
}
.form-check-input {
@include form-validation-state-selector($state) {
border-color: $border-color;
&:checked {
background-color: $color;
}
&:focus {
box-shadow: $focus-box-shadow;
}
~ .form-check-label {
color: $color;
}
}
}
.form-check-inline .form-check-input {
~ .#{$state}-feedback {
margin-left: .5em;
}
}
.input-group {
> .form-control:not(:focus),
> .form-select:not(:focus),
> .form-floating:not(:focus-within) {
@include form-validation-state-selector($state) {
@if $state == "valid" {
z-index: 3;
} @else if $state == "invalid" {
z-index: 4;
}
}
}
}
}
Sass 맵 (Sass maps)
이것은 _variables.scss의 검증 Sass 맵이에요. 다른 또는 추가 상태를 생성하려면 이를 재정의하거나 확장해요.
scss/_variables.scss
$form-validation-states: (
"valid": (
"color": var(--#{$prefix}form-valid-color),
"icon": $form-feedback-icon-valid,
"tooltip-color": #fff,
"tooltip-bg-color": var(--#{$prefix}success),
"focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity),
"border-color": var(--#{$prefix}form-valid-border-color),
),
"invalid": (
"color": var(--#{$prefix}form-invalid-color),
"icon": $form-feedback-icon-invalid,
"tooltip-color": #fff,
"tooltip-bg-color": var(--#{$prefix}danger),
"focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity),
"border-color": var(--#{$prefix}form-invalid-border-color),
)
);
$form-validation-states의 맵은 툴팁과 포커스 스타일을 재정의할 수 있는 세 가지 선택적 매개변수를 포함할 수 있어요.
Sass 루프 (Sass loops)
$form-validation-states 맵 값을 순회해 검증 스타일을 생성하는 데 사용돼요. 위의 Sass 맵에 대한 수정은 이 루프를 통해 컴파일된 CSS에 반영돼요.
scss/forms/_validation.scss
@each $state, $data in $form-validation-states {
@include form-validation-state($state, $data...);
}
커스터마이즈 (Customizing)
검증 상태는 $form-validation-states 맵으로 Sass를 통해 커스터마이즈할 수 있어요. _variables.scss 파일에 있는 이 Sass 맵은 기본 유효/무효 검증 상태를 생성하는 방법이에요. 각 상태의 색상, 아이콘, 툴팁 색상, 포커스 셰도우를 커스터마이즈하기 위한 중첩 맵이 포함돼 있어요. 브라우저는 다른 상태를 지원하지 않지만, 커스텀 스타일을 사용하는 사람들은 더 복잡한 폼 피드백을 쉽게 추가할 수 있어요.