Project Soljik DevLog
  • Project Soljik 소개 문서
  • 프로젝트 소개
    • 솔직한 온라인 쇼핑, Soljik
    • 기술 스택
    • 컨벤션
  • 최적화
    • 이미지 최적화
    • Lazy Loading / Code Splitting
    • 렌더링 최적화
  • 트러블 슈팅
    • BaaS(Firebase) 환경에서의 세션 관리
    • Firebase 저장소의 CORS 에러 해결
    • 다음 우편번호 API 적용하기
Powered by GitBook
On this page
  • 렌더링 최적화의 필요성
  • 최적화 방법 / 코드 예시
  • 후기
  1. 최적화

렌더링 최적화

React 프로젝트의 렌더링을 최적화한 과정을 정리한 문서입니다.

PreviousLazy Loading / Code SplittingNextBaaS(Firebase) 환경에서의 세션 관리

Last updated 1 month ago

렌더링 최적화의 필요성

React 애플리케이션에서는 상태(state)나 props가 변경될 때 컴포넌트가 다시 렌더링됩니다. 특히 어떤 컴포넌트가 리렌더링되면 그 하위의 모든 자식 컴포넌트들도 함께 리렌더링되는 특징이 있습니다.

이는 UI 업데이트를 일관성 있게 해주지만, 규모가 큰 프로젝트에서는 불필요한 리렌더링이 성능 저하로 이어질 수 있습니다. 예를 들어 변경되지도 않은 컴포넌트까지 반복 렌더링되면 대량의 리스트를 렌더링하는 경우 프레임 드랍이나 인터랙션 지연을 유발할 수 있습니다.


최적화 방법 / 코드 예시

이번 문서에서는 크롬의 React Developer Tools 확장 프로그램을 활용해 상품 구매 페이지에 렌더링 최적화를 적용하는 과정을 정리합니다.

먼저 상품 구매 페이지는 크게 두 섹션으로 나뉩니다.

  1. 배송 정보 입력 : 구매자의 정보를 입력받는 섹션

  2. 장바구니 정보 : 장바구니에 담긴 상품의 수량을 조정하고, 총 가격 확인과 함께 구매를 트리거할 수 있는 섹션

각각의 섹션에 대해 순서대로 최적화 방법을 알아보겠습니다.

1. 배송 정보 입력

초기의 코드에서는 입력란에 사용한 input 컴포넌트의 onChange 핸들러를 통해 배송 정보의 state 를 업데이트 했습니다. 구매자 항목의 예시 코드를 먼저 보겠습니다.

// 로직 부분
const [buyerName, setBuyerName] = useState<string>('');
const buyerNameErrorMessage = useMemo<string>(()=>{
  const trimmedName = buyerName.trim();
  if (trimmedName) {
    if (/^[가-힣\x20]*$/.test(trimmedName)) return '';
    else return '한글만 사용해주세요.';
  } else {
    return '공백이 아닌 내용을 입력해주세요.';
  }
}, [buyerName]);

// ui 부분
<S.PurchaseStateInput
  title='구매자'
  onChange={(e) => setBuyerName(e.target.value)}
  errorMessage={buyerNameErrorMessage}
/>

이 경우 사용자가 값을 입력할 때 마다 state 가 변화하기 때문에, 매 입력마다 Form 컴포넌트 전체가 리렌더링됩니다.

리렌더링을 방지하기 위해 입력값을 useRef 로 사용할 수 있지만 이 경우 유효성 검사를 실시간으로 확인하기 어렵다는 문제점이 있습니다.

react-hook-form 의 동작 원리는 다음과 같습니다.

  • 입력 초기에는 Ref 기반으로 동작합니다.

  • submit 이 발생한 후로는 유효성 검사를 통해 에러 상태가 변할 때에만 리렌더링 됩니다.

이러한 원리를 통해 렌더링을 최적화할 수 있으며, 유효성 검사 관련 코드의 단순화와 통일성을 챙길 수 있습니다. 그 외에도 다양한 내부 속성이나 메서드들을 지원합니다.

react-hook-form 을 적용하는 기본적인 로직 코드만 살펴보고, 적용 이후의 렌더링 변화를 살펴보겠습니다.

// 로직 부분
const { register, formState: { errors } } = useForm<PurchaseFormData>();
const registerObject = register('buyerName', {
  required: '구매자는 필수 입력입니다.',
  onBlur: (event) => {
    event.target.value = event.target.value.trim();
  },
  validate: (value: string) =>
    !!value.trim() || '공백이 아닌 내용을 입력해주세요.',
  pattern: {
    value: /^[가-힣\x20]*$/,
    message: '한글만 사용해주세요.',
  },
  minLength: {
    value: 2,
    message: '이름은 2자 이상 10자 이하여야 합니다.',
  },
  maxLength: {
    value: 10,
    message: '이름은 2자 이상 10자 이하여야 합니다.',
  },
})

// ui 부분
<S.PurchaseInput
  title='구매자'
  { ...registerObject }
  aria-errormessage={errors.buyerName && errors.buyerName.message}
/>

이제 처음에는 입력값이 변해도 렌더링 되지 않다가, Submit 이 실행된 이후 유효성 검사 결과에 따라 errors 상태가 변할때만 리렌더링 되는것을 확인할 수 있습니다.

2. 장바구니 정보

장바구니 정보는 결제 페이지 뿐 아니라 모든 구매자 페이지에서 확인할 수 있어야 하기 때문에, React Context 를 활용해 구현되어 있습니다.

문제점은 Context 내부에 정의된 장바구니 목록 state 가 하위 컴포넌트 뿐 아니라 최상위 컴포넌트에서도 사용되기 때문에, 장바구니 목록 내 상품의 수량을 수정할 경우 전체 컴포넌트가 반복적으로 렌더링된다는 점입니다.

이 현상을 해결하기 위해서는 2가지 작업이 선행되어야 합니다.

  1. 장바구니 목록 state 와 동일한 정보를 저장하는 ref 를 만들어서 사용합니다.

  2. 전체 컨텍스트를 2개의 컨텍스트로 분리합니다.

    1. State 컨텍스트(UI 전용) : 장바구니 목록, 총 가격, etc...

    2. Actions 컨텍스트(로직 전용) : 장바구니 목록 Ref Getter, 장바구니 상품 추가, 장바구니 상품 업데이트, etc...

위에 설명된 내용 중 Actions 컨텍스트에노란색으로 강조된 부분을 보시면 장바구니 목록 Ref Getter 라는 항목이 있습니다.

이 항목은 UI 가 아닌 로직에서 장바구니 정보가 필요할 때, 리렌더링을 유발시킬 수 있는 state 대신 동일한 정보를 담고있는 ref 를 사용하기 위한 메서드입니다. 코드를 요약해 살펴보겠습니다.

// State Context 및 Actions Context 의 타입
type CartItemsStateContextType = {
  items: CartItem[];
  checkItemIsInCart: (product: ProductSchema | undefined) => boolean;
  cartSize: number;
  totalPrice: number;
};

type CartItemsActionsContextType = {
  addItem: (product: ProductSchema, quantity: number) => void;
  updateItem: (product: ProductSchema, quantity: number, max?: number) => void;
  removeItem: (product: ProductSchema) => void;
  clearCart: () => void;
  getItemsRef: () => React.MutableRefObject<CartItem[]>;
};

/**
 * 장바구니 내 데이터를 저장할 Context 의 Provider
 * LocalStorage 를 체크해서 장바구니 상태를 초기화합니다.
 */
export const CartItemsProvider = ({ children }: ReactNode) => {
  // ...생략
  
  // 장바구니 목록의 state, ref 생성
  const [items, setItems, itemsRef] = useStateWithRef<CartItem[]>([]);

  // 장바구니 목록 ref getter
  const getItemsRef = useCallback(() => itemsRef, [itemsRef]);

  // State Context Provider 의 value
  const stateValue = useMemo(() => {
    return { items, cartSize, totalPrice, checkItemIsInCart };
  }, [items, cartSize, totalPrice, checkItemIsInCart]);

  // Actions Context Provider 의 value
  const actionsValue = useMemo(() => {
    return {
      addItem,
      updateItem,
      removeItem,
      clearCart,
      getItemsRef,
    };
  }, [addItem, updateItem, removeItem, clearCart, getItemsRef]);

  return (
    <CartItemsStateContext.Provider value={stateValue}>
      <CartItemsActionsContext.Provider value={actionsValue}>
        {children}
      </CartItemsActionsContext.Provider>
    </CartItemsStateContext.Provider>
  );
};

위 코드에서 state 와 ref 를 병용하기 위해 사용한 useStateWithRef 커스텀 훅은 아래와 같이 구현했습니다.

// useState와 useRef를 동기화해서 같이 쓸 수 있는 커스텀 훅
export function useStateWithRef<T>(
  initialValue: T,
): [T, (value: T | ((prev: T) => T)) => void, React.MutableRefObject<T>] {
  const [state, setState] = useState<T>(initialValue);
  const ref = useRef<T>(state);

  // state 의 setter 와 ref 수정을 묶어서 사용합니다.
  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    setState((prev) => {
      let next;
      if (typeof value === 'function') next = (value as (prev: T) => T)(prev);
      else next = value;

      ref.current = next;
      return next;
    });
  }, []);

  return [state, setValue, ref];
}

그 외에도 상품 구매 페이지 내부의 하위 컴포넌트들(배송 정보 입력, 장바구니 정보, 구매 확인 체크박스, etc...)에 React.memo 메서드를 적용하여 상위 컴포넌트 state 변화 시 필요없는 리렌더링을 줄였습니다.

그럼 이제 코드 수정 후 렌더링의 변화를 살펴보겠습니다.

이제 장바구니 목록 state 가 변할 때, State Context 를 사용중인 컴포넌트(장바구니 정보, 총 가격)만 리렌더링이 발생하는 것을 확인할 수 있습니다.


후기

React 프로젝트에서 렌더링을 최적화하기 위한 방법이 여러가지가 있지만, 기본적으로 컴포넌트의 구조를 잘 만들지 않으면 최적화하기가 어렵다는 것을 느꼈습니다.

프로젝트를 개발에 있어서 React 라이브러리 이해도 뿐 아니라 컴포넌트 구조의 중요성을 느끼게 해준 과정이었습니다.

이러한input 과정의 문제를 해결하기 위해 라이브러리를 도입했습니다.

react-hook-form
상품 구매 페이지
렌더링 최적화 적용 전
react-hook-form 도입 후
렌더링 최적화 적용 전
렌더링 최적화 적용 후