-
Notifications
You must be signed in to change notification settings - Fork 0
React Memoization 적절하게 사용하기
HG.Seo edited this page Dec 13, 2022
·
24 revisions
- React 함수 컴포넌트 내부에서 사용하는 함수들의 경우, 재사용(자식에게 내려주는 Props 용도 등)에 대한 안정성이라는 명목으로 useCallback으로 대부분 감싸주고 있었습니다.
- memoization이라는 기능 자체 그리고 종속성의 존재가 어떠한 영향을 주는지 모른 채, 이대로 사용하는 것이 옳은 것인가에 대한 의문이 들었습니다.
- (참고자료)React Memoization에 대한 학습정리(블로그에 작성)
- React에서 활용할 수 있는 Memoization 기능들에 대한 학습을 토대로 Memoization 기능을 활용한 성급한 최적화 시도가 오히려 성능을 저하시킬 수 있다는 것을 확인할 수 있었습니다.
- React의 useCallback Hook 경우, 함수를 재정의해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 합니다. 이 계산은 오히려 단순히 함수를 재정의하는 것보다 비용이 클 수 있습니다.
- 이에, 우리는 이러한 Memoization 기능에 대해 능동적으로 접근하는 것 보다는 성능 문제를 도출한 후에 이를 개선하기 위한 대응책으로 활용하는 등, 분명히 필요할 때 근거를 가지고 사용하는 것이 옳다는 결론을 내릴 수 있었습니다.
- 최소한 기존에 사용하던대로 함수 컴포넌트 내부의 함수들을 모두 useCallback으로 래핑하는 것만큼은 하지 말아야겠다고 결정했습니다.
/** @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>
);
};
- 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';
- 비즈니스 로직을 파악하기 쉽도록 fetch기능은 service.ts라는 별도 services 파일로 분리했습니다.
import { checkIdServerValidation } from './service';
- id 유효성에 대해 여러개로 나뉘어져 있던 상태 기능을 하나의 상태로만 관리할 수 있도록 통합하고 상태코드와 상태안내문은 상수화하여, 코드 안정성을 높였습니다.
const [validationType, setValidationType] = useState<number>(VALIDATION_RESULT.NULL);
/** @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
- 라이트하우스 결과
- (주요 리팩토링 대상) IdInput 컴포넌트 검사 결과
- 성능
- Profiler
- 라이트하우스 결과
- (주요 리팩토링 대상) IdInput 컴포넌트 검사 결과
- 성능
- Profiler
- 비교결과, 라이트하우스 결과에서 보듯이 useCallback을 사용하지 않더라도 전반적인 성능에 큰 영향이 없었습니다.
- 오히려, 리팩토링 과정에서 추가적으로 내부에서 useState로 관리하는 상태값을 줄인 덕분에 오히려 랜더링 횟수가 감소하였고 속도도 빨라졌습니다.
- 이를 통해, useCallback의 유무가 현재 코드 상에서는 전체적인 성능에 영향을 주지 않았고 리팩토링 과정에서 추가적으로 진행한 상태값을 줄이는 리팩토링이 성능향상에 유의미한 결과를 주었음을 확인할 수 있었습니다.
- useCallback의 사용 유무와 상관없다면, 코드복잡성을 줄이기 위해 useCallback을 사용하지 않는 것이 좋을 것입니다.
- 다른 페이지들도 성능분석을 진행한 후에, 분석 결과를 바탕으로 React Memoization(useMemo, useCallback, React.memo 등) 기능이 필요한 지에 대한 근거를 먼저 검토하고 필요한 경우에 적재적소에 활용하여 성능을 향상시킬 수 있도록 하고자 합니다.
- 📃 기획서
- 📂 Backlog
- 📊 ERD, 폴더 구조
- 🗓️ 회의록