리액트 코드의 재사용성을 높이는 리팩토링

hyun·2021년 11월 23일
1

React

목록 보기
3/3
post-thumbnail

이 포스트에서 다룰 내용

이전 포스트에서 만든 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으로 보관한다.

validateStudentModifyContainer, TeacherModifyContainer, GroupModifyContainer 세 파일 안에 모두 들어있는 상태이다. 각각의 파일은 아래 사진의 모달을 구현한 코드이며, validate는 입력값이 유효한지 검사한 후 유효하지 않다면 빨간색 에러 메시지를 띄워주는 역할을 한다.

StudentModify

TeacherModify

GroupModify

기존 StudentModifyContainer.js

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}/> 
}

기존 StudentPresenter.js

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> &nbsp; {errors[key]} </ModalFormError>  
  )) 
  }
  // 클릭 시 validate 호출
  <SubmitButton onClick={validate} value="학생등록"> 저장하기 </SubmitButton>
}

Student, Teacher, Group의 세 가지 유저가 이 코드를 각각 가지고 있었다. 각 Container별로 담당(학생, 강사, 그룹)이 다르므로 안의 검사 내용과 if 조건식의 key는 달라질 수 있지만, 빈 input과 전화번호 형식 검사를 수행한다는 것이 동일하다. 동일한 작업을 수행하는 코드가 세 군데에 존재할 필요가 없으므로, Component 화할 것이다.

설계

  1. Validate 라는 새 컴포넌트를 만들고, (1) 유효성 검사를 요청한 유저(Student, Teacher, Group 중 어디서 검사요청이 왔는지)(2) 새로 등록할 객체를 받을 것이다.
    1-1. const Validate = (name,object) => { }의 형태가 되겠다.
    1-2. (1)는 동일한 이름의 key에 대해서, 특정 유저만 유효 조건이 다른 경우를 처리하기 위함이다.

  2. (2)에서 받아온 객체의 값이 유효한지 검사를 수행한다.
    2-1. 유효하지 않다면 temp_errors에 에러내용을 저장한다.

  3. 모든 검사를 마친 후 temp_errors가 비어 있다면 true를 리턴하고, 비어 있지 않다면 temp_errors를 리턴한다.

  4. 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;

구현

Components/Validate.js

// 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;

개선된 StudentModifyContainer.js

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});
    }
  };
}

개선된 StudentModifyPresenter.js

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> &nbsp; {errors[key]} </ModalFormError>  
  )) 
  }
  // 클릭 시 validate 호출
  <SubmitButton onClick={handleSubmit} value="학생등록"> 저장하기 </SubmitButton>
}

저장 버튼을 클릭했을 때 validate가 아니라 handleSubmit으로 바꿔주는 것을 잊지 말자. ContainerhandleSubmit에서 Validate를 호출해줄 것이다.

Group, Teacher

이 작업을 Group과 Teacher에도 동일하게 해준다. 각각 가지고 있던 validate 코드를 삭제하고, Components에 있는 Validate를 import해서 사용한다. 이렇게 하면 새 유저가 추가되어 input 유효성 검사를 새롭게 해야 할 때에도 Validate의 조건문만 추가해 주면 된다. 확장에 유연한 코드로 개선되었다!👍

마치며

리액트 코드의 재사용성을 높이는 리팩토링, 제목은 참 거창하지만 한 것은 별로 없는 것 같아 부끄럽다. 리팩토링의 중요성을 인지하고는 있었지만 막상 끝나면 다시 들여다보기 귀찮고 번거로워서 방치해 둔 프로젝트가 몇 있다. 그런 녀석들을 취업 포트폴리오로 사용하려면 안 보여주느니만 못하다는 생각이 들었다. 나의 발전과 취업에 도움이 되었으면 하는 바람으로 지저분하게 짜여진 코드를 정리하고, 리팩토링 경험을 기록해 보았다. 지금 코드 상태는 오늘 한 작업만으로는 당연히 부족하다(ㅠㅠ). 앞으로 작업하고, 또 블로그에 기록할 내용들을 정리해 보면서 포스팅을 마친다.

  • 페이지별로 공용으로 사용하던 Component들을 한 곳에 모아서 재사용할 수 있도록 코드를 개선할 것이다. 같은 디자인을 공유하는 페이지(예를 들면 똑같은 메뉴바와 모달을 두가지 페이지에서 사용하는 경우)가 많은데, 각각의 페이지에서 같은 컴포넌트를 가지고 있는 상태에서 한 파일에 모여있는 컴포넌트를 가져다 쓸 수 있도록 개선할 것이다.

  • 스타일 컴포넌트 역시 개선할 예정이다. 디자인은 같은데 색깔과 아이콘만 다른 버튼들을 GlobalStyles와 스타일 컴포넌트의 상속을 이용해 리팩토링 한다.

profile
프론트엔드를 공부하고 있습니다.

0개의 댓글