웹 애플리케이션에서 사용자 경험을 향상시키고 데이터 무결성을 보장하기 위해 프런트엔드 유효성 검사는 필수적인 부분입니다. ThinkPHP 프레임워크는 강력한 모델 기반의 유효성 검사 기능을 제공하지만, 기본적으로 이 기능은 protected 접근 제어자로 보호되어 있어 컨트롤러나 뷰에서 직접 호출하기 어렵습니다. 이 문서에서는 ThinkPHP의 모델 유효성 검사 로직을 확장하고, 컨트롤러와 뷰를 통해 동적으로 필드 유효성을 검사하는 방법을 설명합니다.
모델 계층 코드
ThinkPHP의 Model 클래스에 내장된 유효성 검사 로직을 프런트엔드에서 활용하려면, 해당 기능을 public 메서드를 통해 노출해야 합니다. 이를 위해 Think\Model을 상속받는 기본 모델 클래스를 생성하고, 유효성 검사 메서드를 재정의하거나 래핑하여 접근할 수 있도록 합니다. 아래 예시는 Think\Model의 핵심 유효성 검사 로직을 applyValidationRules라는 공개 메서드로 캡슐화한 것입니다.
<?php
namespace App\Model; // 모델 네임스페이스를 App\Model로 변경
use Think\Model;
/**
* ThinkPHP 모델의 유효성 검사 기능을 확장하여 프런트엔드에서 활용 가능하도록 합니다.
*/
class CustomModel extends Model { // 클래스 이름을 CustomModel로 변경
/**
* 모델의 유효성 검사 규칙을 외부에서 호출할 수 있도록 공개합니다.
* ThinkPHP의 내부 유효성 검사 로직을 기반으로 합니다.
*
* @param array $inputData 검사할 데이터 배열
* @param int $validationScenario 유효성 검사 시나리오 (예: 1=추가, 2=수정, 3=항상)
* @return bool 유효성 검사 성공 여부
*/
public function applyValidationRules(array $inputData, int $validationScenario): bool { // 메서드 이름과 파라미터 변경
// 자동 유효성 검사가 비활성화된 경우 바로 true 반환
if (false === $this->options['validate']) {
return true;
}
$activeRules = [];
// 모델 옵션에 유효성 규칙이 정의되어 있거나, _validate 속성에 정의된 경우 사용
if (!empty($this->options['validate'])) {
$activeRules = $this->options['validate'];
unset($this->options['validate']); // 옵션에서 제거하여 의도치 않은 재사용 방지
} elseif (!empty($this->_validate)) {
$activeRules = $this->_validate;
}
// 유효성 검사 규칙이 정의된 경우 처리 시작
if (!empty($activeRules)) {
// 배치 유효성 검사 모드인 경우, 오류 메시지 배열 초기화
if ($this->patchValidate) {
$this->error = [];
}
foreach ($activeRules as $ruleConfig) {
// 규칙 정의 형식: array(필드명, 규칙, 메시지, 조건, 타입, 시나리오, 파라미터)
// 현재 시나리오($validationScenario)에 따라 유효성 검사 실행 여부 판단
$scenarioCheck = $ruleConfig[5] ?? null;
// 유효성 검사를 실행해야 하는 시나리오인지 확인
if (empty($scenarioCheck) || ($scenarioCheck == self::MODEL_BOTH && $validationScenario < 3) || $scenarioCheck == $validationScenario) {
// 메시지가 다국어 형식({%언어정의})인 경우 처리
if (isset($ruleConfig[2]) && 0 === strpos($ruleConfig[2], '{%') && strpos($ruleConfig[2], '}')) {
$ruleConfig[2] = L(substr($ruleConfig[2], 2, -1));
}
// 유효성 검사 조건과 타입을 설정 (기본값 사용)
$validateCondition = $ruleConfig[3] ?? self::EXISTS_VALIDATE;
$validateType = $ruleConfig[4] ?? 'regex';
// 유효성 검사 조건에 따른 처리 분기
switch ($validateCondition) {
case self::MUST_VALIDATE: // 필수 검사: 필드 유무와 상관없이 무조건 검사
if (false === $this->_validationField($inputData, $ruleConfig)) {
return false;
}
break;
case self::VALUE_VALIDATE: // 값이 비어있지 않을 때만 검사
if (isset($inputData[$ruleConfig[0]]) && '' !== trim($inputData[$ruleConfig[0]])) {
if (false === $this->_validationField($inputData, $ruleConfig)) {
return false;
}
}
break;
default: // 기본값: 필드가 존재하면 검사
if (isset($inputData[$ruleConfig[0]])) {
if (false === $this->_validationField($inputData, $ruleConfig)) {
return false;
}
}
}
}
}
// 배치 유효성 검사 모드인 경우, 수집된 오류가 있으면 false 반환
if (!empty($this->error)) {
return false;
}
}
return true;
}
}
컨트롤러 계층 코드
컨트롤러는 프런트엔드에서 전송된 AJAX 요청을 처리하고, 확장된 모델의 유효성 검사 메서드를 호출하는 역할을 합니다. 일반적으로 공통 컨트롤러(예: BaseController)에 해당 메서드를 구현하고, 다른 컨트롤러들이 이를 상속받아 사용합니다. 특히 데이터 수정 시 고유성(unique) 검사를 올바르게 수행하려면 해당 레코드의 주 키(Primary Key) ID를 함께 전달해야 합니다.
<?php
// 다른 네임스페이스 및 use 구문...
use App\Model\CustomModel; // 변경된 모델 네임스페이스를 사용
use Think\Controller; // ThinkPHP Controller
class BaseController extends Controller { // 클래스 이름을 BaseController로 변경
/**
* AJAX 요청을 통해 개별 필드의 유효성을 검사합니다.
* 데이터 수정 시 고유성 검사를 위해 레코드 ID를 전달해야 합니다.
*/
public function validateFieldAjax(): void { // 메서드 이름 변경
$inputFieldName = I('fieldName'); // 요청된 필드 이름
$fieldCurrentValue = I('fieldValue'); // 요청된 필드 값
$recordId = I('recordId'); // 수정 시 필요한 레코드 ID
// 모델 이름 결정: URL에서 'model' 파라미터가 있으면 사용, 없으면 현재 컨트롤러 이름 사용
$modelName = I('model') ?: ucfirst(CONTROLLER_NAME);
/** @var CustomModel $targetModel */ // IDE를 위한 타입 힌트
$targetModel = D($modelName); // 모델 인스턴스 생성
$validationData = [$inputFieldName => $fieldCurrentValue];
if ($recordId) {
// 수정 모드인 경우, 고유성(unique) 검사를 위해 ID 포함
$validationData['id'] = $recordId;
}
// 시나리오 3 (MODEL_BOTH)으로 유효성 검사 실행
$validationResult = $targetModel->applyValidationRules($validationData, 3); // 변경된 메서드 이름 호출
if (!$validationResult) {
// 유효성 검사 실패 시 오류 메시지와 함께 JSON 응답 반환
$this->ajaxReturn(['status' => 'error', 'message' => $targetModel->getError()]); // 'msg'를 'message'로 변경
} else {
// 유효성 검사 성공 시 성공 메시지와 함께 JSON 응답 반환
$this->ajaxReturn(['status' => 'ok', 'message' => '유효성 검사 성공']);
}
}
}
뷰 계층 코드
뷰 계층에서는 사용자가 폼 필드에서 포커스를 잃었을 때(blur 이벤트) AJAX 요청을 비동기적으로 전송하여 서버 측 유효성 검사를 트리거합니다. 검사 결과에 따라 해당 필드 옆에 성공 또는 오류 메시지를 실시간으로 표시하여 사용자에게 즉각적인 피드백을 제공합니다.
<script>
// 모든 <input> 요소에 대해 포커스를 잃었을 때(blur 이벤트) 유효성 검사 함수 실행
$("input[name]").blur(function () { // 'name' 속성을 가진 <input> 요소에 바인딩
var currentField = $(this); // 현재 포커스를 잃은 입력 필드
var fieldName = currentField.attr('name'); // 필드 이름
var fieldValue = currentField.val(); // 필드 현재 값
var recordId = $("input[name='id']").val(); // 수정 시 필요한 레코드 ID (폼에 'id' 필드가 있다고 가정)
var requestData = {
fieldName: fieldName,
fieldValue: fieldValue
};
if (recordId) {
requestData.recordId = recordId; // 수정 시나리오를 위해 recordId 포함
}
// AJAX 요청 전송
$.ajax({
type: 'POST',
url: '{:U("Base/validateFieldAjax")}', // 컨트롤러의 validateFieldAjax 메서드 URL 지정
data: requestData,
success: function (response) { // 서버 응답 처리
// 메시지를 표시할 컨테이너 (예: layui-word-aux 클래스를 가진 형제 요소)
var messageContainer = currentField.parent().siblings('.layui-word-aux');
if (response.status === 'error') {
// 유효성 검사 실패 시 오류 아이콘과 메시지 표시
messageContainer.html("<i class='iconfont' style='color:red;'></i> " + response.message);
} else {
// 유효성 검사 성공 시 성공 아이콘 표시
messageContainer.html("<i class='iconfont' style='color:green;'></i>");
}
},
error: function() {
// AJAX 요청 자체 실패 시 처리 (네트워크 오류 등)
currentField.parent().siblings('.layui-word-aux').html("<i class='iconfont' style='color:red;'></i> 서버 통신 오류 발생");
}
});
});
</script>