[이마고웍스] 리렌더링으로 인한 버튼 이벤트 ignore 이슈 해결 과정

Ji-Heon Park·2023년 6월 9일
6

Imagoworks

목록 보기
1/10

Imagoworks의 Cloud Team은 서비스 전반을 설계하고 개발하며 운영 및 관리하는 팀입니다. 이마고웍스 입사 초기에 팀의 일원으로서 버그픽스 및 개인 테스크 작업을 진행했습니다.

가장 먼저 이마고웍스 디자인시스템 고도화테스크를 맡았습니다. 내용은 디자인 시스템 내 버튼류 컴포넌트들의 onClick 이벤트가 가장 우선순위로 동작할 수 있도록 변경하는 작업이었습니다. 이유는 회원가입/로그인 폼에서 textfield의 유효성 검증을 하는 onBlur이벤트에 의해 onClick이벤트가 작동하지 않는 이슈가 있었습니다.

시나리오

  1. Action

    • imago-accounts/client 브라우저 접속
    • 폼/계정 작성
    • 폼/비밀번호 작성
    • 로그인 버튼 클릭
  2. Expected Result

    • 로그인
  3. Actual Result

    • 버튼 이벤트 발생 x
  4. Description

    • 로그인 버튼 두번 클릭 or 폼 외부 클릭 후 버튼 누르면 정상 작동
    • input에 포커싱 되어 있는 경우 버튼 클릭이 안됨

Try 1. 이벤트의 고유 순서를 이용한 순서 변경

이벤트 순서는 테스트 내용에 따르면 위와 같습니다. onMouseDown이 가장 우선 순위이므로 onClick대신 사용하여 onBlur보다 먼저 이벤트를 작동시킬 수 있습니다.

하지만 두 이벤트는 서로 다른 기대치를 갖는 근본적으로 다른 이벤트이기에 바꿔서 사용하는 것이 권장되지 않습니다. 소프트웨어 예측 가능성 손상되기 때문입니다.

<TextField
  onBlur={() => {
    console.log('TextField onBlur');
  }}
/>
<Button
  type="button"
  onClick={() => {
    console.log('Button onClick');
  }}
  onMouseDown={() => {
    console.log('Button onMouseDown');
  }}
>
 onMouseDown Button
</Button>


// ---------- console result ----------
// 1. Button onMouseDown
// 2. TextField onBlur
// 3. Button onClick

Try 2. 이벤트 작동 제어

클릭 이외 다른 이벤트의 동작을 막습니다. 문제가 되는 onBlur의 이벤트를 막아 버튼 이벤트가 정상 작동을 합니다. 그러나 순서를 조작하는 것이 아닌 작동 자체를 막는것이기에 적합하지 않습니다.

<Button
  id={'loginBtn'}
  type="submit"
  onClick={() => {
    console.log('onClick');
  }}
  onMouseDown={(e: any) => {
    e.preventDefault();
  }}
>

이벤트 순서와 관련하여 위 두가지 방법을 시도하였으나 몇가지 의문이 들었습니다

  • 이벤트의 고유 순서를 임의로 조작하는 것이 과연 올바른 해결법인가?
  • 이벤트 순서상 onBlur가 우선 순위이기는 하지만 후순위의 이벤트가 작동은 하는 것이 정상적임, 그러나 문제 코드는 버튼의 이벤트 자체가 동작하지 않는다. 그렇다면 이벤트 순서의 문제가 아니지 않을까?
  • 문제 상황은 네스팅 된 상황이 아닙니다. 즉, 처음 테스크 내용과 달리 이벤트 버블링이나 캡처링의 문제가 아닙니다.

문제에 대한 고찰

이벤트 순서의 문제가 아니라고 판단 후 문제 코드를 분석해보았습니다.

이마고웍스의 로그인 폼은 input 작성 후 버튼 클릭을 하면 onBlur이벤트(유효성검증)가 먼저 작동하고 버튼 클릭 이벤트(로그인)가 작동해야 합니다. 그 중 onBlur 이벤트는 다음과 같습니다.

const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
    const id = e.target.id;
    if (id === EMAIL) {
      setValue(id, watch(id)?.trim());
    }
    trigger(id);
    clearError();
};

위 코드 중 setValue, trigger, clearError는 모두 리액트의 state를 변화시키는 훅입니다. React Devtool로 확인 결과 폼 전체가 리렌더링되는 것을 확인하였습니다.

즉 onBlur 이벤트로 컴포넌트가 리렌더링 되며 클릭 이벤트가 작동해야할 버튼이 사라진것입니다.

리렌더링?

리액트에서 렌더링이란, 컴포넌트가 현재 props와 state의 상태에 기초하여 UI를 어떻게 구성할지 컴포넌트에게 요청하는 작업을 의미합니다. 리렌더링이 되는 상황은 아래와 같습니다.

  • state 업데이트
  • props 업데이트
  • 부모컴포넌트 렌더링
  • shouldComponentUpdate에서 true가 반환
  • forceUpdate가 실행될 때

Try 3. RHF 훅 변경

이마고웍스의 폼은 모두 RHF로 구현되어 있습니다. 이 중 RHF의 리렌더링을 최소화하는 훅으로 변경하였습니다. 그러나 모든 훅에 대체재가 있지 않아 리렌더링을 막을 수 없었습니다.

  • Consider using useWatch instead of watch to localize rerenders at the component level where the value actually needs to watched.
  • The use of useFormState instead of useFormContext to get only the form's state instead of all it's methods.
  • Use getValues to retrieve some form value instead of watch to avoid subscribing (aka. rerendering) to form values.
  • Use reset when you need to manually set most of the forms values instead of setValue which sets them one by one.
  • Consider uncontrolled inputs via register instead of Controller / useController to avoid updating the forms state
    https://github.com/orgs/react-hook-form/discussions/7611

Try 4. Memoization

버튼 컴포넌트를 메모이제이션하여 리렌더링을 막을 수 있지 않을까하는 생각이 떠올랐습니다. React.memo의 두번째 인자로 비교 함수를 커스텀할 수 있습니다.

객체의 깊은 수준 비교를 위해 JSON.stringify 사용했습니다. 이유는 아래와 같습니다.

  • Object.assign() 중첩 객체 비교x
  • Spread Operator 중첩 객체 비교x
  • lodash isEqual() 중첩 객체 비교 o, 함수 비교 o
  • JSON 객체 메서드 중첩 객체 비교 o, 함수 비교 x → 함수를 처리하지 못하기에 문제 상황에서 적합
const MemoizedButton = React.memo(Button, (prevProps, nextProps) => {
  const copiedPrevProps = { ...prevProps };
  const copiedNextProps = { ...nextProps };
  
  delete copiedPrevProps['children'];
  delete copiedNextProps['children'];
  
  return JSON.stringify(copiedPrevProps) === JSON.stringify(copiedNextProps);
});

위 메모이제이션 버튼을 사용하여 버튼 컴포넌트의 리렌더링을 막은 결과 onBlur -> onClick의 이벤트가 정상적으로 작동합니다.

아쉬운 점

You should only rely on memo as a performance optimization. If your code doesn’t work without it, find the underlying problem and fix it first. Then you may add memo to improve performance.

당장의 이슈는 메모이제이션으로 해결했습니다. 하지만 공식문서에도 나와있듯이 React.memo()는 성능 최적화를 위함이지 렌더링을 막기위해서 사용하는 것이 권장되지 않습니다. 본질적인 문제 해결을 위해서는 유효성검증과 Submit이 이루어지는 로그인/회원가입 폼의 로직 자체를 수정해야 할 것입니다.

입사 한달이 지난 지금 클라우드 프론트엔드 팀에서 최적화 + 코드 리팩토링 업무를 맡게 되었습니다. 입사 한달차 신입이지만 자유롭게 아이디어를 제시하면 자세한 피드백과 함께 믿고 맡겨주십니다. 회원가입/로그인 폼의 로직 수정하는 업무 또한 현재 리스트업을 해놓았습니다. 추후 메모이제이션을 통한 임시방편이 아닌 리렌더링을 고려한 로직 수정을 통해 본질적인 문제 해결을 하고자 합니다.

profile
Frontend Developer | 기록되지 않은 것은 기억되지 않는다

5개의 댓글

comment-user-thumbnail
2023년 6월 9일

따봉, 잘보고갑니다 :)

1개의 답글
comment-user-thumbnail
2023년 6월 24일

우와ㅎ 잘보고 갑니닷 👍

1개의 답글

관련 채용 정보