Skip to content

React Memoization 적절하게 사용하기

HG.Seo edited this page Dec 13, 2022 · 24 revisions

컴포넌트 내부의 useCallback 남용 이대로 괜찮은걸까?

  • React 함수 컴포넌트 내부에서 사용하는 함수들의 경우, 재사용(자식에게 내려주는 Props 용도 등)에 대한 안정성이라는 명목으로 useCallback으로 대부분 감싸주고 있었습니다.
  • memoization이라는 기능 자체 그리고 종속성의 존재가 어떠한 영향을 주는지 모른 채, 이대로 사용하는 것이 옳은 것인가에 대한 의문이 들었습니다.

Memoization 기능은 공짜가 아니다!

  • (참고자료)React Memoization에 대한 학습정리(블로그에 작성)
  • React에서 활용할 수 있는 Memoization 기능들에 대한 학습을 토대로 Memoization 기능을 활용한 성급한 최적화 시도가 오히려 성능을 저하시킬 수 있다는 것을 확인할 수 있었습니다.
  • React의 useCallback Hook 경우, 함수를 재정의해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 합니다. 이 계산은 오히려 단순히 함수를 재정의하는 것보다 비용이 클 수 있습니다.
  • 이에, 우리는 이러한 Memoization 기능에 대해 능동적으로 접근하는 것 보다는 성능 문제를 도출한 후에 이를 개선하기 위한 대응책으로 활용하는 등, 분명히 필요할 때 근거를 가지고 사용하는 것이 옳다는 결론을 내릴 수 있었습니다.
  • 최소한 기존에 사용하던대로 함수 컴포넌트 내부의 함수들을 모두 useCallback으로 래핑하는 것만큼은 하지 말아야겠다고 결정했습니다.

Refactoring

(예) 회원가입화면 ID 입력 컴포넌트

image

Before

/** @jsxImportSource @emotion/react */

import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { css } from '@emotion/react';

import { API, RESULT } from 'utils/constants';

import {
  // ...중략...
} from './styles';

export const IdInput = ({ setId }: { setId: Dispatch<SetStateAction<string>> }) => {
  const [idDraft, setIdDraft] = useState<string>('');
  const [idWarning, setIdWarning] = useState<string>('');
  const [idDuplicationCheckResult, setIdDuplicationCheckResult] = useState<string>('');

  // 아이디값 입력에 따른 상태관리
  const handleOnChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setIdDraft(e.target.value);
  }, []);

  // 서버측 id 유효성 검사를 위해 fetch 통신(쿼리스트링)
  const sendIdToServer = useCallback(() => {
    fetch(`${process.env.REACT_APP_FETCH_URL}${API.VALIDATE}?${new URLSearchParams({ id: idDraft })}`)
      .then((res) => res.json())
      // ...중략...
  }, [idDraft]);

  // 클라이언트측 id 유효성 검사
  // 아이디 요소 확인
  const isValidIdStr = useCallback((id: string) => {
    // ...중략...
  }, []);

  // 아이디 길이 확인
  const isValidIdLength = useCallback((id: string) => {
    // ...중략...
  }, []);

  // 아이디 유효성 검사
  const isValidId = useCallback(() => {
    // ...중략...
  }, [idDraft]);

  // id값이 유효하면 서버로 보내주기
  const handleClick = useCallback(() => {
    // ...중략...
  }, [idDraft]);

  // 사용자가 id값을 입력할때마다 검사
  useEffect(() => {
    // ...중략...
  }, [idDraft]);

  const isAllValid = useCallback(() => {
    // ...중략...
  }, [idWarning, idDuplicationCheckResult]);

  return (
    <div>
      <div css={registerPageInputWrapperStyle}>
        // ...중략...
    </div>
  );
};

Refactoring 과정

useCallback 남용 리팩토링

  • useCallback이 필요하거나 useCallback을 적용해도 비용이 크지 않으리라 생각되는 함수에만 적용하고자 했습니다.
// 사용자의 입력값 변화마다 호출되므로 useCallback으로 최적화
  const handleOnChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setIdDraft(e.target.value);
  }, []);

// id값이 유효하면 서버로 보내주기
  // 버튼 클릭이 발생할 때만 일어나는 이벤트이고 id입력 시마다 client측 유효성 검사를 진행하고 있으므로 굳이 useCallback을 적용할만큼 자주 일어나진 않음
  const handleClick = () => {
    if (!isValidId(idDraft)) {
      return;
    }
    // 아이디값 서버측 유효성 검사
    checkIdServerValidation(idDraft)

// ...중략...
  • 기존에 useCallback으로 감싸져있던 내부 함수들 중에 자주 사용되지만 함수 컴포넌트 내의 상태관리에 관여하지 않고 주요 비즈니스 로직이라 판단되지 않는 함수는 별도 utils 파일에 분리 후 import해서 사용했습니다.
// 기존 id 유효성 검사를 별도 util파일로 분리
import { isValidId, isValidIdLength, isValidIdStr } from './util';

기타 추가 Refactoring 사항

  • 비즈니스 로직을 파악하기 쉽도록 fetch기능은 service.ts라는 별도 services 파일로 분리했습니다.
import { checkIdServerValidation } from './service';
  • id 유효성에 대해 여러개로 나뉘어져 있던 상태 기능을 하나의 상태로만 관리할 수 있도록 통합하고 상태코드와 상태안내문은 상수화하여, 코드 안정성을 높였습니다.
const [validationType, setValidationType] = useState<number>(VALIDATION_RESULT.NULL);

After

/** @jsxImportSource @emotion/react */

import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';

import { checkIdServerValidation } from './service';
import { isValidId, isValidIdLength, isValidIdStr } from './util';
import { VALIDATION_INFO, VALIDATION_RESULT } from './constants';

import { idButtonStyle, idInputStyle, idInputWrapperStyle, idValidationStyle } from './idInput.styles';

export const IdInput = ({ setId }: { setId: Dispatch<SetStateAction<string>> }) => {
  // 유효성이 확정되지 않은 예비 ID 값
  const [idDraft, setIdDraft] = useState<string>('');
  const [validationType, setValidationType] = useState<number>(VALIDATION_RESULT.NULL);

  // 아이디값 입력에 따른 상태관리
  // 사용자의 입력값 변화마다 호출되므로 useCallback으로 최적화
  const handleOnChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setIdDraft(e.target.value);
  }, []);

  // id값이 유효하면 서버로 보내주기
  // 버튼 클릭이 발생할 때만 일어나는 이벤트이고 id입력 시마다 client측 유효성 검사를 진행하고 있으므로 굳이 useCallback을 적용할만큼 자주 일어나진 않음
  const handleClick = () => {
      // ...중략...
  };

  // 사용자가 id값을 입력할때마다 유효성 검사 결과를 알려주어 UX 향상
  useEffect(() => {
    // ...중략...
  }, [idDraft]);

  return (
    <>
      <div css={idInputWrapperStyle(validationType)}>
        <input placeholder='아이디' value={idDraft} onChange={handleOnChange} css={idInputStyle} />
        <button type='button' onClick={handleClick} css={idButtonStyle}>
          <span>중복확인</span>
        </button>
      </div>
      {validationType !== VALIDATION_RESULT.NULL && (
        <span css={idValidationStyle(validationType)}>{VALIDATION_INFO[validationType]}</span>
      )}
    </>
  );
};

이번 리팩토링은 성능에 유의미한 영향이 있었을까?

테스팅 동작

bandicam.2022-12-04.16-02-16-900.mp4

Before

  1. 라이트하우스 결과 image
  2. (주요 리팩토링 대상) IdInput 컴포넌트 검사 결과
  • 성능 image
  • Profiler image

After

  1. 라이트하우스 결과 image
  2. (주요 리팩토링 대상) IdInput 컴포넌트 검사 결과
  • 성능 image
  • Profiler image

결과에 대한 분석

  • 비교결과, 라이트하우스 결과에서 보듯이 useCallback을 사용하지 않더라도 전반적인 성능에 큰 영향이 없었습니다.
  • 오히려, 리팩토링 과정에서 추가적으로 내부에서 useState로 관리하는 상태값을 줄인 덕분에 오히려 랜더링 횟수가 감소하였고 속도도 빨라졌습니다.
  • 이를 통해, useCallback의 유무가 현재 코드 상에서는 전체적인 성능에 영향을 주지 않았고 리팩토링 과정에서 추가적으로 진행한 상태값을 줄이는 리팩토링이 성능향상에 유의미한 결과를 주었음을 확인할 수 있었습니다.
  • useCallback의 사용 유무와 상관없다면, 코드복잡성을 줄이기 위해 useCallback을 사용하지 않는 것이 좋을 것입니다.

앞으로 남은 과제

  • 다른 페이지들도 성능분석을 진행한 후에, 분석 결과를 바탕으로 React Memoization(useMemo, useCallback, React.memo 등) 기능이 필요한 지에 대한 근거를 먼저 검토하고 필요한 경우에 적재적소에 활용하여 성능을 향상시킬 수 있도록 하고자 합니다.

얼리버드

프로젝트

개발일지

스프린트 계획

멘토링

데일리 스크럼

데일리 개인 회고

위클리 그룹 회고

스터디

Clone this wiki locally