Skip to content

React Memoization 적절하게 사용하기

HG.Seo edited this page Dec 4, 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 {
  idValidationStyle,
  registerPageIdButtonStyle,
  registerPageInputStyle,
  registerPageInputWrapperStyle,
} 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 통신(쿼리스트링)
  // code 10000 : 유효한ID, 10001 : 유효하지않음, 10002: 중복됨
  const sendIdToServer = useCallback(() => {
    fetch(`${process.env.REACT_APP_FETCH_URL}${API.VALIDATE}?${new URLSearchParams({ id: idDraft })}`)
      .then((res) => res.json())
      .then((res) => {
        if (res.code === 10000) {
          setId(idDraft);
          setIdDuplicationCheckResult('유효한 아이디 입니다.');
          return;
        }
        if (res.code === 20001) {
          setIdWarning('유효하지 않은 Id 형식입니다.');
          return;
        }
        if (res.code === 20002) {
          setIdWarning('중복되는 Id 입니다.');
        }
      })
      .catch(() => {
        setIdWarning('중복검사에 실패했습니다.');
      });
  }, [idDraft]);

  // 클라이언트측 id 유효성 검사
  // 아이디 요소 확인
  const isValidIdStr = useCallback((id: string) => {
    const regexEngNum = /^[a-zA-Z0-9]*$/;
    return regexEngNum.test(id);
  }, []);

  // 아이디 길이 확인
  const isValidIdLength = useCallback((id: string) => {
    if (id.length == 0) return true;
    return id.length >= 4 && id.length <= 15;
  }, []);

  // 아이디 유효성 검사
  const isValidId = useCallback(() => {
    if (!isValidIdLength(idDraft)) return false;
    return isValidIdStr(idDraft);
  }, [idDraft]);

  // id값이 유효하면 서버로 보내주기
  const handleClick = useCallback(() => {
    if (!isValidId()) {
      setIdWarning('4글자 이상, 15글자 이하의 알파벳과 숫자로 작성바랍니다.');
      return;
    }
    sendIdToServer();
  }, [idDraft]);

  // 사용자가 id값을 입력할때마다 검사
  useEffect(() => {
    setIdDuplicationCheckResult('');
    if (!isValidIdStr(idDraft)) {
      setIdWarning('알파벳과 숫자로만 이루어져야 합니다.');
      return;
    }
    if (!isValidIdLength(idDraft)) {
      setIdWarning('4글자 이상 15글자 이하만 가능합니다.');
      return;
    }
    setIdWarning('');
  }, [idDraft]);

  const isAllValid = useCallback(() => {
    if (idWarning.length > 0) return RESULT.FAIL;
    if (idDuplicationCheckResult.length > 0) return RESULT.SUCCESS;
    return RESULT.NULL;
  }, [idWarning, idDuplicationCheckResult]);

  return (
    <div>
      <div css={registerPageInputWrapperStyle}>
        <input
          css={css(registerPageInputStyle, { width: 300 })}
          placeholder='아이디'
          value={idDraft}
          onChange={handleOnChange}
        />
        <button type='button' css={registerPageIdButtonStyle} onClick={handleClick}>
          <span>중복확인</span>
        </button>
      </div>
      {isAllValid() !== RESULT.NULL && (
        <span css={idValidationStyle(isAllValid())}>
          {isAllValid() === RESULT.FAIL ? idWarning : idDuplicationCheckResult}
        </span>
      )}
    </div>
  );
};

Refactoring 과정

useCallback 남용 리팩토링

  • 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)

...(중략)...
  • 자주 사용되지만, 함수 컴포넌트 내의 상태관리에 관여하지 않고 주요 비즈니스 로직이라 판단되지 않는 함수는 별도 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 = () => {
    if (!isValidId(idDraft)) {
      return;
    }
    // 아이디값 서버측 유효성 검사
    checkIdServerValidation(idDraft)
      .then((res) => res.json())
      // code 10000 : 유효한ID, 10001 : 유효하지않음, 10002: 중복됨
      .then((res) => {
        if (res.code === 10000) {
          setId(idDraft);
          setValidationType(VALIDATION_RESULT.SUCCESS);
          return;
        }
        if (res.code === 20001) {
          setValidationType(VALIDATION_RESULT.WRONG_STR);
          return;
        }
        if (res.code === 20002) {
          setValidationType(VALIDATION_RESULT.DUPLICATED);
        }
      })
      .catch(() => {
        setValidationType(VALIDATION_RESULT.NULL);
        alert('잠시 후 다시 시도해주세요.');
      });
  };

  // 사용자가 id값을 입력할때마다 유효성 검사 결과를 알려주어 UX 향상
  useEffect(() => {
    setValidationType(VALIDATION_RESULT.NULL);
    if (!isValidIdStr(idDraft)) {
      setValidationType(VALIDATION_RESULT.WRONG_STR);
      return;
    }
    if (!isValidIdLength(idDraft)) {
      setValidationType(VALIDATION_RESULT.WRONG_LENGTH);
      return;
    }
    setValidationType(VALIDATION_RESULT.NULL);
  }, [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>
      )}
    </>
  );
};

앞으로 남은 과제

  • 성능분석을 진행한 후에, 분석 결과에 따라 React Memoization(useMemo, useCallback, React.memo 등) 기능이 필요한 경우 적재적소에 활용하여 성능을 향상시킬 수 있도록 하고자 합니다.

얼리버드

프로젝트

개발일지

스프린트 계획

멘토링

데일리 스크럼

데일리 개인 회고

위클리 그룹 회고

스터디

Clone this wiki locally