이전 포스트에서 만든 validate
코드의 리팩토링을 진행할 것이다. 이 프로젝트에서 validate
는 새로운 학생, 강사, 그룹이 추가될 때마다 각각의 입력값이 유효한지 검사해주는 컴포넌트였다. 학생, 강사, 그룹의 Container
가 모두 validate
를 가지고 있을 필요가 없기 때문에 공용으로 사용할 수 있도록 Components
폴더로 이동시키고자 한다.
또, 입력값이 없는 경우와 전화번호 검사의 경우 학생과 강사 모두가 같은 로직을 사용하므로 나뉘어져 있을 필요가 없다. 이것도 역시 한번에 검사할 수 있도록 코드를 개선할 것이다.
Components
ㄴ GlobalStyles.js
ㄴ Menu.js
ㄴ Modal.js
ㄴ Router.js
Routes
ㄴ GroupModify
ㄴ index.js
ㄴ GroupModifyPresenter.js
ㄴ GroupModifyContainer.js
ㄴ StudentModify
ㄴ index.js
ㄴ StudentModifyPresenter.js
ㄴ StudentModifyContainer.js
ㄴ TeacherModify
ㄴ index.js
ㄴ TeacherModifyPresenter.js
ㄴ TeacherModifyContainer.js
재사용하는 컴포넌트는 Components
폴더에, 실제 페이지를 구성하는 코드는 Routes
폴더에서 Container-Presenter Pattern
으로 보관한다.
validate
가 StudentModifyContainer
, TeacherModifyContainer
, GroupModifyContainer
세 파일 안에 모두 들어있는 상태이다. 각각의 파일은 아래 사진의 모달을 구현한 코드이며, validate는 입력값이 유효한지 검사한 후 유효하지 않다면 빨간색 에러 메시지를 띄워주는 역할을 한다.
const [errors, setErrors] = useState(new Object());
const StudentModifyContainer = () => {
const validate = () => {
const temp_errors = {};
const regPhone = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/;
const newStudent = {
name: inputName.current.value,
school: inputSchool.current.value,
phone: inputPhone.current.value,
parents: inputParents.current.value,
category,
grade,
subjects,
officeHours
};
for (var key in newStudent) {
// Input값이 없는 경우
if (newStudent[key].length === 0 || newStudent[key] === undefined) {
if (key === 'name') {
temp_errors[key] = '학생 이름을 입력해주세요.';
} else if (key === 'school') {
temp_errors[key] = '학교를 입력해주세요.';
} else if (key === 'phone') {
temp_errors[key] = '학생 연락처를 입력해주세요.';
} else if (key === 'parents') {
temp_errors[key] = '부모님 연락처를 입력해주세요.';
} else if (key === 'category') {
temp_errors[key] = '계열을 선택해주세요.';
} else if (key === 'grade') {
temp_errors[key] = '학년을 선택해주세요.';
} else if (key === 'subjects') {
temp_errors[key] = '수강과목을 선택해주세요.';
} else if (key === 'officeHours') {
temp_errors[key] = '등원요일을 선택해주세요.';
}
}
// 과목을 추가했는데 선호강사를 선택 안한 경우
if (key === 'subjects') {
for (var i = 0; i < newStudent[key].length; i++) {
if (!newStudent[key][i].teacher) {
temp_errors[key] = newStudent[key][i].name + '과목의 선호강사를 선택해주세요.';
}
}
}
// 전화번호가 입력되었다면, 전화번호 형식을 검사
if (key === 'phone' && newStudent[key].length != 0 && regPhone.test(newStudent[key]) === false) {
temp_errors[key] = '학생 연락처가 형식에 맞지 않습니다.';
}
if (key === 'parents' && newStudent[key].length != 0 && regPhone.test(newStudent[key]) === false) {
temp_errors[key] = '부모님 연락처가 형식에 맞지 않습니다.';
}
}
// 에러가 없으면 handleSubmit
if (Object.keys(temp_errors).length === 0 && temp_errors.constructor === Object) {
handleSubmit(newStudent);
} else {
setErrors({ ...temp_errors });
}
};
const handleSubmit = (newStudent) => {
// isModify가 true이면 수정, false이면 새 학생 등록
HandleConfirm('저장하시겠습니까?', () => (isModify ? updateStudent(newStudent) : createStudent(newStudent)), null);
}
return <StudentModifyPresenter errors={errors} validate={validate}/>
}
const StudentPresenter = ({errors, validate}) => {
// 전략
// errors가 비어 있다면 null을,
// 비어 있지 않다면(error가 있다면) errors의 key를 반복문으로 돌면서 화면에 에러메시지 출력
{ Object.keys(errors).length === 0
? null
: Object.keys(errors).map((key, index)=>(
<ModalFormError> <i class="fas fa-exclamation-circle"></i> {errors[key]} </ModalFormError>
))
}
// 클릭 시 validate 호출
<SubmitButton onClick={validate} value="학생등록"> 저장하기 </SubmitButton>
}
Student, Teacher, Group의 세 가지 유저가 이 코드를 각각 가지고 있었다. 각 Container
별로 담당(학생, 강사, 그룹)이 다르므로 안의 검사 내용과 if 조건식의 key는 달라질 수 있지만, 빈 input과 전화번호 형식 검사를 수행한다는 것이 동일하다. 동일한 작업을 수행하는 코드가 세 군데에 존재할 필요가 없으므로, Component
화할 것이다.
Validate
라는 새 컴포넌트를 만들고, (1) 유효성 검사를 요청한 유저(Student, Teacher, Group 중 어디서 검사요청이 왔는지)와 (2) 새로 등록할 객체를 받을 것이다.
1-1. const Validate = (name,object) => { }
의 형태가 되겠다.
1-2. (1)는 동일한 이름의 key에 대해서, 특정 유저만 유효 조건이 다른 경우를 처리하기 위함이다.
(2)에서 받아온 객체의 값이 유효한지 검사를 수행한다.
2-1. 유효하지 않다면 temp_errors
에 에러내용을 저장한다.
모든 검사를 마친 후 temp_errors
가 비어 있다면 true
를 리턴하고, 비어 있지 않다면 temp_errors
를 리턴한다.
각 Container
들은 새 값을 submit하기 전에 Validate
를 호출해 검사한다.
4-1. Validate
의 리턴값이 true
라면 정상적으로 등록한다.
4-2. 아니라면 errors
에 저장한다.
세 유저가 동일한 Key를 사용하는 경우가 있으므로 코드 단축을 기대할 수 있겠다.
Components
폴더 아래에 Validate.js
라는 새 파일을 생성한다. 각각의 Container
로부터 새로 등록할 객체(newStudent
, newTeacher
, newGroup
)를 object
라는 이름으로 받아올 것이다.
또, 어떤 대상에 관한 것인지(학생인지 강사인지 그룹인지) 알기 위해 name
이라는 인자로 받아올 것이다. 그리고 전화번호를 검사하는 정규식 regPhone
과 에러를 저장할 temp_errors
객체를 선언해준다.
// Components/Validate.js
const Validate = (name, object) => {
const temp_errors = {};
const regPhone = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/;
}
export default Validate;
// name은 "Student","Teacher","Group"으로 들어온다. 어떤 모달의 에러를 다룰 것인지 알기 위함
// object는 새로운 학생,강사,그룹 객체
const Validate = (name, object) => {
const temp_errors = {};
const regPhone = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/;
for(var key in object){
// 입력값이 없는 경우
if(object[key] === undefined || object[key].length === 0){
// 학생 & 강사 & 그룹 공용
if (key === 'name') {
temp_errors[key] = '이름을 입력해주세요.';
} else if (key === 'phone') {
temp_errors[key] = '연락처를 입력해주세요.';
} else if (key === 'officeHours') {
temp_errors[key] = '요일을 선택해주세요.';
}
// 학생
else if (key === 'school') {
temp_errors[key] = '학교를 입력해주세요.';
} else if (key === 'parents') {
temp_errors[key] = '부모님 연락처를 입력해주세요.';
} else if (key === 'grade') {
temp_errors[key] = '학년을 선택해주세요.';
} else if (key === 'category') {
temp_errors[key] = '계열을 선택해주세요.';
} else if (key === 'subjects') {
temp_errors[key] = '수강과목을 선택해주세요.';
}
// 강사
else if ( key === 'subject') {
temp_errors[key] = '담당과목을 선택해주세요.';
}
// 그룹
else if (key === "teacher"){
temp_errors[key] = '강사를 선택해주세요.';
} else if (key === 'students'){
temp_errors[key] = '학생을 선택해주세요.';
}
}
// 전화번호 검사
if (key === 'phone' && object[key].length != 0 && regPhone.test(object[key]) === false) {
temp_errors[key] = '연락처가 형식에 맞지 않습니다.';
}
if (key === 'parents' && object[key].length != 0 && regPhone.test(object[key]) === false) {
temp_errors[key] = '부모님 연락처가 형식에 맞지 않습니다.';
}
// 학생의 수강과목을 추가했는데 선호강사를 선택 안한 경우
if (key === 'subjects') {
for (var i = 0; i < object[key].length; i++) {
if (!object[key][i].teacher) {
temp_errors[key] = object[key][i].name + '과목의 선호강사를 선택해주세요.';
}
}
}
// 그룹수업의 요일이 선택되었는데 시간은 선택 안한 경우
if (key === 'officeHours' && name === "Group") {
for (var i = 0; i < object[key].length; i++) {
if (object[key].length != 0 && object[key][i].times.length === 0) {
temp_errors[key] = object[key][i].day + '요일의 수업시간을 선택해주세요.';
}
}
}
}
// temp_error가 비어 있다면 true를,
// 비어 있지 않다면 temp_errors를 리턴한다.
if (Object.keys(temp_errors).length === 0 && temp_errors.constructor === Object) {
return true;
} else {
return temp_errors;
}
}
export default Validate;
import Validate from 'Components/Validate';
const StudentModifyContainer = () => {
const handleSubmit = () => {
const newStudent = {
name: inputName.current.value,
school: inputSchool.current.value,
phone: inputPhone.current.value,
parents: inputParents.current.value,
category,
grade,
subjects,
officeHours
};
if (isModify) newStudent._id = student._id;
// Validate검사의 리턴값을 result에 저장하고
var result = Validate("Student", newStudent)
// result가 true면, 즉 모든 값이 유효하면 newStudent를 저장해주고
// 아니라면 result에는 temp_errors가 담겨 있을 것이므로, errors에 저장
if(result === true){
HandleConfirm('저장하시겠습니까?', () => (isModify ? updateStudent(newStudent) : createStudent(newStudent)), null);
} else {
setErrors({...result});
}
};
}
const StudentPresenter = ({errors}) => {
// 전략
// errors가 비어 있다면 null을,
// 비어 있지 않다면(error가 있다면) errors의 key를 반복문으로 돌면서 화면에 에러메시지 출력
{ Object.keys(errors).length === 0
? null
: Object.keys(errors).map((key, index)=>(
<ModalFormError> <i class="fas fa-exclamation-circle"></i> {errors[key]} </ModalFormError>
))
}
// 클릭 시 validate 호출
<SubmitButton onClick={handleSubmit} value="학생등록"> 저장하기 </SubmitButton>
}
저장 버튼을 클릭했을 때 validate
가 아니라 handleSubmit
으로 바꿔주는 것을 잊지 말자. Container
의 handleSubmit
에서 Validate
를 호출해줄 것이다.
이 작업을 Group과 Teacher에도 동일하게 해준다. 각각 가지고 있던 validate
코드를 삭제하고, Components
에 있는 Validate
를 import해서 사용한다. 이렇게 하면 새 유저가 추가되어 input 유효성 검사를 새롭게 해야 할 때에도 Validate
의 조건문만 추가해 주면 된다. 확장에 유연한 코드로 개선되었다!👍
리액트 코드의 재사용성을 높이는 리팩토링, 제목은 참 거창하지만 한 것은 별로 없는 것 같아 부끄럽다. 리팩토링의 중요성을 인지하고는 있었지만 막상 끝나면 다시 들여다보기 귀찮고 번거로워서 방치해 둔 프로젝트가 몇 있다. 그런 녀석들을 취업 포트폴리오로 사용하려면 안 보여주느니만 못하다는 생각이 들었다. 나의 발전과 취업에 도움이 되었으면 하는 바람으로 지저분하게 짜여진 코드를 정리하고, 리팩토링 경험을 기록해 보았다. 지금 코드 상태는 오늘 한 작업만으로는 당연히 부족하다(ㅠㅠ). 앞으로 작업하고, 또 블로그에 기록할 내용들을 정리해 보면서 포스팅을 마친다.
페이지별로 공용으로 사용하던 Component들을 한 곳에 모아서 재사용할 수 있도록 코드를 개선할 것이다. 같은 디자인을 공유하는 페이지(예를 들면 똑같은 메뉴바와 모달을 두가지 페이지에서 사용하는 경우)가 많은데, 각각의 페이지에서 같은 컴포넌트를 가지고 있는 상태에서 한 파일에 모여있는 컴포넌트를 가져다 쓸 수 있도록 개선할 것이다.
스타일 컴포넌트 역시 개선할 예정이다. 디자인은 같은데 색깔과 아이콘만 다른 버튼들을 GlobalStyles
와 스타일 컴포넌트의 상속을 이용해 리팩토링 한다.