[React Performance Optimization] AutomationSetting

김병진·2021년 3월 26일
0
post-thumbnail

AutomationSetting

이번에는 자동화 설정 컴포넌트입니다. Material-ui 라이브러리의 RangeSlider를 가져와 범위를 조절하는 컴포넌트입니다. 사용 예제는 아래와 같습니다.

ex

슬라이더를 움직이면 바로 아래 칩의 내용도 바뀌는 구조입니다.

RESULT

자동화 설정에서 슬라이더의 값을 10단계 움직여봤습니다.

  • 최적화 전 프로파일링
    beofre
  • 최적화 후 프로파일링
    after

0.1ms 이하의 커밋은 제외했습니다.

BeforeAfterComp.
Commits19 steps15 stepsx0.42
Largest Time3.6ms2.5msx1.4

이번 최적화에는 큰 최적화를 이루지 못했습니다. 그래서 이번에는 리팩터링 관점에서 코드를 개선해볼까합니다.

HOW

  1. Edit if Condition to Class
  2. Way How to Dispatch Slider Values

WHY

  1. Edit if Condition to Class

리팩터링 관련 책이나 클린 코드에도 나오는 필수 코스입니다. if문과 for문을 꺼리는 경향이 있습니다. for문은 파이프를 이용해서 처리하고 if문은 다음과 같이 다형성을 이용하여 기피합니다.

  • before
const getOffExplanation = (_type) => {
    if(_type === "cooler") {
      return `끄기 : ${valuetext(setting[0])}`
    } else if (_type === "heater" || _type === "led") {
      return `끄기 : ${valuetext(setting[1])}`
    }
  }
  • after
  function getOffExplanation <T extends string> (machine: T) {
    return new RangeExplanationChip(machine, automation).isCoolerException()  		
      ? new CoolerExplanationChip(machine, automation).offText()
      : new RangeExplanationChip(machine, automation).offText();
  }

코드가 보기 좋아진 것 같습니다. 개편 전 코드는 읽기 거부감이 많이 듭니다. 개편 후 가독성이 좋아졌습니다. 또 다른 예를 보도록 하겠습니다. 지저분한 조건문은 최대한 숨겨주도록 합니다.

  • before
    if (machine === "cooler" && status && switches['heater']){
      return;
    }
    else if(machine === "heater" && status && switches['cooler']){
      return;
    }
  • after
    if ( machineClass instanceof TemperatureRangeMachine 
        && machineClass.isAirconditionerConflicted(status)){
      return;
    }

리팩터링 저자 또한 가독성이 너무 떨어지는 순간에는 성능보다 가독성을 더 우선시한다고 합니다. 개편 전 코드는 다시 읽어봐도 이해하기 쉽지 않지만 개편 후 코드는 온도 관련 기계이고 에어컨이 어떤 이유인지 충돌하게 된다면 return하게 된다는 의미를 대충 이해 가능하다고 생각합니다.

조건문의 상태를 하드 코딩하게 된다면 개발자 본인만 이해 가능한 경우가 많을 것입니다. 적절한 이름을 지닌 클래스 혹은 메서드를 생성해 대입해준다면 가독성이 향상될 것입니다.


2. Way How to Dispatch Slider Values

보통 부모 컴포넌트에서 자식 컴포넌트로 함수를 내려주어 부모 컴포넌트에서 정의한 함수가 자식 컴포넌트에서 실행되는 구조는 많이 보셨을 것입니다. 이 구조를 리액트에서 지향합니다. 하지만 때때로 반대의 구조를 원할 때가 있습니다. 즉, 자식 컴포넌트에서 정의한 함수를 부모 컴포넌트에서 실행하는 것입니다. 사실 리액트에서 좋아하는 구조는 아닙니다. 종종 필요한 경우가 있어서 이번에 리액트가 만들어놓은 hooks를 통해 구현하려 합니다.

주로 범위를 가진 RangeSlider 컴포넌트를 가진다면 범위값이 변할 때마다 dispatch 해주는 예제 코드들은 인터넷에 많이 있습니다. 물론 예제로서의 일을 충분히 했지만 그대로 갖다 쓰면 성능에는 쥐약일 수 있습니다. 그래서 useRef와 forwardRef + useImperativeHandle를 이용해 설정값의 변화를 다 마치고 자동화 설정 다음 단계로 넘어갈 때에만 자식 컴포넌트에서 dispatch를 통해 데이터를 저장합니다.

|___ CustomStepper ( 다음 혹은 저장 버튼 존재 )
    |___ RangeSlider ( 저장할 데이터값이 존재 )
    |___ TimeSpanPickerWrapper ( 저장할 데이터값이 존재 )

다른 생각을 가진 분들도 계실 것입니다. RangeSlider에 다음 버튼을 두면 이 컴포넌트에서 다 할 수 있는 것 아니냐? 물론 가능합니다. 그럼 state로 설정한 범위값이 움직일 때마다 필요없는 다음 버튼이 리렌더링될 것이며 RangeSlider 컴포넌트는 범위값을 조절하는 슬라이더로서의 본질이 흐려집니다.

자동화 설정 스텝퍼에 다음 및 저장 버튼이 있습니다. RangeSlider과 TimeSpanPickerWrapper 에 있는 다음이나 저장 버튼을 누를 경우 데이터가 dispatch 되어 저장됩니다. 문제는 스텝퍼 컴포넌트에 저장하고자 하는 데이터가 존재하지 않고 그 자식 컴포넌트인 RangeSlider 혹은 TimeSpanPickerWrapper 컴포넌트에 존재합니다. 이런 경우 필요한 것이 useRef와 forwardRef + useImperativeHandle 입니다. 부모 컴포넌트가 정해놓은 명령적인 함수의 선언을 자식 컴포넌트의 ref 태그에 넣어 내보냅니다.

// 부모 컴포넌트
function handleNext () {
  setActiveStep((prevActiveStep) => ++prevActiveStep );
  if ( nextButtonRef.current !== null ) { 
    nextButtonRef.current.handleNextStep(); 
  }
}
// 자식 컴포넌트
const RangeSlider = React.forwardRef((
  { position: machine }: RangeSliderProps, 
  ref?: React.Ref<TaskNextButtonRef> 
) => {
  ...
  useImperativeHandle(ref, () => ({
      handleNextStep () {
        dispatch( controlAutomation(update(singleAutomation, {
          start: { $set: [ automation[0] ] },
          end: { $set: [ automation[1] ] },
        }) as ReducerAutomationDto ) );
      }
    }))
}

자식 컴포넌트는 위와 같은 useImperativeHandle을 이용하여 부모 컴포넌트인 스텝퍼에서 다음 버튼을 누를 시 handleNextStep 함수를 실행하게 됩니다.

useRef는 컴포넌트 스코프 변수를 만들고 변수값이 렌더링에 영향을 주지 않습니다. 즉, 리렌더링되어도 함수가 재선언되지 않습니다. 자식 컴포넌트에 내려주어도 ref로 내려간 함수나 값은 부모 컴포넌트의 리렌더링이 자식 컴포넌트에게 영향을 주지 않습니다. useCallback은 deps를 통해 재선언 조건이 있지만 useRef는 없습니다.

CONCLUSION

이번 자동화 설정 컴포넌트에는 성능 최적화보다 리팩터링 위주의 글이 되어버렸습니다. 성능만큼 중요한 것 또한 리팩터링을 통한 가독성 향상이라고 생각합니다. 개편 전 코드들이 너무 개판이어서 리팩터링할 것들이 많았습니다.

profile
아이디어를 구현할 수 있는 개발자가 목표입니다.

0개의 댓글